Private/Resolve-AppConfig.ps1
|
#Requires -Version 5.1 <# .SYNOPSIS Merges catalog app definitions with environment app references into a flat resolved array. .DESCRIPTION Accepts the raw config PSCustomObject (from ConvertFrom-Json) and an environment name string. Looks up the environment definition, iterates its apps array, and for each app reference resolves the final per-app properties by merging the catalog entry with any environment-level overrides. Merge rules: - Name = app name (catalog key) - Path = catalog.path - ProcessName = catalog.processName - Arguments = environment override if the 'arguments' property is present on the env app reference (even if empty string), else catalog.arguments, else '' (empty string). The check uses PSObject.Properties to distinguish a present-but-empty-string override from an absent property. - SkipMaximize = catalog.skipMaximize if present, else $false - Priority = environment app ref priority if present (integer), else 0 Apps referenced in the environment but missing from the catalog are skipped with an ERROR log entry. They are not included in the returned array. Processing continues for remaining apps. Throws a terminating error if the requested environment name is not found in the config. This is a terminal condition -- the caller must validate the environment name before calling this function. .PARAMETER Config The parsed config object returned by ConvertFrom-Json. Must have .apps and .environments properties matching the Envoke config schema. .PARAMETER EnvironmentName The name of the environment to resolve. Must match a key in $Config.environments. Case-sensitive (PSCustomObject property access is case-sensitive on lookup). .OUTPUTS PSCustomObject[] Array of resolved app objects. Each object has: Name, Path, ProcessName, Arguments, SkipMaximize, Priority. Empty array if the environment has no apps or all references were missing from the catalog. .NOTES Author: Aaron AlAnsari Created: 2026-02-25 The PSObject.Properties[$appName] access pattern is required for ConvertFrom-Json output. Direct dot notation ($Config.apps.$appName) returns $null for missing keys in most cases, but PSObject.Properties provides a reliable existence check. The 'arguments' override rule uses PSObject.Properties['arguments'] to distinguish: - Property absent (no override key in JSON) -> fall back to catalog.arguments - Property present with empty string ("") -> use override (replaces catalog) This matches the config schema description: "Replaces (does not append to) the catalog default." #> # PSUseOutputTypeCorrectly: the unary comma operator forces a PowerShell Object[] wrapper # around the List.ToArray() result to prevent single-element unboxing through the pipeline. # The element type is always PSCustomObject; the Object[] container is an implementation detail. [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseOutputTypeCorrectly', '')] param () function Resolve-AppConfig { [CmdletBinding()] [OutputType([PSCustomObject[]])] param ( [Parameter(Mandatory)] [PSCustomObject]$Config, [Parameter(Mandatory)] [string]$EnvironmentName ) # Guard: environment must exist in config. $envDef = $Config.environments.$EnvironmentName if ($null -eq $envDef) { throw "Resolve-AppConfig: Environment '$EnvironmentName' not found in config. Available environments: $($Config.environments.PSObject.Properties.Name -join ', ')" } # Use List[PSCustomObject] to avoid O(n^2) array concatenation. $resolved = [System.Collections.Generic.List[PSCustomObject]]::new() foreach ($appRef in $envDef.apps) { $appName = $appRef.name # Look up the catalog entry by name using PSObject.Properties for reliable # existence checking on ConvertFrom-Json PSCustomObject output. $catalogProp = $Config.apps.PSObject.Properties[$appName] if ($null -eq $catalogProp) { Write-EnvkLog -Level 'ERROR' -Message "Resolve-AppConfig: app '$appName' not found in catalog -- skipping" continue } $catalog = $catalogProp.Value # Arguments override rule: # - If the env app reference has an 'arguments' property (even if empty string): use it. # - Otherwise: use catalog.arguments if present, else '' (empty string). $argsProp = $appRef.PSObject.Properties['arguments'] if ($null -ne $argsProp) { $arguments = $argsProp.Value } elseif ($null -ne $catalog.arguments) { $arguments = $catalog.arguments } else { $arguments = '' } # SkipMaximize defaults to $false when omitted from the catalog entry. $skipMaximizeProp = $catalog.PSObject.Properties['skipMaximize'] $skipMaximize = if ($null -ne $skipMaximizeProp) { [bool]$skipMaximizeProp.Value } else { $false } # Priority defaults to 0 when omitted from the environment app reference. # Uses PSObject.Properties to distinguish absent property from present-zero. $priorityProp = $appRef.PSObject.Properties['priority'] $priority = if ($null -ne $priorityProp) { [int]$priorityProp.Value } else { 0 } $resolved.Add([PSCustomObject]@{ Name = $appName Path = $catalog.path ProcessName = $catalog.processName Arguments = $arguments SkipMaximize = $skipMaximize Priority = $priority }) } # Use the unary comma operator to force a PowerShell array return even when # $resolved contains a single element. Without this, PowerShell unwraps a # single-element ToArray() result from the pipeline, returning a bare # PSCustomObject instead of PSCustomObject[]. Callers that check .Count # against an integer require a true array return. # Note: the unary comma forces an Object[] wrapper; PSUseOutputTypeCorrectly # reports this as Information but the element type is always PSCustomObject. return , $resolved.ToArray() } |