Public/FileTransfer/Assert-VmFilesField.ps1
|
<#
.SYNOPSIS Validates the shape of a 'files' array on a VM definition. .DESCRIPTION Performs the shared shape checks for a 'files' array: - 'files' may be absent (returns silently). - When present, must be a JSON array (PSCustomObject or string is rejected). - Each entry must be a JSON object. Two entry forms are supported. The bulk form is gated behind -AllowBulkEntries so existing consumers keep their stricter single-form contract by default. Single form (always allowed) - see Assert-VmFileSingleEntry: { source, target [, ...consumer fields] } Bulk form (enabled by -AllowBulkEntries; targets Copy-VmFilesByPattern) - see Assert-VmFileBulkEntry: { pattern, targetDir [, recurse] [, preserveRelativePath] } Discrimination between the two forms is by the presence of 'source' vs 'pattern'. An entry containing both is ambiguous and rejected; an entry containing neither is rejected with a message that names both options. Additional per-entry rules can be supplied via -PostEntryValidator: a scriptblock that receives ($entry, $context) and throws on any violation. It runs after the shared shape checks for whichever form matched. .PARAMETER Vm The parsed VM definition object (the same object the rest of the schema sees). .PARAMETER AllowedSubFields Strict allow-list for sub-fields on each single-form entry. Defaults to @('source', 'target'). Pass a wider list when the schema is being extended (e.g. @('source', 'target', 'owner')). Does not affect bulk-form entries: those always use the fixed bulk allow-list owned by this function. .PARAMETER AllowBulkEntries Opt-in switch that enables the bulk entry form. Off by default so every existing caller keeps behaving exactly as before - this is the backward-compatibility guarantee for consumers that have not migrated their schemas. .PARAMETER PostEntryValidator Optional scriptblock invoked once per entry after all shared checks pass. Receives ($entry, $context). Should throw on violation. .PARAMETER PostEntryValidatorContext Optional value passed as the second argument to -PostEntryValidator. Use it to hand the validator any data it needs from the surrounding schema. .EXAMPLE # Default shape, single form only. Assert-VmFilesField -Vm $vm .EXAMPLE # Allow 'owner', require it, and validate against a known set. $context = @{ KnownUsers = $vm.users.name } Assert-VmFilesField ` -Vm $vm ` -AllowedSubFields @('source', 'target', 'owner') ` -PostEntryValidator { param($entry, $context) if (-not $entry.PSObject.Properties['owner']) { throw "files[*].owner is required." } if ($entry.owner -notin $context.KnownUsers) { throw "files[*].owner '$($entry.owner)' is not a known user." } } ` -PostEntryValidatorContext $context .EXAMPLE # Mixed single + bulk entries (e.g. Vm-Provisioner schema). Assert-VmFilesField -Vm $vm -AllowBulkEntries #> function Assert-VmFilesField { [CmdletBinding()] param( [Parameter(Mandatory)] [object] $Vm, [Parameter()] [string[]] $AllowedSubFields = @('source', 'target'), [Parameter()] [switch] $AllowBulkEntries, [Parameter()] [scriptblock] $PostEntryValidator, [Parameter()] [object] $PostEntryValidatorContext ) # Fixed bulk allow-list. Not exposed via a parameter: the bulk # form's sub-field set IS the contract with Copy-VmFilesByPattern, # not a per-consumer concern. $bulkAllowedSubFields = @('pattern', 'targetDir', 'recurse', 'preserveRelativePath') if (-not $Vm.PSObject.Properties['files']) { return } $vmName = if ($Vm.PSObject.Properties['vmName']) { $Vm.vmName } else { '(unknown)' } $ctx = "VM '$vmName': files" $files = $Vm.files if ($null -eq $files -or -not ($files -is [System.Collections.IEnumerable]) -or $files -is [string]) { throw "$ctx must be a JSON array of file entries." } $i = 0 foreach ($entry in $files) { $entryCtx = "$ctx[$i]" $i++ if ($null -eq $entry -or $entry -isnot [System.Management.Automation.PSCustomObject]) { throw "$entryCtx must be a JSON object." } if ($AllowBulkEntries) { # Discriminate by presence of 'source' vs 'pattern'. Done # before the per-form sub-field check so an ambiguous or # missing-discriminator entry produces an error that names # the intended form, instead of an 'unknown sub-field # pattern' that hides the real choice. $hasSource = [bool] $entry.PSObject.Properties['source'] $hasPattern = [bool] $entry.PSObject.Properties['pattern'] if ($hasSource -and $hasPattern) { throw "$entryCtx has both 'source' and 'pattern'; only one is allowed (single vs bulk form)." } if (-not $hasSource -and -not $hasPattern) { throw "$entryCtx is missing required sub-field; expected 'source' (single form) or 'pattern' (bulk form)." } if ($hasPattern) { Assert-VmFileBulkEntry ` -EntryCtx $entryCtx ` -Entry $entry ` -AllowedSubFields $bulkAllowedSubFields } else { Assert-VmFileSingleEntry ` -EntryCtx $entryCtx ` -Entry $entry ` -AllowedSubFields $AllowedSubFields } } else { Assert-VmFileSingleEntry ` -EntryCtx $entryCtx ` -Entry $entry ` -AllowedSubFields $AllowedSubFields } # Called after the shared checks pass so the validator can # assume the entry is well-formed for its matched form. if ($null -ne $PostEntryValidator) { & $PostEntryValidator $entry $PostEntryValidatorContext } } } |