Modules/IdLE.Core/Private/Assert-IdleExecutionOptions.ps1
|
# Asserts that ExecutionOptions is valid and rejects ScriptBlocks. # Validates the structure and constraints for retry profiles. # Retry parameter limits (hard constraints to prevent misconfiguration) $script:IDLE_RETRY_MAX_ATTEMPTS_LIMIT = 10 $script:IDLE_RETRY_INITIAL_DELAY_MS_LIMIT = 60000 $script:IDLE_RETRY_MAX_DELAY_MS_LIMIT = 300000 function Assert-IdleExecutionOptions { [CmdletBinding()] param( [Parameter(Mandatory)] [AllowNull()] [object] $ExecutionOptions ) if ($null -eq $ExecutionOptions) { return } # ExecutionOptions must be a hashtable or IDictionary if ($ExecutionOptions -isnot [System.Collections.IDictionary]) { throw [System.ArgumentException]::new( 'ExecutionOptions must be a hashtable or IDictionary.', 'ExecutionOptions' ) } # Reject ScriptBlocks anywhere in ExecutionOptions Assert-IdleNoScriptBlock -InputObject $ExecutionOptions -Path 'ExecutionOptions' # Validate RetryProfiles if present if ($ExecutionOptions.Contains('RetryProfiles')) { $retryProfiles = $ExecutionOptions['RetryProfiles'] if ($null -ne $retryProfiles -and $retryProfiles -isnot [System.Collections.IDictionary]) { throw [System.ArgumentException]::new( 'ExecutionOptions.RetryProfiles must be a hashtable or IDictionary.', 'ExecutionOptions' ) } if ($null -ne $retryProfiles) { foreach ($profileKey in $retryProfiles.Keys) { # Profile key must match pattern: ^[A-Za-z0-9_.-]{1,64}$ if ([string]$profileKey -notmatch '^[A-Za-z0-9_.-]{1,64}$') { throw [System.ArgumentException]::new( "RetryProfile key '$profileKey' is invalid. Must match pattern: ^[A-Za-z0-9_.-]{1,64}$", 'ExecutionOptions' ) } $profile = $retryProfiles[$profileKey] if ($null -eq $profile) { throw [System.ArgumentException]::new( "RetryProfile '$profileKey' is null. Each profile must be a hashtable with retry parameters.", 'ExecutionOptions' ) } if ($profile -isnot [System.Collections.IDictionary]) { throw [System.ArgumentException]::new( "RetryProfile '$profileKey' must be a hashtable or IDictionary.", 'ExecutionOptions' ) } # Validate individual retry parameters Assert-IdleRetryProfile -Profile $profile -ProfileKey $profileKey } } } # Validate DefaultRetryProfile if present if ($ExecutionOptions.Contains('DefaultRetryProfile')) { $defaultProfile = $ExecutionOptions['DefaultRetryProfile'] if ($null -ne $defaultProfile -and [string]::IsNullOrWhiteSpace([string]$defaultProfile)) { throw [System.ArgumentException]::new( 'ExecutionOptions.DefaultRetryProfile must not be an empty string.', 'ExecutionOptions' ) } # DefaultRetryProfile must reference a valid profile key if ($null -ne $defaultProfile -and $ExecutionOptions.Contains('RetryProfiles')) { $retryProfiles = $ExecutionOptions['RetryProfiles'] if ($null -ne $retryProfiles -and -not $retryProfiles.Contains([string]$defaultProfile)) { throw [System.ArgumentException]::new( "DefaultRetryProfile '$defaultProfile' references a profile that does not exist in RetryProfiles.", 'ExecutionOptions' ) } } } } function Assert-IdleRetryProfile { [CmdletBinding()] param( [Parameter(Mandatory)] [ValidateNotNull()] [System.Collections.IDictionary] $Profile, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $ProfileKey ) # Validate MaxAttempts (0..10) if ($Profile.Contains('MaxAttempts')) { $maxAttempts = $Profile['MaxAttempts'] if ($maxAttempts -isnot [int] -or $maxAttempts -lt 0 -or $maxAttempts -gt $script:IDLE_RETRY_MAX_ATTEMPTS_LIMIT) { throw [System.ArgumentException]::new( "RetryProfile '$ProfileKey': MaxAttempts must be an integer between 0 and $script:IDLE_RETRY_MAX_ATTEMPTS_LIMIT (inclusive).", 'ExecutionOptions' ) } } # Validate InitialDelayMilliseconds (0..60000) if ($Profile.Contains('InitialDelayMilliseconds')) { $initialDelay = $Profile['InitialDelayMilliseconds'] if ($initialDelay -isnot [int] -or $initialDelay -lt 0 -or $initialDelay -gt $script:IDLE_RETRY_INITIAL_DELAY_MS_LIMIT) { throw [System.ArgumentException]::new( "RetryProfile '$ProfileKey': InitialDelayMilliseconds must be an integer between 0 and $script:IDLE_RETRY_INITIAL_DELAY_MS_LIMIT (inclusive).", 'ExecutionOptions' ) } } # Validate BackoffFactor (>= 1.0) if ($Profile.Contains('BackoffFactor')) { $backoffFactor = $Profile['BackoffFactor'] # Accept both int and double if (($backoffFactor -isnot [double] -and $backoffFactor -isnot [int]) -or ([double]$backoffFactor -lt 1.0)) { throw [System.ArgumentException]::new( "RetryProfile '$ProfileKey': BackoffFactor must be a number >= 1.0.", 'ExecutionOptions' ) } } # Validate MaxDelayMilliseconds (0..300000 and >= InitialDelayMilliseconds) if ($Profile.Contains('MaxDelayMilliseconds')) { $maxDelay = $Profile['MaxDelayMilliseconds'] if ($maxDelay -isnot [int] -or $maxDelay -lt 0 -or $maxDelay -gt $script:IDLE_RETRY_MAX_DELAY_MS_LIMIT) { throw [System.ArgumentException]::new( "RetryProfile '$ProfileKey': MaxDelayMilliseconds must be an integer between 0 and $script:IDLE_RETRY_MAX_DELAY_MS_LIMIT (inclusive).", 'ExecutionOptions' ) } # Check that MaxDelayMilliseconds >= InitialDelayMilliseconds # Use the profile's InitialDelayMilliseconds if present, otherwise use engine default (250ms) $initialDelay = if ($Profile.Contains('InitialDelayMilliseconds')) { $Profile['InitialDelayMilliseconds'] } else { 250 # Engine default } if ($maxDelay -lt $initialDelay) { throw [System.ArgumentException]::new( "RetryProfile '$ProfileKey': MaxDelayMilliseconds ($maxDelay) must be >= InitialDelayMilliseconds ($initialDelay).", 'ExecutionOptions' ) } } # Validate JitterRatio (0.0..1.0) if ($Profile.Contains('JitterRatio')) { $jitterRatio = $Profile['JitterRatio'] # Accept both int and double if (($jitterRatio -isnot [double] -and $jitterRatio -isnot [int]) -or ([double]$jitterRatio -lt 0.0) -or ([double]$jitterRatio -gt 1.0)) { throw [System.ArgumentException]::new( "RetryProfile '$ProfileKey': JitterRatio must be a number between 0.0 and 1.0 (inclusive).", 'ExecutionOptions' ) } } } |