Private/PlanAccess.ps1

<#
    Accès sûrs aux structures de plan.

    Set-StrictMode -Version Latest fait échouer la lecture d'une clé absente
    d'une hashtable ou d'une propriété absente d'un objet. Ces helpers rendent
    l'accès tolérant tout en gardant le mode strict actif pour le reste du code
    (où il attrape de vraies fautes de frappe).
#>


function Get-SPMPlanValue {
    <#
    .SYNOPSIS
        Lit une clé d'une hashtable ou d'un objet, ou $null si absente.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Position = 0)][AllowNull()]$Container,
        [Parameter(Mandatory, Position = 1)][string]$Key
    )

    if ($null -eq $Container) { return $null }

    if ($Container -is [System.Collections.IDictionary]) {
        if ($Container.Contains($Key)) { return $Container[$Key] }

        # Comparaison insensible à la casse : YAML et JSON n'ont pas les mêmes usages
        foreach ($k in $Container.Keys) {
            if ("$k" -ieq $Key) { return $Container[$k] }
        }
        return $null
    }

    $property = $Container.PSObject.Properties[$Key]
    if ($property) { return $property.Value }
    return $null
}

function Merge-SPMPlanSection {
    <#
    .SYNOPSIS
        Fusionne une section fournie par l'utilisateur sur des valeurs par défaut.
    #>

    [CmdletBinding()]
    [OutputType([hashtable])]
    param(
        [Parameter(Mandatory)][hashtable]$Defaults,
        [AllowNull()]$Provided
    )

    $merged = @{}
    foreach ($k in $Defaults.Keys) { $merged[$k] = $Defaults[$k] }

    if ($null -ne $Provided) {
        if ($Provided -is [System.Collections.IDictionary]) {
            foreach ($k in $Provided.Keys) { $merged[$k] = $Provided[$k] }
        }
        else {
            foreach ($p in $Provided.PSObject.Properties) { $merged[$p.Name] = $p.Value }
        }
    }

    return $merged
}

function Resolve-SPMPlanVariable {
    <#
    .SYNOPSIS
        Remplace récursivement ${NOM} par la variable d'environnement correspondante.

    .DESCRIPTION
        Permet de versionner un plan sans y inscrire d'identifiant ni de secret.
        Une variable référencée mais absente est une erreur explicite, jamais une
        chaîne vide qui échouerait plus loin de façon obscure.
    #>

    [CmdletBinding()]
    param([Parameter(Position = 0)][AllowNull()]$Value)

    if ($Value -is [string]) {
        return [regex]::Replace($Value, '\$\{(?<name>[A-Za-z_][A-Za-z0-9_]*)\}', {
                param($m)
                $name = $m.Groups['name'].Value
                $envValue = [System.Environment]::GetEnvironmentVariable($name)
                if ([string]::IsNullOrEmpty($envValue)) {
                    throw "Le plan référence `${$name} mais la variable d'environnement $name n'est pas définie."
                }
                $envValue
            })
    }

    if ($Value -is [System.Collections.IDictionary]) {
        $copy = @{}
        foreach ($k in $Value.Keys) { $copy[$k] = Resolve-SPMPlanVariable -Value $Value[$k] }
        return $copy
    }

    if ($Value -is [System.Collections.IList] -and $Value -isnot [string]) {
        return @($Value | ForEach-Object { Resolve-SPMPlanVariable -Value $_ })
    }

    return $Value
}