< Summary - Kestrun — Combined Coverage

Information
Class: Kestrun.Runtime.PowerShellOpenApiClassExporter
Assembly: Kestrun
File(s): /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Runtime/PowerShellOpenApiClassExporter.cs
Tag: Kestrun/Kestrun@eeafbe813231ed23417e7b339e170e307b2c86f9
Line coverage
76%
Covered lines: 484
Uncovered lines: 147
Coverable lines: 631
Total lines: 1589
Line coverage: 76.7%
Branch coverage
62%
Covered branches: 307
Total branches: 490
Branch coverage: 62.6%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 12/12/2025 - 17:27:19 Line coverage: 91.2% (94/103) Branch coverage: 75% (42/56) Total lines: 313 Tag: Kestrun/Kestrun@826bf9dcf9db118c5de4c78a3259bce9549f0dcd12/18/2025 - 21:41:58 Line coverage: 91.4% (96/105) Branch coverage: 75.8% (44/58) Total lines: 317 Tag: Kestrun/Kestrun@0d738bf294e6281b936d031e1979d928007495ff01/02/2026 - 00:16:25 Line coverage: 34.4% (101/293) Branch coverage: 25.2% (48/190) Total lines: 791 Tag: Kestrun/Kestrun@8405dc23b786b9d436fba0d65fb80baa4171e1d001/02/2026 - 21:56:10 Line coverage: 92.7% (319/344) Branch coverage: 86.1% (193/224) Total lines: 913 Tag: Kestrun/Kestrun@f60326065ebb24cf70b241e459b37baf142e6ed601/14/2026 - 07:55:07 Line coverage: 92.7% (320/345) Branch coverage: 86.2% (195/226) Total lines: 916 Tag: Kestrun/Kestrun@13bd81d8920e7e63e39aafdd188e7d766641ad3501/17/2026 - 04:33:35 Line coverage: 92.2% (368/399) Branch coverage: 84.8% (229/270) Total lines: 1085 Tag: Kestrun/Kestrun@aca34ea8d284564e2f9f6616dc937668dce926ba01/21/2026 - 17:07:46 Line coverage: 85.2% (388/455) Branch coverage: 77.5% (239/308) Total lines: 1215 Tag: Kestrun/Kestrun@3f6f61710c7ef7d5953cab578fe699c1e5e01a3602/05/2026 - 00:28:18 Line coverage: 86.4% (459/531) Branch coverage: 77% (282/366) Total lines: 1383 Tag: Kestrun/Kestrun@d9261bd752e45afa789d10bc0c82b7d5724d958902/18/2026 - 08:33:07 Line coverage: 76.7% (484/631) Branch coverage: 62.6% (307/490) Total lines: 1589 Tag: Kestrun/Kestrun@bf8a937cfb7e8936c225b9df4608f8ddd85558b1 12/12/2025 - 17:27:19 Line coverage: 91.2% (94/103) Branch coverage: 75% (42/56) Total lines: 313 Tag: Kestrun/Kestrun@826bf9dcf9db118c5de4c78a3259bce9549f0dcd12/18/2025 - 21:41:58 Line coverage: 91.4% (96/105) Branch coverage: 75.8% (44/58) Total lines: 317 Tag: Kestrun/Kestrun@0d738bf294e6281b936d031e1979d928007495ff01/02/2026 - 00:16:25 Line coverage: 34.4% (101/293) Branch coverage: 25.2% (48/190) Total lines: 791 Tag: Kestrun/Kestrun@8405dc23b786b9d436fba0d65fb80baa4171e1d001/02/2026 - 21:56:10 Line coverage: 92.7% (319/344) Branch coverage: 86.1% (193/224) Total lines: 913 Tag: Kestrun/Kestrun@f60326065ebb24cf70b241e459b37baf142e6ed601/14/2026 - 07:55:07 Line coverage: 92.7% (320/345) Branch coverage: 86.2% (195/226) Total lines: 916 Tag: Kestrun/Kestrun@13bd81d8920e7e63e39aafdd188e7d766641ad3501/17/2026 - 04:33:35 Line coverage: 92.2% (368/399) Branch coverage: 84.8% (229/270) Total lines: 1085 Tag: Kestrun/Kestrun@aca34ea8d284564e2f9f6616dc937668dce926ba01/21/2026 - 17:07:46 Line coverage: 85.2% (388/455) Branch coverage: 77.5% (239/308) Total lines: 1215 Tag: Kestrun/Kestrun@3f6f61710c7ef7d5953cab578fe699c1e5e01a3602/05/2026 - 00:28:18 Line coverage: 86.4% (459/531) Branch coverage: 77% (282/366) Total lines: 1383 Tag: Kestrun/Kestrun@d9261bd752e45afa789d10bc0c82b7d5724d958902/18/2026 - 08:33:07 Line coverage: 76.7% (484/631) Branch coverage: 62.6% (307/490) Total lines: 1589 Tag: Kestrun/Kestrun@bf8a937cfb7e8936c225b9df4608f8ddd85558b1

Coverage delta

Coverage delta 61 -61

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_ValidClassNames()100%11100%
ExportOpenApiClasses(...)50%22100%
ExportOpenApiClasses(...)95%202096.87%
GetLoadableTypes(...)100%1133.33%
AppendCallback(...)75%44100%
NormalizeBlankLines(...)100%66100%
BuildCallbackFunctionStub(...)100%1616100%
TryExtractParamInfo(...)75%8881.81%
ExtractPowerShellParamBlock(...)81.25%171685.71%
TryConsumeQuoted(...)54.16%962450%
ExtractBodyParameterName(...)92.85%141493.33%
ExtractParamNamesFromStrippedParamBlock(...)90%202095%
StripPowerShellAttributeBlocks(...)94.44%181893.93%
HasOpenApiComponentAttribute(...)100%11100%
AppendClass(...)75%8890.9%
ResolveClassBaseClause(...)100%1010100%
AppendAdditionalPropertiesMembers(...)100%44100%
TryBuildKrBindFormAttribute(...)66.66%66100%
GetRequiredProperties(...)50%9875%
AppendRequiredPropertiesMetadata(...)0%2040%
AppendRequiredPropertiesValidationMethods(...)100%210%
ShouldEmitAdditionalProperties(...)62.5%8883.33%
BuildAdditionalPropertiesMetadata(...)69.23%2626100%
AppendOpenApiXmlMetadataProperty(...)71.42%241463.33%
BuildXmlMetadataHashtable(...)0%600240%
EscapePowerShellString(...)100%11100%
AppendValidationAttributes(...)50%2285.71%
TryFormatValidationAttribute(...)50%3383638.46%
FormatValidateRange(...)0%7280%
FormatValidateLength(...)0%7280%
FormatValidateCount(...)0%7280%
FormatValidatePattern(...)0%2040%
FormatValidateSet(...)0%4260%
FormatPowerShellLiteral(...)0%1332360%
ToPowerShellTypeName(...)100%1212100%
GetNullableTypeName(...)100%22100%
GetOpenApiArrayWrapperTypeName(...)100%11100%
GetCollapsedOpenApiPrimitiveTypeName(...)100%66100%
GetEnumTypeName(...)100%22100%
GetPrimitiveTypeName(...)100%11100%
GetArrayTypeName(...)100%44100%
FormatComponentOrFallbackName(...)100%44100%
CollectExportableEnums(...)100%66100%
FindEnumsInType()56.25%251666.66%
AppendEnum(...)62.5%8890.9%
ResolveElementArrayType(...)90%101083.33%
.cctor()100%11100%
ResolvePrimitiveTypeName(...)100%44100%
TryGetOpenApiValueUnderlyingType(...)90%1010100%
TryGetArrayComponentElementType(...)62.5%181678.94%
TopologicalSortByPropertyDependencies(...)100%22100%
Visit(...)100%88100%
GetComponentDependencyType(...)100%66100%
TryGetFormPayloadBasePsName(...)66.66%66100%
WriteOpenApiTempScript(...)100%11100%

File(s)

/home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Runtime/PowerShellOpenApiClassExporter.cs

