Utilities/Test-ExpressionCacheProviderSpec.ps1
<#
.SYNOPSIS Validates a single ExpressionCache provider spec and normalizes it to a hashtable. .DESCRIPTION Enforces the strict provider descriptor contract used by Initialize-ExpressionCache: - Spec must include Name (string), GetOrCreate (string or scriptblock), and Config (hashtable/IDictionary). - Optional Initialize and Clear must be a function name (string) or a scriptblock if present. - Function-name strings are validated with Get-Command -Name <string>. - Returns a normalized **hashtable** (not PSCustomObject). .PARAMETER Spec Provider descriptor to validate. Accepts hashtable, ordered hashtable, or PSCustomObject. .OUTPUTS [hashtable] A normalized provider spec (Name, GetOrCreate, [Initialize], [ClearCache], Config as hashtable). .EXAMPLE $spec = @{ Name = 'InMemoryCache' GetOrCreate = 'Get-InMemory-CachedValue' Initialize = 'Initialize-InMemoryCache' ClearCache = 'Clear-InMemory-Cache' Config = @{ DefaultMaxAge = (New-TimeSpan -Minutes 10) } } $valid = Test-ExpressionCacheProviderSpec -Spec $spec #> function Test-ExpressionCacheProviderSpec { param([Parameter(Mandatory)][object]$Spec) function ConvertTo-Hashtable { param([Parameter(Mandatory)][object]$InputObject) # Handle accidental arrays (caused by stray outputs upstream) if ($InputObject -is [array]) { # Pick the single spec-like object if present $candidates = $InputObject | Where-Object { $_ -is [System.Collections.IDictionary] -or $_ -is [pscustomobject] } if ($candidates.Count -eq 1) { $InputObject = $candidates[0] } else { $types = ($InputObject | ForEach-Object { $_.GetType().FullName }) -join ', ' throw "ExpressionCache: Provider spec must be a hashtable/IDictionary or PSCustomObject. Got array: $types" } } if ($InputObject -is [System.Collections.IDictionary]) { return $InputObject } if ($InputObject -is [pscustomobject]) { $ht = @{} foreach ($p in $InputObject.PSObject.Properties) { $ht[$p.Name] = $p.Value } return $ht } throw "ExpressionCache: Provider spec must be a hashtable/IDictionary or PSCustomObject. Got: $($InputObject.GetType().FullName)" } function Ensure-Functor { param( [Parameter(Mandatory)][object]$Value, [Parameter(Mandatory)][string]$PropName, [Parameter(Mandatory)][string]$ProviderName ) if ($Value -is [scriptblock]) { return } # silent success if ($Value -is [string]) { if ([string]::IsNullOrWhiteSpace($Value)) { throw "ExpressionCache: Provider '$ProviderName': '$PropName' cannot be empty." } # Verify function exists; discard output to stay silent $null = Get-Command -Name $Value -ErrorAction SilentlyContinue if (-not $?) { throw "ExpressionCache: Provider '$ProviderName': command '$Value' (from '$PropName') not found." } return # silent success } throw "ExpressionCache: Provider '$ProviderName': '$PropName' must be a function name (string) or a scriptblock. Got: $($Value.GetType().FullName)" } # Normalize to plain hashtable (and strip accidental array wrappers) $specHt = ConvertTo-Hashtable $Spec foreach ($req in 'Name','GetOrCreate','Config') { if (-not ($specHt.Keys -contains $req) -or -not $specHt[$req]) { throw "ExpressionCache: Provider spec missing required property '$req'." } } if ($specHt['Name'] -isnot [string] -or [string]::IsNullOrWhiteSpace($specHt['Name'])) { throw "ExpressionCache: Provider 'Name' must be a non-empty string." } $name = [string]$specHt['Name'] # Validate functors (silent on success) Ensure-Functor -Value $specHt['GetOrCreate'] -PropName 'GetOrCreate' -ProviderName $name foreach ($opt in 'Initialize','Clear') { if ($specHt.Keys -contains $opt -and $null -ne $specHt[$opt]) { Ensure-Functor -Value $specHt[$opt] -PropName $opt -ProviderName $name } } # Config -> hashtable (and handle PSCO) $cfg = $specHt['Config'] if ($cfg -isnot [System.Collections.IDictionary] -and $cfg -isnot [pscustomobject]) { throw "ExpressionCache: Provider '$name': Config must be a hashtable or PSCustomObject." } $specHt['Config'] = ConvertTo-Hashtable $cfg return $specHt # only output; no stray $true’s } |