Private/SwaggerSchema.ps1
|
# Schema and operation helpers for New-FunctionsFromSwagger. They read the generator state in # $script:SwaggerGeneratorState (set by New-FunctionsFromSwagger for the duration of one run). function Resolve-ComponentSchema { param([object]$Schema) if (-not $Schema) { return $null } # Only cache $ref-based schema resolutions to avoid hash code collisions # Non-ref schemas are passed through directly without caching if (-not ($Schema.PSObject.Properties.Name -contains '$ref')) { return $Schema } $cacheKey = $Schema.'$ref' # Check cache first if ($script:SwaggerGeneratorState.SchemaCache.ContainsKey($cacheKey)) { return $script:SwaggerGeneratorState.SchemaCache[$cacheKey] } # Resolve the schema $refName = $Schema.'$ref' -replace '^#/components/schemas/', '' $resolved = if ($script:SwaggerGeneratorState.SchemaTable.ContainsKey($refName)) { Write-Verbose " Resolving $refName from schemaTable" $fromTable = $script:SwaggerGeneratorState.SchemaTable[$refName] Write-Verbose " From table - type: $($fromTable.type), has properties: $($null -ne $fromTable.properties)" $fromTable } else { Write-Verbose " WARNING: $refName not found in schemaTable!" $Schema } Write-Verbose " Resolved result - type: $($resolved.type), has properties: $($null -ne $resolved.properties), PSObject properties: $($resolved.PSObject.Properties.Name -join ', ')" # Cache the result $script:SwaggerGeneratorState.SchemaCache[$cacheKey] = $resolved return $resolved } function Get-PSTypeFromSchema { param([object]$Schema) if (-not $Schema) { return '[object]' } $resolved = Resolve-ComponentSchema $Schema if ($resolved -ne $Schema) { return Get-PSTypeFromSchema $resolved } if ($resolved.type) { switch ($resolved.type) { 'integer' { if ($resolved.format -eq 'int64') { return '[long]' } else { return '[int]' } } 'number' { return '[double]' } 'boolean' { return '[bool]' } 'string' { return '[string]' } 'array' { if ($resolved.items) { $innerType = Get-PSTypeFromSchema $resolved.items switch ($innerType) { '[string]' { return '[string[]]' } '[int]' { return '[int[]]' } '[long]' { return '[long[]]' } '[double]' { return '[double[]]' } '[bool]' { return '[bool[]]' } '[hashtable]' { return '[hashtable[]]' } default { return '[object[]]' } } } return '[object[]]' } 'object' { return '[hashtable]' } } } return '[object]' } function Get-NormalizedSchema { param([object]$Schema) if (-not $Schema) { return $null } $resolved = Resolve-ComponentSchema $Schema $nullable = $false if ($resolved.PSObject.Properties.Name -contains 'nullable' -and $resolved.nullable) { $nullable = [bool]$resolved.nullable } # Repair swagger inconsistencies: if a property has default:null but lacks nullable:true, treat it as nullable. # This aligns conversion schema nullability inference with body builder's AllowsNull logic (lines ~1800+) and # prevents converter throws when default:null is present. OpenAPI 3 spec technically requires nullable:true for null defaults; # so this heuristic only activates when swagger metadata is inconsistent. if (-not $nullable -and $resolved.PSObject.Properties.Name -contains 'default' -and $null -eq $resolved.default) { $nullable = $true } if ($resolved.PSObject.Properties.Name -contains 'allOf' -and $resolved.allOf) { $merged = [ordered]@{ type = 'object'; nullable = $nullable; properties = [ordered]@{}; required = @() } foreach ($component in $resolved.allOf) { $child = Get-NormalizedSchema $component if (-not $child) { continue } if ($child.type -eq 'object') { if ($child.PSObject.Properties.Name -contains 'properties' -and $child.properties) { foreach ($prop in $child.properties.GetEnumerator()) { $merged.properties[$prop.Key] = $prop.Value } } if ($child.PSObject.Properties.Name -contains 'required' -and $child.required) { $merged.required += $child.required } if ($child.PSObject.Properties.Name -contains 'additionalProperties') { $merged.additionalProperties = $child.additionalProperties } } elseif ($child.type -eq 'array' -and (-not $merged.Contains('items'))) { $merged.items = $child.items } } if ($resolved.PSObject.Properties.Name -contains 'properties' -and $resolved.properties) { foreach ($prop in $resolved.properties.PSObject.Properties) { $merged.properties[$prop.Name] = Get-NormalizedSchema $prop.Value } } if ($resolved.PSObject.Properties.Name -contains 'required' -and $resolved.required) { $merged.required += @($resolved.required) } if ($resolved.PSObject.Properties.Name -contains 'additionalProperties') { $merged.additionalProperties = $resolved.additionalProperties } return $merged } $typeValue = $resolved.type if (-not $typeValue) { if ($resolved.PSObject.Properties.Name -contains 'properties' -and $resolved.properties) { $typeValue = 'object' } elseif ($resolved.PSObject.Properties.Name -contains 'items' -and $resolved.items) { $typeValue = 'array' } } switch ($typeValue) { 'object' { $descriptor = [ordered]@{ type = 'object' nullable = $nullable required = @() properties = [ordered]@{} } if ($resolved.PSObject.Properties.Name -contains 'required' -and $resolved.required) { $descriptor.required = @($resolved.required) } if ($resolved.PSObject.Properties.Name -contains 'properties' -and $resolved.properties) { foreach ($prop in $resolved.properties.PSObject.Properties) { $descriptor.properties[$prop.Name] = Get-NormalizedSchema $prop.Value } } if ($resolved.PSObject.Properties.Name -contains 'additionalProperties') { $descriptor.additionalProperties = $resolved.additionalProperties } return $descriptor } 'array' { $descriptor = [ordered]@{ type = 'array' nullable = $nullable items = $null } if ($resolved.PSObject.Properties.Name -contains 'items') { $descriptor.items = Get-NormalizedSchema $resolved.items } if ($resolved.PSObject.Properties.Name -contains 'minItems') { $descriptor.minItems = [int]$resolved.minItems } if ($resolved.PSObject.Properties.Name -contains 'maxItems') { $descriptor.maxItems = [int]$resolved.maxItems } return $descriptor } default { $descriptor = [ordered]@{ type = if ($typeValue) { $typeValue } else { 'object' } nullable = $nullable } if ($resolved.PSObject.Properties.Name -contains 'enum' -and $resolved.enum) { $descriptor.enum = @($resolved.enum) } if ($resolved.PSObject.Properties.Name -contains 'format' -and $resolved.format) { $descriptor.format = $resolved.format } return $descriptor } } } function Add-ConversionDefinition { param( [string]$Key, [object]$Schema, [string]$Context ) if ([string]::IsNullOrWhiteSpace($Key) -or -not $Schema) { return $null } if ($script:SwaggerGeneratorState.ConversionDefinitions.Contains($Key)) { return $Key } $descriptor = Get-NormalizedSchema $Schema if (-not $descriptor) { return $null } $script:SwaggerGeneratorState.ConversionDefinitions[$Key] = [ordered]@{ Context = $Context Schema = $descriptor } [void]$script:SwaggerGeneratorState.ConversionKeySet.Add($Key) return $Key } # Applies -StripPathPrefix / -EnsurePathPrefix to an operation path function Format-GeneratedPath { param([string]$Path) if ([string]::IsNullOrWhiteSpace($Path)) { return $Path } $normalized = $Path.Trim() if (-not $normalized.StartsWith('/')) { $normalized = '/' + $normalized } if ($script:SwaggerGeneratorState.StripPrefix) { $prefixLength = $script:SwaggerGeneratorState.StripPrefix.Length if ($normalized.StartsWith($script:SwaggerGeneratorState.StripPrefix, [System.StringComparison]::OrdinalIgnoreCase)) { $normalized = $normalized.Substring($prefixLength) } $normalized = '/' + $normalized.TrimStart('/') return $normalized } if ($script:SwaggerGeneratorState.EnsurePrefix) { if (-not $normalized.StartsWith($script:SwaggerGeneratorState.EnsurePrefix, [System.StringComparison]::OrdinalIgnoreCase)) { $normalized = $script:SwaggerGeneratorState.EnsurePrefix.TrimEnd('/') + '/' + $normalized.TrimStart('/') } if (-not $normalized.StartsWith('/')) { $normalized = '/' + $normalized } return $normalized } return $normalized } function Get-SchemaDetail { param([object]$Schema) $resolved = Resolve-ComponentSchema $Schema $meta = [ordered]@{ Schema = $resolved PSType = Get-PSTypeFromSchema $resolved Nullable = $false ReadOnly = $false WriteOnly = $false HasDefault = $false DefaultValue = $null DefaultLiteral = $null EnumValues = @() Format = $null Description = $null } if ($resolved) { if ($resolved.PSObject.Properties.Name -contains 'nullable' -and $resolved.nullable) { $meta.Nullable = [bool]$resolved.nullable } if ($resolved.PSObject.Properties.Name -contains 'readOnly' -and $resolved.readOnly) { $meta.ReadOnly = [bool]$resolved.readOnly } if ($resolved.PSObject.Properties.Name -contains 'writeOnly' -and $resolved.writeOnly) { $meta.WriteOnly = [bool]$resolved.writeOnly } if ($resolved.PSObject.Properties.Name -contains 'default') { $meta.HasDefault = $true $meta.DefaultValue = $resolved.default if ($meta.DefaultValue -is [string]) { $jsonCandidate = $meta.DefaultValue.Trim() $isArrayType = $meta.PSType -match '\[\]' $isHashtableType = $meta.PSType -eq '[hashtable]' -or $meta.PSType -eq '[object]' -or $meta.PSType -eq '[pscustomobject]' $isHashtableArrayType = $meta.PSType -eq '[hashtable[]]' -or $meta.PSType -eq '[object[]]' if ($jsonCandidate -like '`[*`]') { if ($isArrayType -or $isHashtableArrayType) { try { $parsedArray = ConvertFrom-Json -InputObject $jsonCandidate -ErrorAction Stop if ($null -ne $parsedArray) { $meta.DefaultValue = $parsedArray } } catch { # fall back to original string default Write-Debug "Ignored: $($_.Exception.Message)" } } } elseif ($jsonCandidate -like '{*}') { if ($isHashtableType) { try { $parsedObject = ConvertFrom-Json -InputObject $jsonCandidate -ErrorAction Stop if ($null -ne $parsedObject) { $meta.DefaultValue = $parsedObject } } catch { # fall back to original string default Write-Debug "Ignored: $($_.Exception.Message)" } } } } $meta.DefaultLiteral = ConvertTo-PowerShellLiteral $meta.DefaultValue } if ($resolved.PSObject.Properties.Name -contains 'enum' -and $resolved.enum) { $meta.EnumValues = @($resolved.enum) } if ($resolved.PSObject.Properties.Name -contains 'format' -and $resolved.format) { $meta.Format = $resolved.format } if ($resolved.PSObject.Properties.Name -contains 'description' -and $resolved.description) { $meta.Description = $resolved.description -replace "`r|`n", ' ' } } return [pscustomobject]$meta } function Get-BodyParamDefinition { param( [object]$Schema, [string[]]$PathSegments, [hashtable]$DescriptionHints, [hashtable]$UsedNames, [System.Collections.ArrayList]$ReadOnlyCollector, [bool]$ParentIsRequired = $false ) $definitions = [System.Collections.Generic.List[object]]::new() if (-not $Schema) { return $definitions } $resolved = Resolve-ComponentSchema $Schema if (-not $resolved) { return $definitions } $compositeSchemas = [System.Collections.Generic.List[object]]::new() if ($resolved.PSObject.Properties.Name -contains 'allOf') { foreach ($component in $resolved.allOf) { $compositeSchemas.Add($component) } } if ($compositeSchemas.Count -gt 0) { foreach ($component in $compositeSchemas) { $childDefs = Get-BodyParamDefinition -Schema $component -PathSegments $PathSegments -DescriptionHints $DescriptionHints -UsedNames $UsedNames -ReadOnlyCollector $ReadOnlyCollector -ParentIsRequired $ParentIsRequired foreach ($def in $childDefs) { $definitions.Add($def) } } } $effectiveSchema = $resolved if (-not $effectiveSchema.properties) { Write-Verbose " No properties found in effectiveSchema for path: $($PathSegments -join '.')" Write-Verbose " effectiveSchema type: $($effectiveSchema.GetType().FullName)" Write-Verbose " effectiveSchema.PSObject.Properties.Name: $($effectiveSchema.PSObject.Properties.Name -join ', ')" return $definitions } $requiredProps = @() if ($effectiveSchema.required) { $requiredProps = @($effectiveSchema.required) } foreach ($prop in $effectiveSchema.properties.PSObject.Properties) { $origPropName = $prop.Name $childPath = $PathSegments + $origPropName $propMeta = Get-SchemaDetail $prop.Value if ($propMeta.ReadOnly) { if ($ReadOnlyCollector) { [void]$ReadOnlyCollector.Add(($childPath -join '.')) } continue } $resolvedChild = Resolve-ComponentSchema $propMeta.Schema $hasChildProperties = $false if ($resolvedChild) { if ($resolvedChild.PSObject.Properties.Name -contains 'properties' -and $resolvedChild.properties) { if ($resolvedChild.properties.PSObject.Properties.Count -gt 0) { $hasChildProperties = $true } } if (-not $hasChildProperties -and $resolvedChild.PSObject.Properties.Name -contains 'allOf') { foreach ($component in $resolvedChild.allOf) { $resolvedComponent = Resolve-ComponentSchema $component if ($resolvedComponent -and $resolvedComponent.PSObject.Properties.Name -contains 'properties' -and $resolvedComponent.properties -and $resolvedComponent.properties.PSObject.Properties.Count -gt 0) { $hasChildProperties = $true break } } } } $paramName = Get-UniqueBodyParameterName -Segments $childPath -UsedNames $UsedNames $psType = $propMeta.PSType $isRequired = $requiredProps -contains $origPropName $hasEffectiveDefault = $false # Treat empty strings, nulls, and all-zeros GUIDs as "no effective default" so they're omitted when not provided if ($propMeta.HasDefault -and $propMeta.DefaultLiteral -and $propMeta.DefaultLiteral -ne "''" -and $propMeta.DefaultLiteral -ne '$null' -and $propMeta.DefaultLiteral -ne "'00000000-0000-0000-0000-000000000000'") { $hasEffectiveDefault = $true } $allowsNull = [bool]$propMeta.Nullable if (-not $allowsNull -and $propMeta.HasDefault -and $null -eq $propMeta.DefaultValue) { $allowsNull = $true } if (-not $allowsNull -and $propMeta.DefaultLiteral -eq '$null') { $allowsNull = $true } $description = $propMeta.Description if (-not $description) { $description = "Body property ${origPropName}" } $description = Format-HelpTextSegment $description if (-not $description) { $description = "Body property ${origPropName}" } $hint = Get-HintForPath -DescriptionHints $DescriptionHints -PathSegments $childPath -ParameterName $paramName if ($hint) { $hint = Format-HelpTextSegment $hint } $definitions.Add([pscustomobject]@{ Name = $paramName PathSegments = $childPath OrigName = $origPropName Type = $psType Required = $isRequired ParentRequired = $ParentIsRequired HasDefault = $propMeta.HasDefault HasEffectiveDefault = $hasEffectiveDefault DefaultLiteral = $propMeta.DefaultLiteral DefaultValue = $propMeta.DefaultValue AllowsNull = $allowsNull Enum = $propMeta.EnumValues Format = $propMeta.Format Nullable = $propMeta.Nullable WriteOnly = $propMeta.WriteOnly Hint = $hint HelpSummary = "(body: $($childPath -join '.')) - $description" Description = $description HasChildProperties = $hasChildProperties }) if ($resolvedChild -and $resolvedChild.type -eq 'object' -and $resolvedChild.properties) { $childDefs = Get-BodyParamDefinition -Schema $propMeta.Schema -PathSegments $childPath -DescriptionHints $DescriptionHints -UsedNames $UsedNames -ReadOnlyCollector $ReadOnlyCollector -ParentIsRequired ($isRequired -or $ParentIsRequired) foreach ($def in $childDefs) { $definitions.Add($def) } } } return $definitions } function Get-SwaggerParamValidation { param([string]$operationKey) return $script:SwaggerGeneratorState.Validation[$operationKey] } function Get-SwaggerParamLabelToId { param([string]$operationKey) return $script:SwaggerGeneratorState.LabelToId[$operationKey] } function Get-OperationKey { param([string]$Path, [string]$Method) $normalizedPath = Format-GeneratedPath -Path $Path return (($normalizedPath.TrimStart('/')) -replace '[^A-Za-z0-9]', '_') + '_' + $Method.ToLowerInvariant() } # Fills the Validation and LabelToId maps from parameter enums and "name": "label=id|..." hints in operation descriptions function Initialize-SwaggerParamMapping { # 1) explicit enums from components/schemas if ($script:SwaggerGeneratorState.Swagger.components -and $script:SwaggerGeneratorState.Swagger.components.schemas) { foreach ($schemaName in $script:SwaggerGeneratorState.Swagger.components.schemas.PSObject.Properties.Name) { $schema = $script:SwaggerGeneratorState.Swagger.components.schemas.$schemaName if ($schema.properties) { foreach ($propName in $schema.properties.PSObject.Properties.Name) { $prop = $schema.properties.$propName if ($prop.enum) { $key = "schema::$schemaName" if (-not $script:SwaggerGeneratorState.Validation.ContainsKey($key)) { $script:SwaggerGeneratorState.Validation[$key] = @{} } $script:SwaggerGeneratorState.Validation[$key][$propName] = @($prop.enum) } } } } } # 2) operations: parameter enums and parameter hints in the description foreach ($path in $script:SwaggerGeneratorState.Swagger.paths.PSObject.Properties.Name) { $pathObj = $script:SwaggerGeneratorState.Swagger.paths.$path foreach ($methodProp in $pathObj.PSObject.Properties) { if ($script:SwaggerGeneratorState.HttpMethodNames -notcontains $methodProp.Name.ToLowerInvariant()) { continue } $op = $methodProp.Value if ($null -eq $op) { continue } $opKey = Get-OperationKey -Path $path -Method $methodProp.Name $localMap = @{} # a) explicit parameter schemas with enum if ($op.parameters) { foreach ($p in $op.parameters) { if ($p.schema -and $p.schema.enum) { $localMap[[string]$p.name] = @($p.schema.enum) } } } # b) description with a JSON-like block: attempt to extract quoted key:value pairs if ($op.description) { $desc = [string]$op.description $start = $desc.IndexOf('{') if ($start -ge 0) { $searchStart = [Math]::Min($start + 1, [Math]::Max(0, $desc.Length - 1)) $end = $desc.IndexOf('}', $searchStart) } else { $end = -1 } if ($start -ge 0 -and $end -gt $start) { $block = $desc.Substring($start, $end - $start + 1) $regex = '"([^\"]+)"\s*:\s*"([^\"]+)"' foreach ($pairMatch in [regex]::Matches($block, $regex)) { $k = $pairMatch.Groups[1].Value $v = $pairMatch.Groups[2].Value $parts = $v -split '\|' if ($parts.Count -eq 1) { $parts = $v -split ',' } $vals = [System.Collections.Generic.List[string]]::new() $idMap = [ordered]@{} foreach ($part in $parts) { $s = $part.Trim() if ($s -match '^(.*?)\s*=\s*([0-9]+)\s*$') { $label = ($matches[1]).Trim() $vals.Add($label) $idMap[$label] = [int]$matches[2] } elseif ($s -match '^(.*?)\s*=\s*([0-9a-fA-F\-]{1,})\s*$') { $label = ($matches[1]).Trim() $vals.Add($label) $idMap[$label] = $matches[2] } else { $vals.Add($s) } } if ($vals.Count -gt 0) { $localMap[$k] = @($vals) if ($idMap.Keys.Count -gt 0) { if (-not $script:SwaggerGeneratorState.LabelToId.ContainsKey($opKey)) { $script:SwaggerGeneratorState.LabelToId[$opKey] = @{} } $script:SwaggerGeneratorState.LabelToId[$opKey][$k] = $idMap } } } } } if ($localMap.Keys.Count -gt 0) { $script:SwaggerGeneratorState.Validation[$opKey] = $localMap } } } } |