Private/SwaggerOutput.ps1

# Output helpers for New-FunctionsFromSwagger: writing generated files (honouring -PreviewOnly and ShouldProcess),
# removing files from earlier versions and applying FunctionCustomizations.ps1. They read $script:SwaggerGeneratorState.

# Reads a code template from Templates/Swagger (<Name>.ps1.template) with LF line endings
function Get-SwaggerTemplate {
    [CmdletBinding()]
    [OutputType([string])]
    param([Parameter(Mandatory = $true)][string] $Name)

    $templateFolder = Join-Path -Path (Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'Templates') -ChildPath 'Swagger'
    $templatePath = Join-Path -Path $templateFolder -ChildPath ($Name + '.ps1.template')
    # The file ends with one line break that is not part of the template
    $text = (Get-Content -LiteralPath $templatePath -Raw -Encoding UTF8 -ErrorAction Stop) -replace "`r`n", "`n"
    if ($text.EndsWith("`n")) { $text = $text.Substring(0, $text.Length - 1) }
    return $text
}

# Writes a generated file. -PreviewOnly: nothing is written. Otherwise ShouldProcess decides (-WhatIf, -Confirm).
# Content is written as UTF-8 (with a BOM for script files so Windows PowerShell 5.1 reads non-ASCII text) through a
# temporary file, so a failure never leaves a partial file behind. Every file is recorded in the run's FileResults.
function Write-AtomicFile {
    param(
        [Parameter(Mandatory = $true)][string] $Path,
        [Parameter(Mandatory = $true)][AllowEmptyString()][object] $Content,
        [Parameter()][string] $Kind = 'Function',
        [Parameter()][switch] $NoBom
    )

    $record = [pscustomobject]@{ Kind = $Kind; Name = (Split-Path -Path $Path -Leaf); Path = $Path; Action = 'Preview' }
    $script:SwaggerGeneratorState.FileResults.Add($record)
    if ($script:SwaggerGeneratorState.PreviewOnly) { return }
    if (-not $script:SwaggerGeneratorState.Cmdlet.ShouldProcess($Path, 'Write generated file')) {
        $record.Action = 'Skipped'
        return
    }

    if ($Content -is [array]) { $Content = ($Content -join "`n") + "`n" }
    $dir = Split-Path -Path $Path
    if (-not (Test-Path -LiteralPath $dir)) { $null = New-Item -ItemType Directory -Path $dir -Force -WhatIf:$false -Confirm:$false }

    $tmpName = Join-Path -Path $dir -ChildPath ([System.Guid]::NewGuid().ToString() + '.tmp')
    try {
        Write-Utf8TextFile -Path $tmpName -Content ([string]$Content) -Utf8Bom:(-not $NoBom)
        Move-Item -Force -LiteralPath $tmpName -Destination $Path -WhatIf:$false -Confirm:$false
    }
    catch {
        if (Test-Path -LiteralPath $tmpName) { Remove-Item -LiteralPath $tmpName -Force -ErrorAction SilentlyContinue -WhatIf:$false -Confirm:$false }
        throw
    }
    $record.Action = 'Written'
}


# Removes a file written by an earlier version of the generator (honours -PreviewOnly and ShouldProcess)
function Invoke-LegacyGeneratedFileRemoval {
    param([Parameter(Mandatory = $true)][string] $Path)

    if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return }
    # Never remove a file this run writes (for example when the noun prefix makes the same file name)
    foreach ($recorded in $script:SwaggerGeneratorState.FileResults) {
        if ([string]::Equals($recorded.Path, $Path, [System.StringComparison]::OrdinalIgnoreCase)) { return }
    }
    $record = [pscustomobject]@{ Kind = 'Removed'; Name = (Split-Path -Path $Path -Leaf); Path = $Path; Action = 'Preview' }
    $script:SwaggerGeneratorState.FileResults.Add($record)
    if ($script:SwaggerGeneratorState.PreviewOnly) { return }
    if ($script:SwaggerGeneratorState.Cmdlet.ShouldProcess($Path, 'Remove file written by an earlier version of New-FunctionsFromSwagger')) {
        Remove-Item -LiteralPath $Path -Force -WhatIf:$false -Confirm:$false
        $record.Action = 'Removed'
    }
    else {
        $record.Action = 'Skipped'
    }
}


# Loads Public/Generated/FunctionCustomizations.ps1 from the target module (user-authored settings, not generated code)
function Get-GeneratedFunctionCustomizationMap {
    if ($null -ne $script:SwaggerGeneratorState.CustomizationCache) { return $script:SwaggerGeneratorState.CustomizationCache }
    $script:SwaggerGeneratorState.CustomizationCache = @{}
    $customDir = Join-Path -Path (Join-Path -Path $script:SwaggerGeneratorState.ModuleRoot -ChildPath 'Public') -ChildPath 'Generated'
    $customFile = Join-Path $customDir 'FunctionCustomizations.ps1'
    if (Test-Path -LiteralPath $customFile) {
        try {
            $result = . $customFile
            if ($result -is [hashtable]) { $script:SwaggerGeneratorState.CustomizationCache = $result }
            elseif (Get-Variable -Name 'FunctionCustomizations' -Scope 0 -ErrorAction SilentlyContinue) {
                $candidate = (Get-Variable -Name 'FunctionCustomizations' -Scope 0 -ErrorAction SilentlyContinue).Value
                if ($candidate -is [hashtable]) { $script:SwaggerGeneratorState.CustomizationCache = $candidate }
                Remove-Variable -Name 'FunctionCustomizations' -Scope 0 -ErrorAction SilentlyContinue
            }
            elseif ($null -ne $result -and $result.PSObject.Properties.Name -contains 'FunctionCustomizations' -and $result.FunctionCustomizations -is [hashtable]) {
                $script:SwaggerGeneratorState.CustomizationCache = $result.FunctionCustomizations
            }
        }
        catch {
            Write-Warning "Failed to load function customization script at ${customFile}: $($_.Exception.Message)"
        }
    }
    if (-not $script:SwaggerGeneratorState.CustomizationCache) { $script:SwaggerGeneratorState.CustomizationCache = @{} }
    return $script:SwaggerGeneratorState.CustomizationCache
}


