Private/Generator/Write-OpenApiGenPlan.ps1
|
function Write-OpenApiGenPlan { <# .SYNOPSIS Writes a generation plan to disk and reports what it did with every file. .DESCRIPTION The only generator function that touches the disk. For each planned file: missing -> Created; same bytes -> Unchanged; different -> Replaced, but only with -Force. Overrides.ps1 is created when missing and otherwise always Preserved, even with -Force. With -Force, generated command files under Public/ that the plan no longer has are Removed. Without -Force, any file that would be replaced or removed stops the run before anything is written. Every write and removal goes through ShouldProcess: with -WhatIf nothing is written and the affected files are reported as Skipped (PlannedAction says what would have happened). Files are UTF-8 with LF line endings, with a BOM only when the text is not plain ASCII. Returns one { RelativePath, Path, Kind, FunctionName, OperationId, Action, PlannedAction } per file. #> [CmdletBinding(SupportsShouldProcess = $true)] [OutputType([pscustomobject])] param( [Parameter(Mandatory = $true)] [object]$Plan, [Parameter()] [switch]$Force ) $root = $Plan.ModulePath $utf8 = New-Object -TypeName System.Text.UTF8Encoding -ArgumentList $false $toFullPath = { param([string]$RelativePath) $full = $root foreach ($part in $RelativePath.Split('/')) { $full = Join-Path -Path $full -ChildPath $part } return $full } $toBytes = { param([string]$Text) $body = $utf8.GetBytes($Text) if ($Text -match '[^\x00-\x7F]') { $withBom = New-Object -TypeName 'byte[]' -ArgumentList ($body.Length + 3) $withBom[0] = 0xEF $withBom[1] = 0xBB $withBom[2] = 0xBF [System.Array]::Copy($body, 0, $withBom, 3, $body.Length) return , $withBom } return , $body } $sameBytes = { param([byte[]]$Left, [byte[]]$Right) return ($Left.Length -eq $Right.Length) -and ([System.Convert]::ToBase64String($Left) -ceq [System.Convert]::ToBase64String($Right)) } # 1. Decide what happens to every file $entries = New-Object -TypeName System.Collections.ArrayList foreach ($file in @($Plan.Files)) { $fullPath = & $toFullPath $file.RelativePath $bytes = & $toBytes $file.Content $planned = 'Create' if (Test-Path -LiteralPath $fullPath -PathType Leaf) { if ($file.Kind -eq 'Overrides') { $planned = 'Preserve' } elseif (& $sameBytes ([System.IO.File]::ReadAllBytes($fullPath)) $bytes) { $planned = 'Unchanged' } else { $planned = 'Replace' } } [void]$entries.Add([pscustomobject]@{ File = $file; Path = $fullPath; Bytes = $bytes; Planned = $planned }) } $publicRoot = Join-Path -Path $root -ChildPath 'Public' if (Test-Path -LiteralPath $publicRoot -PathType Container) { $plannedPaths = New-Object -TypeName 'System.Collections.Generic.HashSet[string]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) foreach ($entry in $entries) { [void]$plannedPaths.Add([System.IO.Path]::GetFullPath($entry.Path)) } $existing = @(Get-ChildItem -LiteralPath $publicRoot -Filter '*.ps1' -Recurse -File | Sort-Object -Property FullName) foreach ($item in $existing) { if (-not $plannedPaths.Contains([System.IO.Path]::GetFullPath($item.FullName))) { $relative = 'Public/' + $item.FullName.Substring($publicRoot.Length).TrimStart('\', '/').Replace('\', '/') $stale = [pscustomobject]@{ RelativePath = $relative; Kind = 'Stale'; Content = $null; FunctionName = $item.BaseName; OperationId = $null } [void]$entries.Add([pscustomobject]@{ File = $stale; Path = $item.FullName; Bytes = $null; Planned = 'Remove' }) } } } $conflicts = @($entries | Where-Object -FilterScript { $_.Planned -eq 'Replace' -or $_.Planned -eq 'Remove' }) if ($conflicts.Count -gt 0 -and -not $Force) { $names = @($conflicts | Select-Object -First 5 | ForEach-Object -Process { $_.File.RelativePath }) -join ', ' throw "The module folder '$root' already has $($conflicts.Count) generated file(s) that differ from the new output ($names). Use -Force to replace them; Overrides.ps1 is never replaced." } # 2. Apply foreach ($entry in $entries) { $action = $null switch ($entry.Planned) { 'Unchanged' { $action = 'Unchanged' } 'Preserve' { $action = 'Preserved' } 'Remove' { $action = 'Skipped' if ($PSCmdlet.ShouldProcess($entry.Path, 'Remove generated file that is no longer in the document')) { Remove-Item -LiteralPath $entry.Path -Force $action = 'Removed' } } default { $action = 'Skipped' $verb = 'Create file' if ($entry.Planned -eq 'Replace') { $verb = 'Replace file' } if ($PSCmdlet.ShouldProcess($entry.Path, $verb)) { $directory = Split-Path -Path $entry.Path -Parent if (-not (Test-Path -LiteralPath $directory -PathType Container)) { [void](New-Item -Path $directory -ItemType Directory -Force) } [System.IO.File]::WriteAllBytes($entry.Path, $entry.Bytes) $action = 'Created' if ($entry.Planned -eq 'Replace') { $action = 'Replaced' } } } } [pscustomobject]@{ PSTypeName = 'Tcs.OpenApi.GeneratedFile' RelativePath = $entry.File.RelativePath Path = $entry.Path Kind = $entry.File.Kind FunctionName = $entry.File.FunctionName OperationId = $entry.File.OperationId Action = $action PlannedAction = $entry.Planned } } } |