Private/Generator/New-OpenApiGenPlan.ps1

function New-OpenApiGenPlan {
    <#
    .SYNOPSIS
        Builds the generation plan for a module: every file with its content, the functions, the
        generator findings and the skipped operations. Writes nothing.

    .DESCRIPTION
        Operations are processed sorted by path, then method (ordinal). For each one: the command
        name (Get-OpenApiGenCommandName, then Resolve-OpenApiGenNameCollision across all operations),
        the parameter model, the operation metadata and the rendered wrapper, which must parse and bind
        (Test-OpenApiGenFunction) or the operation is skipped. Operations the document model flags as
        Unsupported, without an operationId or with the operationId of an earlier operation (ordinal) are
        skipped too. Every skip is an OA070 finding.

        Files (RelativePath uses '/'): <Name>.psd1, <Name>.psm1, README.md, Overrides.ps1 (Kind
        'Overrides': never overwritten), OpenApi/operations.json, OpenApi/source.json (compressed, without null or empty
        properties),
        Public/<Tag>/<Verb>-<Noun>.ps1 and Public/_Connection/<Verb>-<Prefix>Context.ps1. The output
        depends only on the inputs: same document and options give the same text.

        Returns { ModuleName, ModulePath, ManifestPath, Files, Functions, Findings, Skipped }.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Builds an in-memory plan only; Write-OpenApiGenPlan does the writing.')]
    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param(
        [Parameter(Mandatory = $true)]
        [object]$Document,

        [Parameter(Mandatory = $true)]
        [object]$Option,

        [Parameter(Mandatory = $true)]
        [hashtable]$Template
    )

    $findings = New-Object -TypeName System.Collections.ArrayList
    $skipped = New-Object -TypeName System.Collections.ArrayList
    $files = New-Object -TypeName System.Collections.ArrayList
    $functions = New-Object -TypeName System.Collections.ArrayList
    $metadataList = New-Object -TypeName System.Collections.ArrayList

    $addSkip = {
        param($Item, [string]$Reason, [string]$Severity)
        [void]$skipped.Add([pscustomobject]@{
                OperationId = $Item.OperationId
                Method      = ([string]$Item.Method).ToUpperInvariant()
                Path        = [string]$Item.Path
                Reason      = $Reason
            })
        [void]$findings.Add((New-OpenApiGenFinding -Severity $Severity -Code 'OA070' -Operation $Item -Message "Operation '$($Item.OperationId)' ($(([string]$Item.Method).ToUpperInvariant()) $($Item.Path)) was skipped: $Reason Call it with Invoke-OpenApiRequest instead."))
    }

    # Operations whose operationId the document model generated (finding OA010)
    $generatedIds = @($Document.Findings | Where-Object -FilterScript { $null -ne $_ -and $_.Code -eq 'OA010' } | ForEach-Object -Process { [string]$_.Operation })

    $operations = Get-OpenApiGenOrdinalSorted -InputObject @($Document.Operations) -Key { [string]$_.Path + [char]0 + ([string]$_.Method).ToUpperInvariant() }
    $supported = New-Object -TypeName System.Collections.ArrayList
    $seenIds = New-Object -TypeName 'System.Collections.Generic.HashSet[string]' -ArgumentList ([System.StringComparer]::Ordinal)
    foreach ($operation in $operations) {
        if ($operation.Unsupported -eq $true) {
            & $addSkip $operation 'the document model marked it as unsupported (see the earlier findings for this operation).' 'Warning'
            continue
        }
        # The metadata is keyed by operationId, so it must be present and unique
        if ([string]::IsNullOrEmpty([string]$operation.OperationId)) {
            & $addSkip $operation 'it has no operationId.' 'Error'
            continue
        }
        if (-not $seenIds.Add([string]$operation.OperationId)) {
            & $addSkip $operation 'another operation has the same operationId.' 'Error'
            continue
        }
        [void]$supported.Add($operation)
    }

    # Names
    $reservedNames = @('Set', 'Get', 'Remove') | ForEach-Object -Process { "$_-$($Option.Prefix)Context" }
    # The engine's commands must not be shadowed by a generated command
    $reservedNames += @('Get-OpenApiContext', 'Import-OpenApiDocument', 'Invoke-OpenApiRequest', 'New-OpenApiModule', 'Remove-OpenApiContext', 'Set-OpenApiContext', 'Test-OpenApiDocument')
    $candidates = @(foreach ($operation in $supported) {
            $candidate = Get-OpenApiGenCommandName -Operation $operation -NounPrefix $Option.NounPrefix -OperationIdGenerated:($generatedIds -ccontains [string]$operation.OperationId)
            foreach ($finding in $candidate.Findings) {
                [void]$findings.Add($finding)
            }
            $candidate
        })
    $resolved = Resolve-OpenApiGenNameCollision -Candidate $candidates -Reserved $reservedNames -BuiltIn (Get-OpenApiGenBuiltInCommandName) -BuiltInPrefix $Option.Prefix
    foreach ($finding in $resolved.Findings) {
        [void]$findings.Add($finding)
    }

    # Wrappers
    for ($i = 0; $i -lt $supported.Count; $i++) {
        $operation = $supported[$i]
        $name = $resolved.Names[$i]
        $model = Get-OpenApiGenParameterModel -Operation $operation -BaseNoun $name.BaseNoun -Schemas $Document.Schemas
        $metadata = ConvertTo-OpenApiGenMetadataEntry -Operation $operation -Document $Document -Service $Option.Service -UnwrapProperty ([string]$Option.UnwrapProperty)
        $text = ConvertTo-OpenApiGenFunction -CommandName $name -Operation $operation -ParameterModel $model -ResponseTypeName $metadata.ResponseTypeName -Template $Template['Function.ps1']
        $check = Test-OpenApiGenFunction -Text $text -FunctionName $name.Name
        if (-not $check.IsValid) {
            & $addSkip $operation ("the generated function did not pass the parse and bind check: " + ($check.Errors -join ' ') + '.') 'Error'
            continue
        }
        foreach ($finding in $model.Findings) {
            [void]$findings.Add($finding)
        }
        $tag = ''
        $tags = @($operation.Tags | Where-Object -FilterScript { -not [string]::IsNullOrWhiteSpace([string]$_) })
        if ($tags.Count -gt 0) {
            $tag = ConvertTo-OpenApiGenPascalCase -Value ([string]$tags[0])
        }
        if ($tag -eq '') {
            $tag = 'Default'
        }
        $relativePath = "Public/$tag/$($name.Name).ps1"
        [void]$files.Add([pscustomobject]@{ RelativePath = $relativePath; Kind = 'Function'; Content = $text; FunctionName = $name.Name; OperationId = $operation.OperationId })
        [void]$metadataList.Add($metadata)
        $summary = [string]$operation.Summary
        if ([string]::IsNullOrWhiteSpace($summary)) {
            $summary = [string]$operation.Description
        }
        [void]$functions.Add([pscustomobject]@{
                Name        = $name.Name
                OperationId = $operation.OperationId
                File        = $relativePath
                Tag         = $tag
                Method      = ([string]$operation.Method).ToUpperInvariant()
                Path        = [string]$operation.Path
                Summary     = $summary
            })
    }

    # Connection commands
    $authExample = Get-OpenApiGenAuthExample -Document $Document
    $connections = @(ConvertTo-OpenApiGenConnection -Prefix $Option.Prefix -Service $Option.Service -ModuleName $Option.ModuleName -Server @($Document.Servers) -AuthExample $authExample -Template $Template)
    foreach ($connection in $connections) {
        $check = Test-OpenApiGenFunction -Text $connection.Text -FunctionName $connection.Name
        if (-not $check.IsValid) {
            throw "The generated connection command '$($connection.Name)' is not valid: $($check.Errors -join ' ')"
        }
        [void]$files.Add([pscustomobject]@{ RelativePath = $connection.RelativePath; Kind = 'Function'; Content = $connection.Text; FunctionName = $connection.Name; OperationId = $null })
        [void]$functions.Add([pscustomobject]@{
                Name        = $connection.Name
                OperationId = $null
                File        = $connection.RelativePath
                Tag         = '_Connection'
                Method      = $null
                Path        = $null
                Summary     = "$(($connection.Name -split '-')[0]) the connection used by the commands of this module."
            })
    }

    # Module files
    $functionNames = [string[]]@($functions | ForEach-Object -Process { $_.Name })
    $manifestName = "$($Option.ModuleName).psd1"
    [void]$files.Add([pscustomobject]@{ RelativePath = $manifestName; Kind = 'Manifest'; Content = (ConvertTo-OpenApiGenManifest -Option $Option -Document $Document -FunctionName $functionNames -Template $Template['Module.psd1']); FunctionName = $null; OperationId = $null })
    $moduleValues = @{
        ModuleName       = $Option.ModuleName
        Prefix           = $Option.Prefix
        GeneratorVersion = $Option.GeneratorVersion
        ServiceLiteral   = (ConvertTo-OpenApiGenLiteral -Value $Option.Service)
    }
    [void]$files.Add([pscustomobject]@{ RelativePath = "$($Option.ModuleName).psm1"; Kind = 'RootModule'; Content = (Expand-OpenApiGenTemplate -Template $Template['Module.psm1'] -Value $moduleValues); FunctionName = $null; OperationId = $null })
    [void]$files.Add([pscustomobject]@{ RelativePath = 'Overrides.ps1'; Kind = 'Overrides'; Content = (Expand-OpenApiGenTemplate -Template $Template['Overrides.ps1'] -Value $moduleValues); FunctionName = $null; OperationId = $null })
    $readme = ConvertTo-OpenApiGenReadme -Option $Option -Document $Document -Function $functions.ToArray() -ConnectExample $connections[0].ConnectExample -Template $Template['README.md']
    [void]$files.Add([pscustomobject]@{ RelativePath = 'README.md'; Kind = 'Readme'; Content = $readme; FunctionName = $null; OperationId = $null })
    $sortedMetadata = Get-OpenApiGenOrdinalSorted -InputObject $metadataList.ToArray() -Key { [string]$_.OperationId }
    [void]$files.Add([pscustomobject]@{ RelativePath = 'OpenApi/operations.json'; Kind = 'Metadata'; Content = ((ConvertTo-OpenApiGenJson -InputObject $sortedMetadata) + "`n"); FunctionName = $null; OperationId = $null })
    [void]$files.Add([pscustomobject]@{ RelativePath = 'OpenApi/source.json'; Kind = 'Source'; Content = ((ConvertTo-OpenApiGenJson -InputObject $Document -Compress -SkipEmpty) + "`n"); FunctionName = $null; OperationId = $null })

    return [pscustomobject]@{
        PSTypeName   = 'Tcs.OpenApi.GenerationPlan'
        ModuleName   = $Option.ModuleName
        ModulePath   = $Option.ModulePath
        ManifestPath = (Join-Path -Path $Option.ModulePath -ChildPath $manifestName)
        Files        = (Get-OpenApiGenOrdinalSorted -InputObject $files.ToArray() -Key { $_.RelativePath })
        Functions    = (Get-OpenApiGenOrdinalSorted -InputObject $functions.ToArray() -Key { $_.Name })
        Findings     = $findings.ToArray()
        Skipped      = $skipped.ToArray()
    }
}