Private/Assets/Test-AvmAssetConfig.ps1
|
function Test-AvmAssetConfig { <# .SYNOPSIS Validate a hashtable that purports to be an Avm pinned-asset config. .DESCRIPTION Throws [System.Data.DataException] with a precise message when the config fails the schema. Returns $true on success so it can be used in an assertion-style call (`Test-AvmAssetConfig $cfg | Out-Null`). Schema (avm.config.json - pinned-asset slice): { "schemaVersion": 1, "assets": { "<name>": { "source": "https://...", // required, https:// "ref": "<git-ref-or-sha>", // one of ref or sha256 "sha256": "<64-hex>", // one of ref or sha256 "path": "<subdir>", // optional "type": "git" | "archive" // optional, inferred } } } Notes: - Asset names must match ^[a-z][a-z0-9-]*$ (lowercase kebab-case). - Either 'ref' or 'sha256' is required; both is allowed (sha256 will be used to verify the materialised archive at resolve time). - 'source' must start with https:// (or file:// when -AllowFileUrls is set, used by tests and offline fixtures). - 'type' is optional. When omitted, downstream Resolve-AvmPinnedAsset infers it from the URL suffix (.tar.gz / .tgz / .zip -> archive, everything else -> git). - This validator only checks shape. Downloading, materialising, and SHA verification are the responsibility of the (separate) Resolve-AvmPinnedAsset slice. #> [CmdletBinding()] [OutputType([bool])] param( [Parameter(Mandatory, ValueFromPipeline)] [hashtable] $Config, # Test-only escape hatch. When set, source values may also start # with file:// (used by fixture configs under tests/fixtures/). [switch] $AllowFileUrls ) begin { Set-StrictMode -Version 3.0 $ErrorActionPreference = 'Stop' } process { if (-not $Config.ContainsKey('schemaVersion')) { throw [System.Data.DataException]::new("avm.config: missing 'schemaVersion'.") } if ($Config.schemaVersion -ne 1) { throw [System.Data.DataException]::new( "avm.config: unsupported schemaVersion '$($Config.schemaVersion)'. Expected 1.") } if (-not $Config.ContainsKey('assets')) { throw [System.Data.DataException]::new("avm.config: missing 'assets' map.") } $assets = $Config.assets if ($assets -isnot [System.Collections.IDictionary]) { throw [System.Data.DataException]::new("avm.config: 'assets' must be a hashtable.") } $nameRegex = '^[a-z][a-z0-9-]*$' $sha256Regex = '^[0-9a-f]{64}$' $validTypes = @('git', 'archive') $knownKeys = @('source', 'ref', 'sha256', 'path', 'type') foreach ($name in $assets.Keys) { if ($name -cnotmatch $nameRegex) { throw [System.Data.DataException]::new( "avm.config: asset name '$name' must be lowercase kebab-case (^[a-z][a-z0-9-]*$).") } $a = $assets[$name] if ($a -isnot [System.Collections.IDictionary]) { throw [System.Data.DataException]::new( "avm.config: asset '$name' descriptor must be a hashtable.") } foreach ($k in $a.Keys) { if ($knownKeys -cnotcontains $k) { throw [System.Data.DataException]::new( "avm.config: asset '$name' has unknown key '$k'. Allowed: $($knownKeys -join ', ').") } } if (-not $a.ContainsKey('source')) { throw [System.Data.DataException]::new( "avm.config: asset '$name' missing required key 'source'.") } $source = [string]$a.source if ([string]::IsNullOrWhiteSpace($source)) { throw [System.Data.DataException]::new( "avm.config: asset '$name' has empty 'source'.") } if (-not $source.StartsWith('https://')) { if (-not ($AllowFileUrls -and $source.StartsWith('file://'))) { throw [System.Data.DataException]::new( "avm.config: asset '$name' source must start with 'https://'.") } } $hasRef = $a.ContainsKey('ref') -and -not [string]::IsNullOrWhiteSpace([string]$a.ref) $hasSha = $a.ContainsKey('sha256') -and -not [string]::IsNullOrWhiteSpace([string]$a.sha256) if (-not ($hasRef -or $hasSha)) { throw [System.Data.DataException]::new( "avm.config: asset '$name' requires one of 'ref' or 'sha256'.") } if ($hasSha -and ([string]$a.sha256) -cnotmatch $sha256Regex) { throw [System.Data.DataException]::new( "avm.config: asset '$name' sha256 must be 64-char lowercase hex.") } if ($a.ContainsKey('type')) { if ($validTypes -cnotcontains $a.type) { throw [System.Data.DataException]::new( "avm.config: asset '$name' type '$($a.type)' is not one of: $($validTypes -join ', ').") } } if ($a.ContainsKey('path') -and [string]::IsNullOrWhiteSpace([string]$a.path)) { throw [System.Data.DataException]::new( "avm.config: asset '$name' has empty 'path'.") } } return $true } } |