Modules/IdLE.Core/Public/Test-IdleWorkflowDefinitionObject.ps1
|
function Test-IdleWorkflowDefinitionObject { <# .SYNOPSIS Loads and strictly validates a workflow definition (PSD1). .DESCRIPTION Performs strict schema validation (unknown keys = error), verifies the workflow is data-only (no ScriptBlocks), and optionally validates compatibility with a LifecycleRequest. .PARAMETER WorkflowPath Path to the workflow definition PSD1. .PARAMETER Request Optional request object. If provided, Workflow.LifecycleEvent must match Request.LifecycleEvent. .OUTPUTS PSCustomObject (PSTypeName: IdLE.WorkflowDefinition) #> [CmdletBinding()] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $WorkflowPath, [Parameter()] [AllowNull()] [object] $Request ) # 1) Load PSD1 data (no execution). $workflow = Import-IdleWorkflowDefinition -WorkflowPath $WorkflowPath # 2) Enforce "data-only": no ScriptBlocks anywhere in the workflow object. # This matches the project's config safety rules. Assert-IdleNoScriptBlock -InputObject $workflow -Path 'Workflow' # 3) Strict schema validation: unknown keys and missing required keys are errors. # using a resizable list to collect all violations and have .Add method available later $errors = [System.Collections.Generic.List[string]]::new() $schemaErrors = Test-IdleWorkflowSchema -Workflow $workflow if ($schemaErrors) { foreach ($e in @($schemaErrors)) { $null = $errors.Add([string]$e) } } # 4) Optional compatibility check with request (LifecycleEvent match). if ($null -ne $Request) { if (-not ($Request.PSObject.Properties.Name -contains 'LifecycleEvent')) { $errors.Add("Request object does not contain required property 'LifecycleEvent'.") } else { $reqEvent = [string]$Request.LifecycleEvent $wfEvent = [string]$workflow.LifecycleEvent if (-not [string]::IsNullOrWhiteSpace($reqEvent) -and -not $reqEvent.Equals($wfEvent, [System.StringComparison]::OrdinalIgnoreCase)) { $errors.Add("Workflow LifecycleEvent '$wfEvent' does not match request LifecycleEvent '$reqEvent'.") } } } # 4b) Validate step definitions (Name/Type/Condition/With + data-only). $idx = 0 foreach ($s in @($workflow.Steps)) { $stepErrors = Test-IdleStepDefinition -Step $s -Index $idx foreach ($e in @($stepErrors)) { $null = $errors.Add([string]$e) } $idx++ } # 4c) Validate OnFailureSteps definitions (Name/Type/Condition/With + data-only). # These are executed only when a run fails, but they must be valid workflow steps. if ($workflow.ContainsKey('OnFailureSteps') -and $null -ne $workflow.OnFailureSteps) { $idx = 0 foreach ($s in @($workflow.OnFailureSteps)) { $stepErrors = Test-IdleStepDefinition -Step $s -Index $idx foreach ($e in @($stepErrors)) { # Re-label errors so operators can clearly distinguish the step collection. $normalizedError = ([string]$e) -replace '^Step\[(\d+)\]', 'OnFailureSteps[$1]' $null = $errors.Add($normalizedError) } $idx++ } } if ($errors.Count -gt 0) { # Fail early with a single terminating exception, including all violations. $message = "Workflow validation failed:`n- " + ($errors -join "`n- ") throw [System.ArgumentException]::new($message, 'WorkflowPath') } # 5) Return normalized object (stable contract for planning). # PSCustomObject avoids class/type load-order problems across modules. return [pscustomobject]@{ PSTypeName = 'IdLE.WorkflowDefinition' Name = [string]$workflow.Name LifecycleEvent = [string]$workflow.LifecycleEvent Description = if ($workflow.ContainsKey('Description')) { [string]$workflow.Description } else { $null } Steps = @($workflow.Steps) OnFailureSteps = if ($workflow.ContainsKey('OnFailureSteps') -and $null -ne $workflow.OnFailureSteps) { @($workflow.OnFailureSteps) } else { @() } } } |