#LineLine coverage
 1using System.Reflection;
 2using System.Text;
 3
 4namespace Kestrun.Runtime;
 5
 6/// <summary>
 7/// Exports OpenAPI component classes as PowerShell class definitions.
 8/// </summary>
 9public static class PowerShellOpenApiClassExporter
 10{
 11    /// <summary>
 12    /// Holds valid class names to be used as type in the OpenAPI function definitions.
 13    /// </summary>
 2814    public static List<string> ValidClassNames { get; } = [];
 15
 16    /// <summary>
 17    /// Exports OpenAPI component classes found in loaded assemblies
 18    /// as PowerShell class definitions.
 19    /// </summary>
 20    /// <param name="userCallbacks">Optional user-defined functions to include in the export.</param>
 21    /// <returns>The path to the temporary PowerShell script containing the class definitions.</returns>
 22    public static string ExportOpenApiClasses(Dictionary<string, string>? userCallbacks)
 23    {
 6324        var assemblies = AppDomain.CurrentDomain.GetAssemblies()
 1959925           .Where(a => a.FullName is not null &&
 1959926                    a.FullName.Contains("PowerShell Class Assembly"))
 6327           .ToArray();
 6328        return ExportOpenApiClasses(assemblies: assemblies, userCallbacks: userCallbacks);
 29    }
 30
 31    /// <summary>
 32    /// Exports OpenAPI component classes found in the specified assemblies
 33    /// as PowerShell class definitions
 34    /// </summary>
 35    /// <param name="assemblies">The assemblies to scan for OpenAPI component classes.</param>
 36    ///  <param name="userCallbacks"> Optional user-defined functions to include in the export.</param>
 37    /// <returns>The path to the temporary PowerShell script containing the class definitions.</returns>
 38    public static string ExportOpenApiClasses(Assembly[] assemblies, Dictionary<string, string>? userCallbacks)
 39    {
 40        // 1. Collect all component classes
 6741        var componentTypes = assemblies
 6742            .SelectMany(GetLoadableTypes)
 139643            .Where(t => t.IsClass && !t.IsAbstract)
 6744            .Where(HasOpenApiComponentAttribute)
 6745            .ToList();
 46
 47        // Collect any enums required by the component graph.
 48        // If a class property uses an enum type constraint, that enum must exist in the session
 49        // before the class definition is parsed.
 6750        var enumTypes = CollectExportableEnums(componentTypes)
 151            .OrderBy(t => t.Name, StringComparer.Ordinal)
 6752            .ToList();
 53
 54        // For quick lookup when choosing type names
 6755        var componentSet = new HashSet<Type>(componentTypes);
 56
 57        // 2. Topologically sort by "uses other component as property type"
 6758        var sorted = TopologicalSortByPropertyDependencies(componentTypes, componentSet);
 6759        var hasCallbacks = userCallbacks is not null && userCallbacks.Count > 0;
 60
 61        // nothing to export
 6762        if (sorted.Count == 0 && !hasCallbacks)
 63        {
 6364            return string.Empty;
 65        }
 66
 67        // 3. Emit PowerShell classes (and optional callback functions)
 468        var sb = new StringBuilder();
 69
 470        if (enumTypes.Count > 0)
 71        {
 172            _ = sb.AppendLine("# ================================================");
 173            _ = sb.AppendLine("#   Kestrun OpenAPI Autogenerated Enum Definitions");
 174            _ = sb.AppendLine("# ================================================");
 175            _ = sb.AppendLine();
 76
 477            foreach (var enumType in enumTypes)
 78            {
 179                AppendEnum(enumType, sb);
 180                _ = sb.AppendLine(); // blank line between enums
 81            }
 82        }
 83
 3484        foreach (var type in sorted)
 85        {
 86            // Skip types without full name (should not happen)
 1387            if (type.FullName is null)
 88            {
 89                continue;
 90            }
 1391            if (ValidClassNames.Contains(type.FullName))
 92            {
 93                // Already registered remove old entry
 094                _ = ValidClassNames.Remove(type.FullName);
 95            }
 96            // Register valid class name
 1397            ValidClassNames.Add(type.FullName);
 98            // Emit class definition
 1399            AppendClass(type, componentSet, sb);
 13100            _ = sb.AppendLine(); // blank line between classes
 101        }
 102
 4103        if (hasCallbacks)
 104        {
 2105            AppendCallback(sb, userCallbacks);
 106        }
 107        // 4. Write to temp script file
 4108        return WriteOpenApiTempScript(sb.ToString());
 109    }
 110
 111    /// <summary>
 112    /// Safely retrieves loadable types from an assembly, handling partial load failures.
 113    /// </summary>
 114    /// <param name="assembly">The assembly to inspect.</param>
 115    /// <returns>All loadable types from the assembly.</returns>
 116    private static IEnumerable<Type> GetLoadableTypes(Assembly assembly)
 117    {
 118        try
 119        {
 53120            return assembly.GetTypes();
 121        }
 0122        catch (ReflectionTypeLoadException ex)
 123        {
 0124            return ex.Types.Where(static t => t is not null).Cast<Type>();
 125        }
 0126        catch
 127        {
 0128            return [];
 129        }
 53130    }
 131
 132    /// <summary>
 133    /// Appends user-defined callback functions to the PowerShell script.
 134    /// </summary>
 135    /// <param name="sb"> The StringBuilder to append the callback functions to. </param>
 136    /// <param name="userCallbacks"> The dictionary of user-defined callback functions, where the key is the function na
 137    private static void AppendCallback(StringBuilder sb, Dictionary<string, string>? userCallbacks)
 138    {
 2139        _ = sb.AppendLine("# ================================================");
 2140        _ = sb.AppendLine("#   Kestrun User Callback Functions");
 2141        _ = sb.AppendLine("# ================================================");
 2142        _ = sb.AppendLine();
 143
 13144        foreach (var kvp in userCallbacks!.OrderBy(k => k.Key, StringComparer.OrdinalIgnoreCase))
 145        {
 3146            var name = kvp.Key;
 3147            var definition = kvp.Value ?? string.Empty;
 148
 149            // Emit a standardized callback function wrapper:
 150            // - keeps parameter type constraints
 151            // - strips OpenAPI/Parameter attributes
 152            // - builds $params and calls $Context.Response.AddCallbackParameters(...)
 3153            var functionScript = BuildCallbackFunctionStub(name, definition);
 3154            var normalized = NormalizeBlankLines(functionScript);
 3155            _ = sb.AppendLine(normalized);
 3156            _ = sb.AppendLine();
 157        }
 2158    }
 159
 160    /// <summary>
 161    /// Normalizes blank lines in the provided PowerShell script.
 162    /// </summary>
 163    /// <param name="script">The PowerShell script as a string.</param>
 164    /// <returns>A string with normalized blank lines.</returns>
 165    private static string NormalizeBlankLines(string script)
 166    {
 6167        if (string.IsNullOrWhiteSpace(script))
 168        {
 1169            return string.Empty;
 170        }
 171
 172        // Normalize newlines first
 5173        script = script.Replace("\r\n", "\n").Replace("\r", "\n");
 174
 5175        var lines = script.Split('\n');
 5176        var sb = new StringBuilder(script.Length);
 177
 174178        for (var idx = 0; idx < lines.Length; idx++)
 179        {
 82180            var line = lines[idx].TrimEnd();
 82181            var isBlank = string.IsNullOrWhiteSpace(line);
 182
 183            // For callback function export we want compact output:
 184            // drop ALL whitespace-only lines (attribute stripping leaves many single blank lines).
 82185            if (!isBlank)
 186            {
 72187                _ = sb.AppendLine(line);
 188            }
 189        }
 190
 191        // Trim trailing newlines
 5192        return sb.ToString().TrimEnd();
 193    }
 194
 195    /// <summary>
 196    /// Builds a PowerShell function stub for a user-defined callback function.
 197    /// </summary>
 198    /// <param name="functionName"> The name of the callback function. </param>
 199    /// <param name="definition"> The PowerShell function definition as a string. </param>
 200    /// <returns>A string containing the standardized PowerShell function stub.</returns>
 201    private static string BuildCallbackFunctionStub(string functionName, string definition)
 202    {
 3203        var (paramBlock, paramNames, bodyParamName) = TryExtractParamInfo(definition);
 204
 205        // Fall back to a no-param function if we can't parse anything.
 3206        var strippedParamBlock = StripPowerShellAttributeBlocks(paramBlock);
 3207        strippedParamBlock = NormalizeBlankLines(strippedParamBlock);
 208
 209        // Ensure we always have a param(...) block for consistent output.
 3210        if (string.IsNullOrWhiteSpace(strippedParamBlock))
 211        {
 1212            strippedParamBlock = "param()";
 1213            paramNames = [];
 214        }
 215
 3216        var sb = new StringBuilder();
 3217        _ = sb.AppendLine($"function {functionName} {{");
 218
 219        // Normalize indentation:
 220        // - "param(" line: 4 spaces
 221        // - parameter lines: 8 spaces
 222        // - closing ")": 4 spaces
 22223        foreach (var rawLine in strippedParamBlock.Replace("\r\n", "\n").Replace("\r", "\n").Split('\n'))
 224        {
 8225            var l = rawLine.Trim();
 8226            if (l.Length == 0)
 227            {
 228                continue;
 229            }
 230
 8231            if (l.Equals(")", StringComparison.Ordinal))
 232            {
 2233                _ = sb.Append("    ").AppendLine(l);
 2234                continue;
 235            }
 236
 6237            if (l.StartsWith("param", StringComparison.OrdinalIgnoreCase))
 238            {
 3239                _ = sb.Append("    ").AppendLine(l);
 3240                continue;
 241            }
 242
 3243            _ = sb.Append("        ").AppendLine(l);
 244        }
 245
 3246        _ = sb.AppendLine("    $FunctionName = $MyInvocation.MyCommand.Name");
 3247        _ = sb.AppendLine("    if ($null -eq $Context -or $null -eq $Context.Response) {");
 3248        _ = sb.AppendLine("        if (Test-KrLogger) {");
 3249        _ = sb.AppendLine("            Write-KrLog -Level Warning -Message '{function} must be called inside a route scr
 3250        _ = sb.AppendLine("        } else {");
 3251        _ = sb.AppendLine("            Write-Warning -Message \"$FunctionName must be called inside a route script with 
 3252        _ = sb.AppendLine("        }");
 3253        _ = sb.AppendLine("        return");
 3254        _ = sb.AppendLine("    }");
 3255        _ = sb.AppendLine("    Write-KrLog -Level Information -Message 'Defined callback function {CallbackFunction}' -V
 3256        _ = sb.AppendLine("    $params = [System.Collections.Generic.Dictionary[string, object]]::new()");
 257
 12258        foreach (var p in paramNames)
 259        {
 260            // Use the exact casing captured from the param block; dictionary keys are case-insensitive in C#.
 3261            _ = sb.AppendLine($"    $params['{p}'] = ${p}");
 262        }
 263
 3264        _ = sb.AppendLine(bodyParamName is { Length: > 0 }
 3265            ? $"    $bodyParameterName = '{bodyParamName}'"
 3266            : "    $bodyParameterName = $null");
 267
 3268        _ = sb.AppendLine();
 3269        _ = sb.AppendLine("    $Context.Response.AddCallbackParameters(");
 3270        _ = sb.AppendLine("        $MyInvocation.MyCommand.Name,");
 3271        _ = sb.AppendLine("        $bodyParameterName,");
 3272        _ = sb.AppendLine("        $params)");
 3273        _ = sb.AppendLine("}");
 274
 3275        return sb.ToString();
 276    }
 277
 278    private static (string ParamBlock, List<string> ParamNames, string? BodyParamName) TryExtractParamInfo(string defini
 279    {
 3280        if (string.IsNullOrWhiteSpace(definition))
 281        {
 0282            return (string.Empty, [], null);
 283        }
 284
 285        // Try to isolate the param(...) block from a FunctionInfo.Definition string.
 3286        var paramBlock = ExtractPowerShellParamBlock(definition);
 3287        if (string.IsNullOrWhiteSpace(paramBlock))
 288        {
 1289            return (string.Empty, [], null);
 290        }
 291
 292        // Identify the request body parameter name (prefer OpenApiRequestBody attribute if present)
 293        // Example: [OpenApiRequestBody(...)] [PaymentStatusChangedEvent]$Body
 2294        var bodyParamName = ExtractBodyParameterName(paramBlock);
 295
 296        // Strip attribute blocks so we keep only type constraints + $paramName
 2297        var stripped = StripPowerShellAttributeBlocks(paramBlock);
 2298        var paramNames = ExtractParamNamesFromStrippedParamBlock(stripped);
 299
 300        // If we didn't find OpenApiRequestBody, default to Body if present.
 3301        if (string.IsNullOrWhiteSpace(bodyParamName) && paramNames.Any(p => string.Equals(p, "Body", StringComparison.Or
 302        {
 0303            bodyParamName = paramNames.First(p => string.Equals(p, "Body", StringComparison.OrdinalIgnoreCase));
 304        }
 305
 2306        return (paramBlock, paramNames, bodyParamName);
 307    }
 308
 309    /// <summary>
 310    /// States for scanning PowerShell script for quoted segments.
 311    /// </summary>
 312    private enum ScanState
 313    {
 314        /// <summary>
 315        /// Normal scanning state (not inside quotes).
 316        /// </summary>
 317        Normal,
 318        /// <summary>
 319        /// Inside single-quoted string segment.
 320        /// </summary>
 321        SingleQuoted,
 322        /// <summary>
 323        /// Inside double-quoted string segment.
 324        /// </summary>
 325        DoubleQuoted
 326    }
 327
 328    /// <summary>
 329    /// Extracts the parameter block from a PowerShell function definition.
 330    /// </summary>
 331    /// <param name="definition"> The PowerShell function definition string. </param>
 332    /// <returns>The parameter block string including the 'param(...)' syntax; or an empty string if not found.</returns
 333    private static string ExtractPowerShellParamBlock(string definition)
 334    {
 3335        if (string.IsNullOrEmpty(definition))
 336        {
 0337            return string.Empty;
 338        }
 339
 3340        var idx = definition.IndexOf("param", StringComparison.OrdinalIgnoreCase);
 3341        if (idx < 0)
 342        {
 0343            return string.Empty;
 344        }
 345
 3346        var open = definition.IndexOf('(', idx);
 3347        if (open < 0)
 348        {
 1349            return string.Empty;
 350        }
 351
 2352        var depth = 0;
 2353        var state = ScanState.Normal;
 354
 534355        for (var i = open; i < definition.Length; i++)
 356        {
 267357            if (TryConsumeQuoted(definition, ref i, ref state))
 358            {
 359                continue;
 360            }
 361
 243362            var ch = definition[i];
 363
 243364            if (ch == '(')
 365            {
 5366                depth++;
 5367                continue;
 368            }
 369
 238370            if (ch == ')')
 371            {
 5372                depth--;
 5373                if (depth == 0)
 374                {
 2375                    return definition.Substring(idx, i - idx + 1);
 376                }
 377            }
 378        }
 379
 0380        return string.Empty;
 381    }
 382
 383    /// <summary>
 384    /// Tries to consume a quoted segment in the PowerShell script.
 385    /// </summary>
 386    /// <param name="s"> The input string to scan. </param>
 387    /// <param name="i"> The current index in the string, passed by reference and updated as the quoted segment is consu
 388    /// <param name="state"> The current scanning state, passed by reference and updated based on quote handling. </para
 389    /// <returns>True if a quoted segment was consumed; otherwise, false.</returns>
 390    private static bool TryConsumeQuoted(string s, ref int i, ref ScanState state)
 391    {
 267392        var ch = s[i];
 393
 394        // Enter quote states
 267395        if (state == ScanState.Normal)
 396        {
 249397            if (ch == '\'') { state = ScanState.SingleQuoted; return true; }
 243398            if (ch == '"') { state = ScanState.DoubleQuoted; return true; }
 243399            return false;
 400        }
 401
 402        // Inside single quotes: '' is an escaped single quote
 22403        if (state == ScanState.SingleQuoted)
 404        {
 22405            if (ch == '\'' && i + 1 < s.Length && s[i + 1] == '\'')
 406            {
 0407                i++; // consume second '
 0408                return true;
 409            }
 410
 22411            if (ch == '\'')
 412            {
 2413                state = ScanState.Normal;
 414            }
 415
 22416            return true;
 417        }
 418
 419        // Inside double quotes: backtick escapes the next char
 0420        if (state == ScanState.DoubleQuoted)
 421        {
 0422            if (ch == '`' && i + 1 < s.Length)
 423            {
 0424                i++; // skip escaped char
 0425                return true;
 426            }
 427
 0428            if (ch == '"')
 429            {
 0430                state = ScanState.Normal;
 431            }
 432
 0433            return true;
 434        }
 435
 0436        return false;
 437    }
 438
 439    /// <summary>
 440    /// Extracts the name of the body parameter from the parameter block, if annotated with [OpenApiRequestBody].
 441    /// </summary>
 442    /// <param name="paramBlock"> The parameter block string to search within. </param>
 443    /// <returns>The name of the body parameter if found; otherwise, null.</returns>
 444    private static string? ExtractBodyParameterName(string paramBlock)
 445    {
 446        // Very targeted heuristic: if [OpenApiRequestBody(...)] is present, pick the following $name.
 447        // This keeps the exporter decoupled from PowerShell AST dependencies.
 2448        var marker = "OpenApiRequestBody";
 2449        var idx = paramBlock.IndexOf(marker, StringComparison.OrdinalIgnoreCase);
 2450        if (idx < 0)
 451        {
 1452            return null;
 453        }
 454
 455        // Search forward for '$' then capture identifier
 180456        for (var i = idx; i < paramBlock.Length; i++)
 457        {
 90458            if (paramBlock[i] != '$')
 459            {
 460                continue;
 461            }
 462
 1463            var start = i + 1;
 1464            var end = start;
 8465            while (end < paramBlock.Length)
 466            {
 8467                var ch = paramBlock[end];
 8468                if (!(char.IsLetterOrDigit(ch) || ch == '_'))
 469                {
 470                    break;
 471                }
 7472                end++;
 473            }
 474
 1475            if (end > start)
 476            {
 1477                return paramBlock[start..end];
 478            }
 479        }
 480
 0481        return null;
 482    }
 483
 484    private static List<string> ExtractParamNamesFromStrippedParamBlock(string strippedParamBlock)
 485    {
 486        // Parse variable names only from within param(...)
 487        // We expect declarations like: [string]$paymentId,
 2488        if (string.IsNullOrWhiteSpace(strippedParamBlock))
 489        {
 0490            return [];
 491        }
 492
 2493        var names = new List<string>();
 2494        var s = strippedParamBlock;
 495
 250496        for (var i = 0; i < s.Length; i++)
 497        {
 123498            if (s[i] != '$')
 499            {
 500                continue;
 501            }
 502
 3503            var start = i + 1;
 3504            var end = start;
 3505            if (start >= s.Length)
 506            {
 507                continue;
 508            }
 509
 3510            if (!(char.IsLetter(s[start]) || s[start] == '_'))
 511            {
 512                continue;
 513            }
 514
 3515            end++;
 21516            while (end < s.Length)
 517            {
 21518                var ch = s[end];
 21519                if (!(char.IsLetterOrDigit(ch) || ch == '_'))
 520                {
 521                    break;
 522                }
 18523                end++;
 524            }
 525
 3526            var name = s[start..end];
 3527            if (!names.Contains(name, StringComparer.OrdinalIgnoreCase))
 528            {
 3529                names.Add(name);
 530            }
 531
 3532            i = end - 1;
 533        }
 534
 2535        return names;
 536    }
 537
 538    private static string StripPowerShellAttributeBlocks(string script)
 539    {
 5540        if (string.IsNullOrWhiteSpace(script))
 541        {
 1542            return string.Empty;
 543        }
 544
 4545        var sb = new StringBuilder(script.Length);
 4546        var i = 0;
 224547        while (i < script.Length)
 548        {
 220549            var ch = script[i];
 220550            if (ch != '[')
 551            {
 208552                _ = sb.Append(ch);
 208553                i++;
 208554                continue;
 555            }
 556
 557            // Capture a full bracket block, handling nested [ ... ] (e.g. generic type constraints)
 12558            var start = i;
 12559            var depth = 0;
 12560            var j = i;
 346561            while (j < script.Length)
 562            {
 346563                var cj = script[j];
 346564                if (cj == '[')
 565                {
 12566                    depth++;
 567                }
 334568                else if (cj == ']')
 569                {
 12570                    depth--;
 12571                    if (depth == 0)
 572                    {
 12573                        j++; // include closing ']'
 12574                        break;
 575                    }
 576                }
 334577                j++;
 578            }
 579
 580            // If unbalanced, just emit the rest
 12581            if (depth != 0)
 582            {
 0583                _ = sb.Append(script.AsSpan(i));
 0584                break;
 585            }
 586
 12587            var block = script.AsSpan(start, j - start);
 588
 589            // Attribute blocks always include parentheses in our usage (e.g. [OpenApiPath(...)], [Parameter()]).
 590            // Keep type constraints like [string], [int], [MyType], [MyType[]], [List[string]].
 12591            if (block.IndexOf('(') >= 0)
 592            {
 6593                i = j;
 6594                continue;
 595            }
 596
 6597            _ = sb.Append(block);
 6598            i = j;
 599        }
 600
 4601        return sb.ToString();
 602    }
 603
 604    /// <summary>
 605    /// Determines if the specified type has an OpenAPI component attribute.
 606    /// </summary>
 607    /// <param name="t">The type to inspect.</param>
 608    /// <returns>True if the type has an OpenAPI component attribute; otherwise, false.</returns>
 609    private static bool HasOpenApiComponentAttribute(Type t)
 610    {
 992611        return t.GetCustomAttributes(inherit: true)
 799612                .Select(a => a.GetType().Name)
 992613                .Any(n =>
 1791614                    n.Contains("OpenApiSchemaComponent", StringComparison.OrdinalIgnoreCase));
 615    }
 616
 617    /// <summary>
 618    /// Appends the PowerShell class definition for the specified type to the StringBuilder.
 619    /// </summary>
 620    /// <param name="type">The type to export as a PowerShell class.</param>
 621    /// <param name="componentSet">The set of known component types.</param>
 622    /// <param name="sb">The StringBuilder to append the class definition to.</param>
 623    private static void AppendClass(Type type, HashSet<Type> componentSet, StringBuilder sb)
 624    {
 13625        var bindFormAttribute = TryBuildKrBindFormAttribute(type, out var formMaxDepth);
 13626        var requiredProperties = GetRequiredProperties(type);
 13627        var additionalPropertiesMetadata = BuildAdditionalPropertiesMetadata(type);
 628
 13629        var baseClause = ResolveClassBaseClause(type, componentSet, bindFormAttribute, formMaxDepth);
 630
 13631        if (bindFormAttribute is not null)
 632        {
 1633            _ = sb.AppendLine(bindFormAttribute);
 634        }
 635
 13636        _ = sb.AppendLine("[NoRunspaceAffinity()]");
 13637        _ = sb.AppendLine($"class {type.Name}{baseClause} {{");
 638
 639        // Only properties *declared* on this type (no inherited ones)
 13640        var props = type.GetProperties(
 13641            BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);
 642
 13643        AppendAdditionalPropertiesMembers(type, props, additionalPropertiesMetadata, sb);
 644
 13645        if (requiredProperties.Length > 0)
 646        {
 0647            AppendRequiredPropertiesMetadata(requiredProperties, sb);
 648        }
 649
 106650        foreach (var p in props)
 651        {
 40652            AppendValidationAttributes(p, sb);
 40653            var psType = ToPowerShellTypeName(p.PropertyType, componentSet, collapseToUnderlyingPrimitives: true);
 40654            _ = sb.AppendLine($"    [{psType}]${p.Name}");
 655        }
 656
 13657        if (requiredProperties.Length > 0)
 658        {
 0659            AppendRequiredPropertiesValidationMethods(requiredProperties, sb);
 660        }
 661
 662        // Add static XML metadata to guide XmlHelper without requiring PowerShell method invocation
 13663        AppendOpenApiXmlMetadataProperty(type, props, sb);
 664
 13665        _ = sb.AppendLine("}");
 13666    }
 667
 668    /// <summary>
 669    /// Resolves the PowerShell class base clause for an exported OpenAPI component.
 670    /// </summary>
 671    /// <param name="type">The component type being exported.</param>
 672    /// <param name="componentSet">The set of known component types.</param>
 673    /// <param name="bindFormAttribute">The generated KrBindForm attribute text, if any.</param>
 674    /// <param name="formMaxDepth">The resolved KrBindForm.MaxNestingDepth value.</param>
 675    /// <returns>The base clause including leading colon, or an empty string when no base type is emitted.</returns>
 676    private static string ResolveClassBaseClause(Type type, HashSet<Type> componentSet, string? bindFormAttribute, int f
 677    {
 13678        if ((bindFormAttribute is null || formMaxDepth > 0)
 13679            && TryGetFormPayloadBasePsName(type, out var formBasePsName))
 680        {
 1681            return $" : {formBasePsName}";
 682        }
 683
 12684        var baseType = type.BaseType;
 12685        if (baseType is null || baseType == typeof(object))
 686        {
 8687            return string.Empty;
 688        }
 689
 4690        var basePsName = ToPowerShellTypeName(baseType, componentSet, collapseToUnderlyingPrimitives: false);
 4691        return $" : {basePsName}";
 692    }
 693
 694    /// <summary>
 695    /// Appends AdditionalProperties members and metadata for a generated class when applicable.
 696    /// </summary>
 697    /// <param name="type">The component type being exported.</param>
 698    /// <param name="props">Declared public instance properties on the type.</param>
 699    /// <param name="additionalPropertiesMetadata">Prebuilt AdditionalProperties metadata block.</param>
 700    /// <param name="sb">The output builder.</param>
 701    private static void AppendAdditionalPropertiesMembers(Type type, PropertyInfo[] props, string additionalPropertiesMe
 702    {
 13703        if (ShouldEmitAdditionalProperties(type, props))
 704        {
 1705            _ = sb.AppendLine("    [hashtable]$AdditionalProperties");
 706        }
 707
 13708        if (string.IsNullOrWhiteSpace(additionalPropertiesMetadata))
 709        {
 11710            return;
 711        }
 712
 2713        _ = sb.AppendLine();
 2714        _ = sb.AppendLine("    # Static AdditionalProperties metadata for this class");
 2715        _ = sb.AppendLine("    static [hashtable] $AdditionalPropertiesMetadata = @{");
 2716        _ = sb.Append(additionalPropertiesMetadata);
 2717        _ = sb.AppendLine("    }");
 2718    }
 719
 720    /// <summary>
 721    /// Determines whether to emit an AdditionalProperties hashtable and metadata based on the presence of an Additional
 722    /// </summary>
 723    /// <param name="type">The type to inspect.</param>
 724    /// <param name="maxDepth">The maximum nesting depth for the form.</param>
 725    /// <returns>A string representing the KrBindForm attribute, or null if not applicable.</returns>
 726    private static string? TryBuildKrBindFormAttribute(Type type, out int maxDepth)
 727    {
 13728        maxDepth = 0;
 13729        var bindAttr = type.GetCustomAttributes(inherit: false)
 38730            .FirstOrDefault(a => a.GetType().Name.Equals("KrBindFormAttribute", StringComparison.OrdinalIgnoreCase));
 731
 13732        if (bindAttr is null)
 733        {
 12734            return null;
 735        }
 736
 1737        var maxDepthProp = bindAttr.GetType().GetProperty("MaxNestingDepth");
 1738        maxDepth = maxDepthProp?.GetValue(bindAttr) as int? ?? 0;
 739
 1740        return maxDepth > 0
 1741            ? $"[KrBindForm(MaxNestingDepth = {maxDepth})]"
 1742            : "[KrBindForm()]";
 743    }
 744
 745    /// <summary>
 746    /// Gets required property names from OpenApiSchemaComponent metadata on a type.
 747    /// </summary>
 748    /// <param name="type">The type to inspect.</param>
 749    /// <returns>An array of required property names.</returns>
 750    private static string[] GetRequiredProperties(Type type)
 751    {
 13752        var schemaAttr = type.GetCustomAttributes(inherit: false)
 36753            .FirstOrDefault(a => a.GetType().Name.Equals("OpenApiSchemaComponent", StringComparison.OrdinalIgnoreCase));
 754
 13755        if (schemaAttr is null)
 756        {
 0757            return [];
 758        }
 759
 13760        var requiredValues = schemaAttr.GetType().GetProperty("RequiredProperties")?.GetValue(schemaAttr) as string[];
 13761        return requiredValues is { Length: > 0 }
 0762            ? [.. requiredValues.Where(v => !string.IsNullOrWhiteSpace(v)).Distinct(StringComparer.OrdinalIgnoreCase)]
 13763            : [];
 764    }
 765
 766    /// <summary>
 767    /// Appends static required-properties metadata to the generated class.
 768    /// </summary>
 769    /// <param name="requiredValues">Required property names to emit.</param>
 770    /// <param name="sb">The output builder.</param>
 771    private static void AppendRequiredPropertiesMetadata(string[] requiredValues, StringBuilder sb)
 772    {
 0773        if (requiredValues is not { Length: > 0 })
 774        {
 0775            return;
 776        }
 777
 0778        var requiredTuple = string.Join(", ", requiredValues.Select(v => $"'{EscapePowerShellString(v)}'"));
 0779        _ = sb.AppendLine();
 0780        _ = sb.AppendLine("    # Static required property names for this class");
 0781        _ = sb.AppendLine($"    static [string[]] $RequiredProperties = @({requiredTuple})");
 0782    }
 783
 784    /// <summary>
 785    /// Appends instance methods that validate required properties for generated PowerShell classes.
 786    /// </summary>
 787    /// <param name="requiredProperties">The required property names.</param>
 788    /// <param name="sb">The output builder.</param>
 789    private static void AppendRequiredPropertiesValidationMethods(string[] requiredProperties, StringBuilder sb)
 790    {
 0791        var requiredTuple = string.Join(", ", requiredProperties.Select(v => $"'{EscapePowerShellString(v)}'"));
 792
 0793        _ = sb.AppendLine();
 0794        _ = sb.AppendLine("    [string[]] GetMissingRequiredProperties() {");
 0795        _ = sb.AppendLine($"        $required = @({requiredTuple})");
 0796        _ = sb.AppendLine("        $missing = [System.Collections.Generic.List[string]]::new()");
 0797        _ = sb.AppendLine("        foreach ($name in $required) {");
 0798        _ = sb.AppendLine("            $value = $this.$name");
 0799        _ = sb.AppendLine("            if ($null -eq $value) {");
 0800        _ = sb.AppendLine("                $missing.Add($name)");
 0801        _ = sb.AppendLine("                continue");
 0802        _ = sb.AppendLine("            }");
 0803        _ = sb.AppendLine("            if ($value -is [string] -and [string]::IsNullOrWhiteSpace([string]$value)) {");
 0804        _ = sb.AppendLine("                $missing.Add($name)");
 0805        _ = sb.AppendLine("                continue");
 0806        _ = sb.AppendLine("            }");
 0807        _ = sb.AppendLine("            if ($value -is [System.Collections.ICollection] -and $value.Count -eq 0) {");
 0808        _ = sb.AppendLine("                $missing.Add($name)");
 0809        _ = sb.AppendLine("            }");
 0810        _ = sb.AppendLine("        }");
 0811        _ = sb.AppendLine("        return $missing.ToArray()");
 0812        _ = sb.AppendLine("    }");
 813
 0814        _ = sb.AppendLine();
 0815        _ = sb.AppendLine("    [bool] ValidateRequiredProperties() {");
 0816        _ = sb.AppendLine("        return $this.GetMissingRequiredProperties().Length -eq 0");
 0817        _ = sb.AppendLine("    }");
 0818    }
 819
 820    private static bool ShouldEmitAdditionalProperties(Type type, PropertyInfo[] props)
 821    {
 53822        if (props.Any(p => string.Equals(p.Name, "AdditionalProperties", StringComparison.OrdinalIgnoreCase)))
 823        {
 1824            return false;
 825        }
 826
 12827        var hasPatternProps = type.GetCustomAttributes(inherit: false)
 33828            .Any(a => a.GetType().Name.Equals("OpenApiPatternPropertiesAttribute", StringComparison.OrdinalIgnoreCase));
 829
 12830        if (hasPatternProps)
 831        {
 0832            return true;
 833        }
 834
 12835        var schemaAttr = type.GetCustomAttributes(inherit: false)
 32836            .FirstOrDefault(a => a.GetType().Name.Equals("OpenApiSchemaComponent", StringComparison.OrdinalIgnoreCase));
 837
 12838        if (schemaAttr is null)
 839        {
 0840            return false;
 841        }
 842
 12843        var allowProp = schemaAttr.GetType().GetProperty("AdditionalPropertiesAllowed");
 12844        return (allowProp?.GetValue(schemaAttr) as bool?) == true;
 845    }
 846
 847    private static string BuildAdditionalPropertiesMetadata(Type type)
 848    {
 13849        var schemaAttr = type.GetCustomAttributes(inherit: false)
 36850            .FirstOrDefault(a => a.GetType().Name.Equals("OpenApiSchemaComponent", StringComparison.OrdinalIgnoreCase));
 851
 13852        var additionalType = schemaAttr?.GetType().GetProperty("AdditionalProperties")?.GetValue(schemaAttr) as Type;
 853
 13854        var patternAttrs = type.GetCustomAttributes(inherit: false)
 25855            .Where(a => a.GetType().Name.Equals("OpenApiPatternPropertiesAttribute", StringComparison.OrdinalIgnoreCase)
 13856            .ToArray();
 857
 13858        if (additionalType is null && patternAttrs.Length == 0)
 859        {
 11860            return string.Empty;
 861        }
 862
 2863        var sb = new StringBuilder();
 864
 2865        if (additionalType is not null)
 866        {
 1867            _ = sb.AppendLine($"        AdditionalPropertiesType = '{additionalType.FullName ?? additionalType.Name}'");
 868        }
 869
 2870        if (patternAttrs.Length > 0)
 871        {
 1872            _ = sb.AppendLine("        PatternProperties = @(");
 873
 4874            foreach (var attr in patternAttrs)
 875            {
 1876                var keyPattern = attr.GetType().GetProperty("KeyPattern")?.GetValue(attr) as string ?? string.Empty;
 1877                var schemaType = attr.GetType().GetProperty("SchemaType")?.GetValue(attr) as Type ?? typeof(string);
 878
 1879                _ = sb.AppendLine("            @{");
 1880                _ = sb.AppendLine($"                KeyPattern = '{EscapePowerShellString(keyPattern)}'");
 1881                _ = sb.AppendLine($"                SchemaType = '{schemaType.FullName ?? schemaType.Name}'");
 1882                _ = sb.AppendLine("            }");
 883            }
 884
 1885            _ = sb.AppendLine("        )");
 886        }
 887
 2888        return sb.ToString();
 889    }
 890
 891    /// <summary>
 892    /// Appends a static hashtable property containing OpenApiXml metadata for the class and its properties.
 893    /// </summary>
 894    /// <remarks>
 895    /// This is emitted as a static property (not a PowerShell class method) so that C# reflection can read the
 896    /// metadata without requiring a PowerShell execution context bound to the current thread.
 897    /// </remarks>
 898    /// <param name="type">The type to extract OpenApiXml metadata from.</param>
 899    /// <param name="props">The properties of the type.</param>
 900    /// <param name="sb">The StringBuilder to append to.</param>
 901    private static void AppendOpenApiXmlMetadataProperty(Type type, PropertyInfo[] props, StringBuilder sb)
 902    {
 13903        _ = sb.AppendLine();
 13904        _ = sb.AppendLine("    # Static OpenApiXml metadata for this class");
 13905        _ = sb.AppendLine("    static [hashtable] $XmlMetadata = @{");
 13906        _ = sb.AppendLine("        ClassName = '" + type.Name + "'");
 907
 908        // Get class-level OpenApiXml attribute
 13909        var classXmlAttr = type.GetCustomAttributes(inherit: false)
 38910            .FirstOrDefault(a => a.GetType().Name == "OpenApiXmlAttribute");
 911
 13912        if (classXmlAttr != null)
 913        {
 0914            var classXml = BuildXmlMetadataHashtable(classXmlAttr, indent: 12);
 0915            if (!string.IsNullOrWhiteSpace(classXml))
 916            {
 0917                _ = sb.AppendLine("        ClassXml = @{");
 0918                _ = sb.AppendLine(classXml);
 0919                _ = sb.AppendLine("        }");
 920            }
 921        }
 922
 923        // Get property-level OpenApiXml attributes
 13924        if (props.Length > 0)
 925        {
 10926            _ = sb.AppendLine("        Properties = @{");
 10927            var hasAnyPropertyXml = false;
 928
 100929            foreach (var prop in props)
 930            {
 40931                var propXmlAttr = prop.GetCustomAttributes(inherit: false)
 52932                    .FirstOrDefault(a => a.GetType().Name == "OpenApiXmlAttribute");
 933
 40934                if (propXmlAttr != null)
 935                {
 0936                    var propXml = BuildXmlMetadataHashtable(propXmlAttr, indent: 16);
 0937                    if (!string.IsNullOrWhiteSpace(propXml))
 938                    {
 0939                        hasAnyPropertyXml = true;
 0940                        _ = sb.AppendLine($"            '{prop.Name}' = @{{");
 0941                        _ = sb.AppendLine(propXml);
 0942                        _ = sb.AppendLine("            }");
 943                    }
 944                }
 945            }
 946
 10947            if (!hasAnyPropertyXml)
 948            {
 10949                _ = sb.AppendLine("            # No property-level XML metadata");
 950            }
 951
 10952            _ = sb.AppendLine("        }");
 953        }
 954
 13955        _ = sb.AppendLine("    }");
 13956    }
 957
 958    /// <summary>
 959    /// Builds a PowerShell hashtable representation of OpenApiXml attribute properties.
 960    /// </summary>
 961    /// <param name="xmlAttr">The OpenApiXml attribute instance.</param>
 962    /// <param name="indent">Number of spaces to indent.</param>
 963    /// <returns>PowerShell hashtable string representation.</returns>
 964    private static string BuildXmlMetadataHashtable(object xmlAttr, int indent)
 965    {
 0966        var attrType = xmlAttr.GetType();
 0967        var sb = new StringBuilder();
 0968        var indentStr = new string(' ', indent);
 969
 970        // Extract properties using reflection
 0971        var nameProp = attrType.GetProperty("Name");
 0972        var namespaceProp = attrType.GetProperty("Namespace");
 0973        var prefixProp = attrType.GetProperty("Prefix");
 0974        var attributeProp = attrType.GetProperty("Attribute");
 0975        var wrappedProp = attrType.GetProperty("Wrapped");
 976
 0977        var name = nameProp?.GetValue(xmlAttr) as string;
 0978        var ns = namespaceProp?.GetValue(xmlAttr) as string;
 0979        var prefix = prefixProp?.GetValue(xmlAttr) as string;
 0980        var isAttribute = attributeProp?.GetValue(xmlAttr) is bool b && b;
 0981        var isWrapped = wrappedProp?.GetValue(xmlAttr) is bool w && w;
 982
 0983        if (!string.IsNullOrWhiteSpace(name))
 984        {
 0985            _ = sb.AppendLine($"{indentStr}Name = '{EscapePowerShellString(name)}'");
 986        }
 987
 0988        if (!string.IsNullOrWhiteSpace(ns))
 989        {
 0990            _ = sb.AppendLine($"{indentStr}Namespace = '{EscapePowerShellString(ns)}'");
 991        }
 992
 0993        if (!string.IsNullOrWhiteSpace(prefix))
 994        {
 0995            _ = sb.AppendLine($"{indentStr}Prefix = '{EscapePowerShellString(prefix)}'");
 996        }
 997
 0998        if (isAttribute)
 999        {
 01000            _ = sb.AppendLine($"{indentStr}Attribute = $true");
 1001        }
 1002
 01003        if (isWrapped)
 1004        {
 01005            _ = sb.AppendLine($"{indentStr}Wrapped = $true");
 1006        }
 1007
 01008        return sb.ToString().TrimEnd();
 1009    }
 1010
 1011    /// <summary>
 1012    /// Escapes single quotes in a string for PowerShell string literals.
 1013    /// </summary>
 1014    /// <param name="str">The string to escape.</param>
 1015    /// <returns>Escaped string safe for PowerShell single-quoted strings.</returns>
 11016    private static string EscapePowerShellString(string str) => str.Replace("'", "''");
 1017
 1018    private static void AppendValidationAttributes(PropertyInfo property, StringBuilder sb)
 1019    {
 401020        var attributes = property.GetCustomAttributes(inherit: false)
 401021            .Select(TryFormatValidationAttribute)
 121022            .Where(s => !string.IsNullOrWhiteSpace(s))
 401023            .ToList();
 1024
 801025        foreach (var attribute in attributes)
 1026        {
 01027            _ = sb.Append("    ").AppendLine(attribute);
 1028        }
 401029    }
 1030
 1031    private static string? TryFormatValidationAttribute(object attribute)
 1032    {
 121033        var typeName = attribute.GetType().Name;
 1034
 121035        return typeName switch
 121036        {
 01037            "ValidateRangeAttribute" => FormatValidateRange(attribute),
 01038            "ValidateLengthAttribute" => FormatValidateLength(attribute),
 01039            "ValidateSetAttribute" => FormatValidateSet(attribute),
 01040            "ValidatePatternAttribute" => FormatValidatePattern(attribute),
 01041            "ValidateCountAttribute" => FormatValidateCount(attribute),
 01042            "ValidateNotNullOrEmptyAttribute" => "[ValidateNotNullOrEmpty()]",
 01043            "ValidateNotNullAttribute" => "[ValidateNotNull()]",
 01044            "ValidateNotNullOrWhiteSpaceAttribute" => "[ValidateNotNullOrWhiteSpace()]",
 121045            _ => null
 121046        };
 1047    }
 1048
 1049    private static string? FormatValidateRange(object attribute)
 1050    {
 01051        var min = attribute.GetType().GetProperty("MinRange")?.GetValue(attribute);
 01052        var max = attribute.GetType().GetProperty("MaxRange")?.GetValue(attribute);
 01053        return min is null || max is null ? null : $"[ValidateRange({FormatPowerShellLiteral(min)}, {FormatPowerShellLit
 1054    }
 1055
 1056    private static string? FormatValidateLength(object attribute)
 1057    {
 01058        var min = attribute.GetType().GetProperty("MinLength")?.GetValue(attribute);
 01059        var max = attribute.GetType().GetProperty("MaxLength")?.GetValue(attribute);
 01060        return min is null || max is null ? null : $"[ValidateLength({FormatPowerShellLiteral(min)}, {FormatPowerShellLi
 1061    }
 1062
 1063    private static string? FormatValidateCount(object attribute)
 1064    {
 01065        var min = attribute.GetType().GetProperty("MinLength")?.GetValue(attribute);
 01066        var max = attribute.GetType().GetProperty("MaxLength")?.GetValue(attribute);
 01067        return min is null || max is null ? null : $"[ValidateCount({FormatPowerShellLiteral(min)}, {FormatPowerShellLit
 1068    }
 1069
 1070    private static string? FormatValidatePattern(object attribute)
 1071    {
 01072        var pattern = attribute.GetType().GetProperty("RegexPattern")?.GetValue(attribute) as string;
 01073        return string.IsNullOrWhiteSpace(pattern) ? null : $"[ValidatePattern('{EscapePowerShellString(pattern)}')]";
 1074    }
 1075
 1076    private static string? FormatValidateSet(object attribute)
 1077    {
 01078        if (attribute.GetType().GetProperty("ValidValues")?.GetValue(attribute) is not IEnumerable<object> values)
 1079        {
 01080            return null;
 1081        }
 1082
 01083        var formatted = values
 01084            .Select(FormatPowerShellLiteral)
 01085            .Where(v => !string.IsNullOrWhiteSpace(v))
 01086            .ToArray();
 1087
 01088        return formatted.Length == 0 ? null : $"[ValidateSet({string.Join(", ", formatted)})]";
 1089    }
 1090
 1091    private static string? FormatPowerShellLiteral(object? value)
 1092    {
 01093        return value is null
 01094            ? "$null"
 01095            : value switch
 01096            {
 01097                string s => $"'{EscapePowerShellString(s)}'",
 01098                char c => $"'{EscapePowerShellString(c.ToString())}'",
 01099                bool b => b ? "$true" : "$false",
 01100                byte or sbyte or short or ushort or int or uint or long or ulong or float or double or decimal =>
 01101                    Convert.ToString(value, System.Globalization.CultureInfo.InvariantCulture),
 01102                _ => $"'{EscapePowerShellString(Convert.ToString(value, System.Globalization.CultureInfo.InvariantCultur
 01103            };
 1104    }
 1105
 1106    /// <summary>
 1107    /// Converts a .NET type to a PowerShell type name.
 1108    /// </summary>
 1109    /// <param name="t"></param>
 1110    /// <param name="componentSet"></param>
 1111    /// <param name="collapseToUnderlyingPrimitives">When true, types derived from OpenApiValue&lt;T&gt; are emitted as 
 1112    /// <returns></returns>
 1113    private static string ToPowerShellTypeName(Type t, HashSet<Type> componentSet, bool collapseToUnderlyingPrimitives)
 1114    {
 561115        return GetNullableTypeName(t, componentSet, collapseToUnderlyingPrimitives)
 561116            ?? GetOpenApiArrayWrapperTypeName(t, componentSet, collapseToUnderlyingPrimitives)
 561117            ?? GetCollapsedOpenApiPrimitiveTypeName(t, componentSet, collapseToUnderlyingPrimitives)
 561118            ?? GetEnumTypeName(t)
 561119            ?? GetPrimitiveTypeName(t)
 561120            ?? GetArrayTypeName(t, componentSet, collapseToUnderlyingPrimitives)
 561121            ?? FormatComponentOrFallbackName(t, componentSet);
 1122    }
 1123
 1124    /// <summary>
 1125    /// Produces a PowerShell nullable type constraint (e.g. <c>Nullable[int]</c>) when the input is a <c>Nullable&lt;T&
 1126    /// </summary>
 1127    /// <param name="t">The CLR type to inspect.</param>
 1128    /// <param name="componentSet">The set of known OpenAPI component types.</param>
 1129    /// <param name="collapseToUnderlyingPrimitives">Whether OpenAPI primitive wrapper types should be collapsed to prim
 1130    /// <returns>The nullable type name, or <c>null</c> when <paramref name="t"/> is not nullable.</returns>
 1131    private static string? GetNullableTypeName(Type t, HashSet<Type> componentSet, bool collapseToUnderlyingPrimitives)
 1132    {
 561133        return Nullable.GetUnderlyingType(t) is Type underlying
 561134            ? $"Nullable[{ToPowerShellTypeName(underlying, componentSet, collapseToUnderlyingPrimitives)}]"
 561135            : null;
 1136    }
 1137
 1138    /// <summary>
 1139    /// Produces an element-array type constraint for OpenAPI schema component array wrapper types when appropriate.
 1140    /// </summary>
 1141    /// <param name="t">The CLR type to inspect.</param>
 1142    /// <param name="componentSet">The set of known OpenAPI component types.</param>
 1143    /// <param name="collapseToUnderlyingPrimitives">Whether OpenAPI primitive wrapper types should be collapsed to prim
 1144    /// <returns>The array wrapper type name, or <c>null</c> when <paramref name="t"/> is not an OpenAPI array wrapper t
 1145    private static string? GetOpenApiArrayWrapperTypeName(Type t, HashSet<Type> componentSet, bool collapseToUnderlyingP
 541146        => ResolveElementArrayType(t, componentSet, collapseToUnderlyingPrimitives);
 1147
 1148    /// <summary>
 1149    /// Produces the underlying primitive PowerShell type name for OpenAPI primitive wrapper types (e.g. OpenApiString/O
 1150    /// </summary>
 1151    /// <param name="t">The CLR type to inspect.</param>
 1152    /// <param name="componentSet">The set of known OpenAPI component types.</param>
 1153    /// <param name="collapseToUnderlyingPrimitives">Whether collapsing is enabled.</param>
 1154    /// <returns>The primitive name, or <c>null</c> when <paramref name="t"/> is not an OpenAPI wrapper type (or collaps
 1155    /// <remarks>
 1156    /// When <paramref name="collapseToUnderlyingPrimitives"/> is <c>true</c>,
 1157    /// types derived from OpenApiValue&lt;T&gt; are emitted as their underlying primitive (e.g., string/double/bool/lon
 1158    /// </remarks>
 1159    private static string? GetCollapsedOpenApiPrimitiveTypeName(Type t, HashSet<Type> componentSet, bool collapseToUnder
 531160        => collapseToUnderlyingPrimitives
 531161           && TryGetOpenApiValueUnderlyingType(t, out var underlying)
 531162           && underlying is not null
 531163            ? ToPowerShellTypeName(underlying, componentSet, collapseToUnderlyingPrimitives)
 531164            : null;
 1165
 1166    /// <summary>
 1167    /// Produces the simple name for enum types so PowerShell can bind against the emitted enum definition.
 1168    /// </summary>
 1169    /// <param name="t">The CLR type to inspect.</param>
 1170    /// <returns>The enum name, or <c>null</c> when <paramref name="t"/> is not an enum.</returns>
 1171    private static string? GetEnumTypeName(Type t)
 471172        => t.IsEnum ? t.Name : null;
 1173
 1174    /// <summary>
 1175    /// Produces the PowerShell type name for well-known CLR primitives.
 1176    /// </summary>
 1177    /// <param name="t">The CLR type to inspect.</param>
 1178    /// <returns>The primitive name, or <c>null</c> when no primitive mapping exists.</returns>
 1179    private static string? GetPrimitiveTypeName(Type t)
 461180        => ResolvePrimitiveTypeName(t);
 1181
 1182    /// <summary>
 1183    /// Produces a PowerShell element-array type constraint (e.g. <c>string[]</c>) for CLR array types.
 1184    /// </summary>
 1185    /// <param name="t">The CLR type to inspect.</param>
 1186    /// <param name="componentSet">The set of known OpenAPI component types.</param>
 1187    /// <param name="collapseToUnderlyingPrimitives">Whether OpenAPI primitive wrapper types should be collapsed to prim
 1188    /// <returns>The formatted array name, or <c>null</c> when <paramref name="t"/> is not an array.</returns>
 1189    private static string? GetArrayTypeName(Type t, HashSet<Type> componentSet, bool collapseToUnderlyingPrimitives)
 111190        => t.IsArray && t.GetElementType() is Type elementType
 111191            ? $"{ToPowerShellTypeName(elementType, componentSet, collapseToUnderlyingPrimitives)}[]"
 111192            : null;
 1193
 1194    /// <summary>
 1195    /// Formats a component type as its simple name or falls back to full name for other reference types.
 1196    /// </summary>
 1197    /// <param name="t">The CLR type to format.</param>
 1198    /// <param name="componentSet">The set of known OpenAPI component types.</param>
 1199    /// <returns>A PowerShell-friendly type name.</returns>
 1200    private static string FormatComponentOrFallbackName(Type t, HashSet<Type> componentSet)
 81201        => componentSet.Contains(t) || t.FullName is null
 81202            ? t.Name
 81203            : t.FullName;
 1204
 1205    /// <summary>
 1206    /// Collects enums referenced by component properties so they can be emitted before class definitions.
 1207    /// </summary>
 1208    /// <param name="componentTypes">Component classes to scan.</param>
 1209    /// <returns>A de-duplicated list of enums to export.</returns>
 1210    private static IEnumerable<Type> CollectExportableEnums(IEnumerable<Type> componentTypes)
 1211    {
 671212        var enums = new HashSet<Type>();
 1213
 1601214        foreach (var componentType in componentTypes)
 1215        {
 1061216            foreach (var p in componentType.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Dec
 1217            {
 821218                foreach (var enumType in FindEnumsInType(p.PropertyType))
 1219                {
 11220                    _ = enums.Add(enumType);
 1221                }
 1222            }
 1223        }
 1224
 671225        return enums;
 1226    }
 1227
 1228    /// <summary>
 1229    /// Finds any enum types within a possibly wrapped type (nullable/array/generic).
 1230    /// </summary>
 1231    /// <param name="t">Type to inspect.</param>
 1232    /// <returns>Zero or more enum types found.</returns>
 1233    private static IEnumerable<Type> FindEnumsInType(Type t)
 1234    {
 1235        // Nullable<T>
 461236        if (Nullable.GetUnderlyingType(t) is Type underlying)
 1237        {
 41238            foreach (var e in FindEnumsInType(underlying))
 1239            {
 01240                yield return e;
 1241            }
 21242            yield break;
 1243        }
 1244
 1245        // Arrays
 441246        if (t.IsArray)
 1247        {
 81248            foreach (var e in FindEnumsInType(t.GetElementType()!))
 1249            {
 01250                yield return e;
 1251            }
 41252            yield break;
 1253        }
 1254
 1255        // Generic arguments
 401256        if (t.IsGenericType)
 1257        {
 01258            foreach (var arg in t.GetGenericArguments())
 1259            {
 01260                foreach (var e in FindEnumsInType(arg))
 1261                {
 01262                    yield return e;
 1263                }
 1264            }
 1265        }
 1266
 401267        if (t.IsEnum)
 1268        {
 11269            yield return t;
 1270        }
 401271    }
 1272
 1273    /// <summary>
 1274    /// Appends a PowerShell enum definition for the specified .NET enum type.
 1275    /// </summary>
 1276    /// <param name="enumType">Enum type to emit.</param>
 1277    /// <param name="sb">Output StringBuilder.</param>
 1278    private static void AppendEnum(Type enumType, StringBuilder sb)
 1279    {
 11280        if (!enumType.IsEnum)
 1281        {
 01282            return;
 1283        }
 1284
 11285        var underlying = Enum.GetUnderlyingType(enumType);
 11286        var psUnderlying = ResolvePrimitiveTypeName(underlying) ?? underlying.FullName ?? "int";
 1287
 11288        _ = sb.AppendLine($"enum {enumType.Name} {{");
 1289
 81290        foreach (var name in Enum.GetNames(enumType))
 1291        {
 31292            var rawValue = Enum.Parse(enumType, name);
 31293            var numericValue = Convert.ChangeType(rawValue, underlying, provider: System.Globalization.CultureInfo.Invar
 1294
 1295            // Always emit explicit values to preserve non-sequential enums.
 31296            _ = sb.AppendLine($"    {name} = [{psUnderlying}]{numericValue}");
 1297        }
 1298
 11299        _ = sb.AppendLine("}");
 11300    }
 1301
 1302    /// <summary>
 1303    /// Resolves the PowerShell type name for OpenAPI array wrapper components.
 1304    /// </summary>
 1305    /// <param name="t">The .NET type to resolve.</param>
 1306    /// <param name="componentSet">The set of known OpenAPI component types.</param>
 1307    /// <param name="collapseToUnderlyingPrimitives">When true, types derived from OpenApiValue&lt;T&gt; are emitted as 
 1308    /// <returns>The PowerShell type name for the array element if applicable; otherwise, null.</returns>
 1309    private static string? ResolveElementArrayType(Type t, HashSet<Type> componentSet, bool collapseToUnderlyingPrimitiv
 1310    {
 1311        // OpenAPI schema component array wrappers:
 1312        // Some PowerShell OpenAPI schemas are modeled as a component class with Array=$true,
 1313        // typically inheriting from the element schema type (e.g. EventDates : Date).
 1314        // When referenced as a property type, we want the PowerShell type constraint to be
 1315        // the element array (e.g. [Date[]]) instead of the wrapper class ([EventDates]).
 1316        // IMPORTANT: this must run before OpenApiValue<T> collapsing so wrappers don't lose their array-ness.
 541317        if (collapseToUnderlyingPrimitives && componentSet.Contains(t) && TryGetArrayComponentElementType(t, out var ele
 1318        {
 1319            // Guard against pathological self-references.
 11320            if (elementType == t)
 1321            {
 01322                return t.Name;
 1323            }
 1324
 11325            var elementPsName = ToPowerShellTypeName(elementType, componentSet, collapseToUnderlyingPrimitives);
 11326            return $"{elementPsName}[]";
 1327        }
 531328        return null;
 1329    }
 1330
 1331    // Mapping of .NET primitive types to PowerShell type names.
 11332    private static readonly Dictionary<Type, string> PrimitiveTypeAliases =
 11333         new()
 11334         {
 11335             [typeof(bool)] = "bool",
 11336             [typeof(byte)] = "byte",
 11337             [typeof(sbyte)] = "sbyte",
 11338             [typeof(short)] = "short",
 11339             [typeof(ushort)] = "ushort",
 11340             [typeof(int)] = "int",
 11341             [typeof(uint)] = "uint",
 11342             [typeof(long)] = "long",
 11343             [typeof(ulong)] = "ulong",
 11344             [typeof(float)] = "float",
 11345             [typeof(double)] = "double",
 11346             [typeof(decimal)] = "decimal",
 11347             [typeof(char)] = "char",
 11348             [typeof(string)] = "string",
 11349             [typeof(object)] = "object",
 11350             [typeof(DateTime)] = "datetime",
 11351             [typeof(Guid)] = "guid",
 11352             [typeof(byte[])] = "byte[]"
 11353         };
 1354
 1355    /// <summary>
 1356    /// Resolves the PowerShell type name for common .NET primitive types.
 1357    /// </summary>
 1358    /// <param name="t">The .NET type to resolve.</param>
 1359    /// <returns>The PowerShell type name if the type is a recognized primitive; otherwise, null.</returns>
 1360    private static string? ResolvePrimitiveTypeName(Type t)
 1361    {
 1362        // unwrap nullable if needed
 471363        t = Nullable.GetUnderlyingType(t) ?? t;
 1364
 471365        return PrimitiveTypeAliases.TryGetValue(t, out var alias) ? alias : null;
 1366    }
 1367
 1368    private static bool TryGetOpenApiValueUnderlyingType(Type t, out Type? underlyingType)
 1369    {
 491370        underlyingType = null;
 1371
 1372        // Walk base types looking for OpenApiScalar<T> (preferred) or OpenApiValue<T> (legacy)
 1373        // by name to avoid hard coupling.
 1374        // OpenApiScalar<T> lives in Kestrun.Annotations and is in the global namespace.
 491375        var current = t;
 1376
 1271377        while (current is not null && current != typeof(object))
 1378        {
 841379            if (current.IsGenericType)
 1380            {
 61381                var def = current.GetGenericTypeDefinition();
 61382                if (string.Equals(def.Name, "OpenApiScalar`1", StringComparison.Ordinal) ||
 61383                    string.Equals(def.Name, "OpenApiValue`1", StringComparison.Ordinal))
 1384                {
 61385                    underlyingType = current.GetGenericArguments()[0];
 61386                    return true;
 1387                }
 1388            }
 1389
 781390            current = current.BaseType;
 1391        }
 1392
 431393        return false;
 1394    }
 1395
 1396    private static bool TryGetArrayComponentElementType(Type componentType, out Type? elementType)
 1397    {
 61398        elementType = null;
 1399
 1400        // We don't take a hard dependency on the annotation type here; this exporter
 1401        // may reflect PowerShell-generated assemblies. We detect the attribute by name
 1402        // and then read common properties via reflection.
 61403        var attr = componentType
 61404            .GetCustomAttributes(inherit: false)
 151405            .FirstOrDefault(a => a.GetType().Name.Contains("OpenApiSchemaComponent", StringComparison.OrdinalIgnoreCase)
 1406
 61407        if (attr is null)
 1408        {
 01409            return false;
 1410        }
 1411
 61412        var attrType = attr.GetType();
 61413        var arrayProp = attrType.GetProperty("Array");
 61414        if (arrayProp?.GetValue(attr) is not bool isArray || !isArray)
 1415        {
 51416            return false;
 1417        }
 1418
 1419        // Prefer explicit ItemsType if provided.
 11420        var itemsTypeProp = attrType.GetProperty("ItemsType");
 11421        if (itemsTypeProp?.GetValue(attr) is Type itemsType)
 1422        {
 01423            elementType = itemsType;
 01424            return true;
 1425        }
 1426
 1427        // Common PowerShell pattern: wrapper inherits from element schema.
 11428        var baseType = componentType.BaseType;
 11429        if (baseType is not null && baseType != typeof(object))
 1430        {
 11431            elementType = baseType;
 11432            return true;
 1433        }
 1434
 01435        return false;
 1436    }
 1437
 1438    /// <summary>
 1439    /// Topologically sort types so that dependencies (property types)
 1440    /// appear before the types that reference them.
 1441    /// </summary>
 1442    /// <param name="types">The list of types to sort.</param>
 1443    /// <param name="componentSet">Set of component types for quick lookup.</param>
 1444    /// <returns>The sorted list of types.</returns>
 1445    private static List<Type> TopologicalSortByPropertyDependencies(
 1446        List<Type> types,
 1447        HashSet<Type> componentSet)
 1448    {
 671449        var result = new List<Type>();
 671450        var visited = new Dictionary<Type, bool>(); // false = temp-mark, true = perm-mark
 1451
 1601452        foreach (var t in types)
 1453        {
 131454            Visit(t, componentSet, visited, result);
 1455        }
 1456
 671457        return result;
 1458    }
 1459
 1460    /// <summary>
 1461    /// Visits the type and its dependencies recursively for topological sorting.
 1462    /// </summary>
 1463    /// <param name="t">Type to visit</param>
 1464    /// <param name="componentSet">Set of component types</param>
 1465    /// <param name="visited">Dictionary tracking visited types and their mark status</param>
 1466    /// <param name="result">List to accumulate the sorted types</param>
 1467    private static void Visit(
 1468     Type t,
 1469     HashSet<Type> componentSet,
 1470     Dictionary<Type, bool> visited,
 1471     List<Type> result)
 1472    {
 201473        if (visited.TryGetValue(t, out var perm))
 1474        {
 71475            if (!perm)
 1476            {
 1477                // cycle; ignore for now
 71478                return;
 1479            }
 1480            return;
 1481        }
 1482
 1483        // temp-mark
 131484        visited[t] = false;
 1485
 131486        var deps = new List<Type>();
 1487
 1488        // 1) Dependencies via property types (component properties)
 131489        var propDeps = t.GetProperties(BindingFlags.Public | BindingFlags.Instance)
 471490                        .Select(p => GetComponentDependencyType(p.PropertyType, componentSet))
 471491                        .Where(dep => dep is not null)
 51492                        .Select(dep => dep!)
 131493                        .Distinct();
 1494
 131495        deps.AddRange(propDeps);
 1496
 1497        // 2) Dependency via base type (parenting)
 131498        var baseType = t.BaseType;
 131499        if (baseType != null && componentSet.Contains(baseType))
 1500        {
 21501            deps.Add(baseType);
 1502        }
 1503
 401504        foreach (var dep in deps.Distinct())
 1505        {
 71506            Visit(dep, componentSet, visited, result);
 1507        }
 1508
 1509        // perm-mark
 131510        visited[t] = true;
 131511        result.Add(t);
 131512    }
 1513
 1514    private static Type? GetComponentDependencyType(Type propertyType, HashSet<Type> componentSet)
 1515    {
 1516        // Unwrap Nullable
 471517        if (Nullable.GetUnderlyingType(propertyType) is Type underlying)
 1518        {
 21519            propertyType = underlying;
 1520        }
 1521
 1522        // Unwrap arrays
 471523        if (propertyType.IsArray)
 1524        {
 41525            propertyType = propertyType.GetElementType()!;
 1526        }
 1527
 471528        return componentSet.Contains(propertyType) ? propertyType : null;
 1529    }
 1530
 1531    /// <summary>
 1532    /// Determines the PowerShell base type for form payload exports based on KrBindForm.MaxNestingDepth.
 1533    /// </summary>
 1534    /// <param name="type">The OpenAPI component type.</param>
 1535    /// <param name="basePsName">The resolved PowerShell base type name.</param>
 1536    /// <returns>True if a form payload base should be applied; otherwise false.</returns>
 1537    private static bool TryGetFormPayloadBasePsName(Type type, out string? basePsName)
 1538    {
 131539        basePsName = null;
 1540
 131541        var bindAttr = type.GetCustomAttributes(inherit: false)
 381542            .FirstOrDefault(a => a.GetType().Name.Equals("KrBindFormAttribute", StringComparison.OrdinalIgnoreCase));
 1543
 131544        if (bindAttr is null)
 1545        {
 121546            return false;
 1547        }
 1548
 11549        var maxDepthProp = bindAttr.GetType().GetProperty("MaxNestingDepth");
 11550        var maxDepth = maxDepthProp?.GetValue(bindAttr) as int?;
 1551
 1552        // If MaxNestingDepth > 0, treat as multipart; otherwise form data.
 11553        basePsName = (maxDepth.GetValueOrDefault(0) > 0)
 11554            ? "Kestrun.Forms.KrMultipart"
 11555            : "Kestrun.Forms.KrFormData";
 11556        return true;
 1557    }
 1558
 1559    /// <summary>
 1560    /// Writes the OpenAPI class definitions to a temporary PowerShell script file.
 1561    /// </summary>
 1562    /// <param name="openApiClasses">The OpenAPI class definitions as a string.</param>
 1563    /// <returns>The path to the temporary PowerShell script file.</returns>
 1564    public static string WriteOpenApiTempScript(string openApiClasses)
 1565    {
 1566        // Use a stable file name so multiple runspaces share the same script
 41567        var tempPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".ps1");
 1568
 1569        // Ensure directory exists
 41570        _ = Directory.CreateDirectory(Path.GetDirectoryName(tempPath)!);
 1571
 1572        // Build content with header
 41573        var sb = new StringBuilder()
 41574        .AppendLine("# ================================================")
 41575        .AppendLine("#   Kestrun OpenAPI Autogenerated Class Definitions")
 41576        .AppendLine("#   DO NOT EDIT - generated at runtime")
 41577        .Append("#   Timestamp: ").Append(DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss")).Append('Z').AppendLine()
 41578        .AppendLine("# ================================================")
 41579        .AppendLine()
 41580        .AppendLine("[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSProvideCommentHelp', '')]")
 41581        .AppendLine("param()")
 41582        .AppendLine(openApiClasses);
 1583
 1584        // Save using UTF-8 without BOM
 41585        File.WriteAllText(tempPath, sb.ToString(), new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
 1586
 41587        return tempPath;
 1588    }
 1589}

Methods/Properties

get_ValidClassNames()
ExportOpenApiClasses(System.Collections.Generic.Dictionary`2<System.String,System.String>)
ExportOpenApiClasses(System.Reflection.Assembly[],System.Collections.Generic.Dictionary`2<System.String,System.String>)
GetLoadableTypes(System.Reflection.Assembly)
AppendCallback(System.Text.StringBuilder,System.Collections.Generic.Dictionary`2<System.String,System.String>)
NormalizeBlankLines(System.String)
BuildCallbackFunctionStub(System.String,System.String)
TryExtractParamInfo(System.String)
ExtractPowerShellParamBlock(System.String)
TryConsumeQuoted(System.String,System.Int32&,Kestrun.Runtime.PowerShellOpenApiClassExporter/ScanState&)
ExtractBodyParameterName(System.String)
ExtractParamNamesFromStrippedParamBlock(System.String)
StripPowerShellAttributeBlocks(System.String)
HasOpenApiComponentAttribute(System.Type)
AppendClass(System.Type,System.Collections.Generic.HashSet`1<System.Type>,System.Text.StringBuilder)
ResolveClassBaseClause(System.Type,System.Collections.Generic.HashSet`1<System.Type>,System.String,System.Int32)
AppendAdditionalPropertiesMembers(System.Type,System.Reflection.PropertyInfo[],System.String,System.Text.StringBuilder)
TryBuildKrBindFormAttribute(System.Type,System.Int32&)
GetRequiredProperties(System.Type)
AppendRequiredPropertiesMetadata(System.String[],System.Text.StringBuilder)
AppendRequiredPropertiesValidationMethods(System.String[],System.Text.StringBuilder)
ShouldEmitAdditionalProperties(System.Type,System.Reflection.PropertyInfo[])
BuildAdditionalPropertiesMetadata(System.Type)
AppendOpenApiXmlMetadataProperty(System.Type,System.Reflection.PropertyInfo[],System.Text.StringBuilder)
BuildXmlMetadataHashtable(System.Object,System.Int32)
EscapePowerShellString(System.String)
AppendValidationAttributes(System.Reflection.PropertyInfo,System.Text.StringBuilder)
TryFormatValidationAttribute(System.Object)
FormatValidateRange(System.Object)
FormatValidateLength(System.Object)
FormatValidateCount(System.Object)
FormatValidatePattern(System.Object)
FormatValidateSet(System.Object)
FormatPowerShellLiteral(System.Object)
ToPowerShellTypeName(System.Type,System.Collections.Generic.HashSet`1<System.Type>,System.Boolean)
GetNullableTypeName(System.Type,System.Collections.Generic.HashSet`1<System.Type>,System.Boolean)
GetOpenApiArrayWrapperTypeName(System.Type,System.Collections.Generic.HashSet`1<System.Type>,System.Boolean)
GetCollapsedOpenApiPrimitiveTypeName(System.Type,System.Collections.Generic.HashSet`1<System.Type>,System.Boolean)
GetEnumTypeName(System.Type)
GetPrimitiveTypeName(System.Type)
GetArrayTypeName(System.Type,System.Collections.Generic.HashSet`1<System.Type>,System.Boolean)
FormatComponentOrFallbackName(System.Type,System.Collections.Generic.HashSet`1<System.Type>)
CollectExportableEnums(System.Collections.Generic.IEnumerable`1<System.Type>)
FindEnumsInType()
AppendEnum(System.Type,System.Text.StringBuilder)
ResolveElementArrayType(System.Type,System.Collections.Generic.HashSet`1<System.Type>,System.Boolean)
.cctor()
ResolvePrimitiveTypeName(System.Type)
TryGetOpenApiValueUnderlyingType(System.Type,System.Type&)
TryGetArrayComponentElementType(System.Type,System.Type&)
TopologicalSortByPropertyDependencies(System.Collections.Generic.List`1<System.Type>,System.Collections.Generic.HashSet`1<System.Type>)
Visit(System.Type,System.Collections.Generic.HashSet`1<System.Type>,System.Collections.Generic.Dictionary`2<System.Type,System.Boolean>,System.Collections.Generic.List`1<System.Type>)
GetComponentDependencyType(System.Type,System.Collections.Generic.HashSet`1<System.Type>)
TryGetFormPayloadBasePsName(System.Type,System.String&)
WriteOpenApiTempScript(System.String)