Modules/IdLE.Core/Private/Test-IdleWorkflowSchema.ps1
|
function Test-IdleWorkflowSchema { [CmdletBinding()] param( [Parameter(Mandatory)] [hashtable] $Workflow ) # Strict validation: collect all schema violations and return them as a list. $errors = [System.Collections.Generic.List[string]]::new() $allowedRootKeys = @('Name', 'LifecycleEvent', 'Steps', 'OnFailureSteps', 'Description') foreach ($key in $Workflow.Keys) { if ($allowedRootKeys -notcontains $key) { $errors.Add("Unknown root key '$key'. Allowed keys: $($allowedRootKeys -join ', ').") } } if (-not $Workflow.ContainsKey('Name') -or [string]::IsNullOrWhiteSpace([string]$Workflow.Name)) { $errors.Add("Missing or empty required root key 'Name'.") } if (-not $Workflow.ContainsKey('LifecycleEvent') -or [string]::IsNullOrWhiteSpace([string]$Workflow.LifecycleEvent)) { $errors.Add("Missing or empty required root key 'LifecycleEvent'.") } if (-not $Workflow.ContainsKey('Steps') -or $null -eq $Workflow.Steps) { $errors.Add("Missing required root key 'Steps'.") } elseif ($Workflow.Steps -isnot [System.Collections.IEnumerable] -or $Workflow.Steps -is [string]) { $errors.Add("'Steps' must be an array/list of step hashtables.") } else { $stepNames = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) $i = 0 foreach ($step in $Workflow.Steps) { $stepPath = "Steps[$i]" if ($null -eq $step -or $step -isnot [hashtable]) { $errors.Add("$stepPath must be a hashtable.") $i++ continue } $allowedStepKeys = @('Name', 'Type', 'Condition', 'With', 'Description', 'RequiresCapabilities') foreach ($k in $step.Keys) { if ($allowedStepKeys -notcontains $k) { $errors.Add("Unknown key '$k' in $stepPath. Allowed keys: $($allowedStepKeys -join ', ').") } } if (-not $step.ContainsKey('Name') -or [string]::IsNullOrWhiteSpace([string]$step.Name)) { $errors.Add("Missing or empty required key '$stepPath.Name'.") } else { if (-not $stepNames.Add([string]$step.Name)) { $errors.Add("Duplicate step name '$($step.Name)' detected. Step names must be unique.") } } if (-not $step.ContainsKey('Type') -or [string]::IsNullOrWhiteSpace([string]$step.Type)) { $errors.Add("Missing or empty required key '$stepPath.Type'.") } # Conditions must be declarative data, never a ScriptBlock/expression. # We only enforce the shape here; semantic validation comes later. if ($step.ContainsKey('Condition') -and $null -ne $step.Condition -and $step.Condition -isnot [hashtable]) { $errors.Add("'$stepPath.Condition' must be a hashtable (declarative condition object).") } # 'With' is step parameter bag (data-only). Detailed validation comes with step metadata later. if ($step.ContainsKey('With') -and $null -ne $step.With -and $step.With -isnot [hashtable]) { $errors.Add("'$stepPath.With' must be a hashtable (step parameters).") } $i++ } } # OnFailureSteps are optional. If present, validate them like regular Steps. if ($Workflow.ContainsKey('OnFailureSteps') -and $null -ne $Workflow.OnFailureSteps) { if ($Workflow.OnFailureSteps -isnot [System.Collections.IEnumerable] -or $Workflow.OnFailureSteps -is [string]) { $errors.Add("'OnFailureSteps' must be an array/list of step hashtables.") } else { $failureStepNames = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) $i = 0 foreach ($step in $Workflow.OnFailureSteps) { $stepPath = "OnFailureSteps[$i]" if ($null -eq $step -or $step -isnot [hashtable]) { $errors.Add("$stepPath must be a hashtable.") $i++ continue } $allowedStepKeys = @('Name', 'Type', 'Condition', 'With', 'Description', 'RequiresCapabilities') foreach ($k in $step.Keys) { if ($allowedStepKeys -notcontains $k) { $errors.Add("Unknown key '$k' in $stepPath. Allowed keys: $($allowedStepKeys -join ', ').") } } if (-not $step.ContainsKey('Name') -or [string]::IsNullOrWhiteSpace([string]$step.Name)) { $errors.Add("Missing or empty required key '$stepPath.Name'.") } else { if (-not $failureStepNames.Add([string]$step.Name)) { $errors.Add("Duplicate step name '$($step.Name)' detected in 'OnFailureSteps'. Step names must be unique within this collection.") } } if (-not $step.ContainsKey('Type') -or [string]::IsNullOrWhiteSpace([string]$step.Type)) { $errors.Add("Missing or empty required key '$stepPath.Type'.") } # Conditions must be declarative data, never a ScriptBlock/expression. # We only enforce the shape here; semantic validation comes later. if ($step.ContainsKey('Condition') -and $null -ne $step.Condition -and $step.Condition -isnot [hashtable]) { $errors.Add("'$stepPath.Condition' must be a hashtable (declarative condition object).") } # 'With' is step parameter bag (data-only). Detailed validation comes with step metadata later. if ($step.ContainsKey('With') -and $null -ne $step.With -and $step.With -isnot [hashtable]) { $errors.Add("'$stepPath.With' must be a hashtable (step parameters).") } $i++ } } } return $errors } |