scripts/internal/yaml-list.ps1
|
<#
.SYNOPSIS Minimal read/serialize for a YAML "top-key: list-of-flat-mappings" schema (F-051). .DESCRIPTION Specrew deliberately avoids a `ConvertFrom-Yaml` / powershell-yaml dependency (see Read-IntakeYaml.ps1). This shared helper handles the specific, controlled schema used by the multi-session state files - a single top-level key whose value is a list of mappings with scalar string fields: <top-key>: - field_a: "value" field_b: "value" Reused by session-management.ps1 (active-sessions.yml) and feature-claims.ps1 (active-features.yml) so the parse/emit logic exists once, not copy-pasted. #> Set-StrictMode -Version Latest function ConvertTo-SpecrewYamlList { <# Serialize an array of [ordered] entries to the top-key list schema. #> param( [Parameter(Mandatory = $true)][string]$TopKey, [AllowEmptyCollection()][AllowNull()][object[]]$Entries ) $entries = @($Entries | Where-Object { $null -ne $_ }) if ($entries.Count -eq 0) { return ('{0}: []{1}' -f $TopKey, [Environment]::NewLine) } $sb = [System.Text.StringBuilder]::new() $null = $sb.Append($TopKey).Append(':').Append([Environment]::NewLine) foreach ($entry in $entries) { $first = $true foreach ($name in $entry.Keys) { $raw = [string]$entry[$name] $escaped = $raw.Replace('\', '\\').Replace('"', '\"') $prefix = if ($first) { ' - ' } else { ' ' } $null = $sb.Append($prefix).Append($name).Append(': "').Append($escaped).Append('"').Append([Environment]::NewLine) $first = $false } } return $sb.ToString() } function ConvertFrom-SpecrewYamlList { <# Parse the top-key list schema into an array of [ordered] hashtables. Tolerant of blank lines and comments; throws on a structurally broken entry so callers can degrade safely. Returns @() when the top key is absent. #> param( [Parameter(Mandatory = $true)][AllowEmptyString()][AllowNull()][string]$Content, [Parameter(Mandatory = $true)][string]$TopKey ) $entries = [System.Collections.Generic.List[object]]::new() if ([string]::IsNullOrWhiteSpace($Content)) { return @() } $lines = $Content -split "`r?`n" $inList = $false $current = $null $escapedKey = [regex]::Escape($TopKey) foreach ($line in $lines) { if ([string]::IsNullOrWhiteSpace($line)) { continue } $trimmedStart = $line.TrimStart() if ($trimmedStart.StartsWith('#')) { continue } if (-not $inList) { if ($line -match ("^{0}:\s*\[\s*\]\s*$" -f $escapedKey)) { return @() } # top-key: [] if ($line -match ("^{0}:\s*$" -f $escapedKey)) { $inList = $true; continue } # top-key: continue # ignore preamble/other keys } if ($line -match '^\s{2}-\s+(?<k>[A-Za-z0-9_]+):\s*(?<v>.*)$') { if ($null -ne $current) { $entries.Add($current) } $current = [ordered]@{} $current[$Matches['k']] = (Convert-SpecrewYamlScalar -Raw $Matches['v']) } elseif ($line -match '^\s{4}(?<k>[A-Za-z0-9_]+):\s*(?<v>.*)$') { if ($null -eq $current) { throw "Malformed YAML list: field line before any '- ' entry start." } $current[$Matches['k']] = (Convert-SpecrewYamlScalar -Raw $Matches['v']) } else { throw "Malformed YAML list line: '$line'" } } if ($null -ne $current) { $entries.Add($current) } return $entries.ToArray() } function Convert-SpecrewYamlScalar { param([Parameter(Mandatory = $true)][AllowEmptyString()][string]$Raw) $v = $Raw.Trim() if ($v.Length -ge 2 -and $v.StartsWith('"') -and $v.EndsWith('"')) { $v = $v.Substring(1, $v.Length - 2).Replace('\"', '"').Replace('\\', '\') } return $v } function Read-SpecrewYamlList { <# Read a YAML-list file safely: missing OR corrupt -> @() (+warning on corrupt). #> param( [Parameter(Mandatory = $true)][string]$Path, [Parameter(Mandatory = $true)][string]$TopKey ) if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return @() } try { $content = Get-Content -LiteralPath $Path -Raw -Encoding UTF8 return @(ConvertFrom-SpecrewYamlList -Content $content -TopKey $TopKey) } catch { Write-Warning ("Specrew: '{0}' could not be parsed ({1}); treating as empty." -f $Path, $_.Exception.Message) return @() } } |