function Invoke-GeneratedFunctionCustomization {
    param(
        [Parameter(Mandatory)][string] $FunctionName,
        [Parameter()][AllowNull()][string] $Content,
        [Parameter()][hashtable] $Metadata
    )

    $map = Get-GeneratedFunctionCustomizationMap
    if (-not $map -or $map.Count -eq 0) { return $Content }

    $current = $Content

    if ($map.ContainsKey('__Pipeline')) {
        foreach ($transform in @($map['__Pipeline'])) {
            if ($transform -isnot [scriptblock]) { continue }
            $ctx = [pscustomobject]@{ FunctionName = $FunctionName; Content = $current; Metadata = $Metadata }
            try {
                $result = & $transform $ctx
                if ($null -ne $result) {
                    if ($result -is [string]) { $current = $result }
                    elseif ($result -is [hashtable] -and $result.ContainsKey('Content')) { $current = [string]$result['Content'] }
                    elseif ($result -is [psobject] -and $result.PSObject.Properties.Name -contains 'Content') { $current = [string]$result.Content }
                }
            }
            catch {
                Write-Warning "Function customization pipeline for $FunctionName failed: $($_.Exception.Message)"
            }
        }
    }

    if (-not $map.ContainsKey($FunctionName)) { return $current }

    $spec = $map[$FunctionName]
    if ($null -eq $spec) { return $current }

    if ($spec -is [string]) { return [string]$spec }
    if ($spec -is [scriptblock]) {
        $ctx = [pscustomobject]@{ FunctionName = $FunctionName; Content = $current; Metadata = $Metadata }
        try {
            $res = & $spec $ctx
            if ($null -eq $res) { return $current }
            if ($res -is [string]) { return $res }
            if ($res -is [hashtable] -and $res.ContainsKey('Content')) { return [string]$res['Content'] }
            if ($res -is [psobject] -and $res.PSObject.Properties.Name -contains 'Content') { return [string]$res.Content }
            return $current
        }
        catch {
            Write-Warning "Function customization for $FunctionName failed: $($_.Exception.Message)"
            return $current
        }
    }
    if ($spec -is [hashtable]) {
        $mode = 'Transform'
        if ($spec.ContainsKey('Mode') -and $spec['Mode']) { $mode = [string]$spec['Mode'] }
        switch ($mode.ToLowerInvariant()) {
            'replace' {
                if ($spec.ContainsKey('Content')) { return [string]$spec['Content'] }
                elseif ($spec.ContainsKey('Script') -and $spec['Script'] -is [scriptblock]) {
                    $ctx = [pscustomobject]@{ FunctionName = $FunctionName; Content = $current; Metadata = $Metadata; Spec = $spec }
                    try {
                        $res = & $spec['Script'] $ctx
                        if ($null -eq $res) { return $current }
                        if ($res -is [string]) { return $res }
                        if ($res -is [hashtable] -and $res.ContainsKey('Content')) { return [string]$res['Content'] }
                        if ($res -is [psobject] -and $res.PSObject.Properties.Name -contains 'Content') { return [string]$res.Content }
                        return $current
                    }
                    catch {
                        Write-Warning "Replacement customization for $FunctionName failed: $($_.Exception.Message)"
                        return $current
                    }
                }
                return $current
            }
            'skip' { return $null }
            default {
                $handler = $null
                if ($spec.ContainsKey('Script') -and $spec['Script'] -is [scriptblock]) { $handler = $spec['Script'] }
                if ($null -eq $handler -and $spec.ContainsKey('Content')) { return [string]$spec['Content'] }
                if ($null -eq $handler) { return $current }
                $ctx = [pscustomobject]@{ FunctionName = $FunctionName; Content = $current; Metadata = $Metadata; Spec = $spec }
                try {
                    $res = & $handler $ctx
                    if ($null -eq $res) { return $current }
                    if ($res -is [string]) { return $res }
                    if ($res -is [hashtable] -and $res.ContainsKey('Content')) { return [string]$res['Content'] }
                    if ($res -is [psobject] -and $res.PSObject.Properties.Name -contains 'Content') { return [string]$res.Content }
                    return $current
                }
                catch {
                    Write-Warning "Transform customization for $FunctionName failed: $($_.Exception.Message)"
                    return $current
                }
            }
        }
    }

    return $current
}