Private/Add-HDTResolvedVariable.ps1

function Add-HDTResolvedVariable {
    <#
        .SYNOPSIS
            Assigns a variable if nothing has resolved it yet, and records where
            the value came from.
 
        .DESCRIPTION
            The single writer of a resolution result, and the place the
            precedence is actually enforced.
 
            The precedence is not a comparison anywhere in the engine.
            Resolve-HDTVariable applies the five sources in order and this
            function refuses to overwrite a variable that is already resolved.
            First writer wins, so applying the sources in precedence order IS
            the precedence, and a later fallback rule can only fill what nothing
            above it set. One rule, held in one place, rather than a priority
            comparison repeated at five call sites.
 
            Two values are stored, deliberately:
 
              the SCOPE receives the RAW value, unexpanded. That is what makes a
              cycle detectable at all - two variables that reference each other
              only look cyclic before expansion - and it is what `when` matching
              and later %Var% expansion read;
 
              the RESULT receives the EXPANDED value, which is what a step, a
              condition and an unattend.xml will actually use.
 
            The provenance record carries both, plus the source, the rule and its
            index, the file, and a 1-based Order, so the rule "every variable
            resolution records which source set it" survives the call rather than
            being a log line that scrolled past.
 
        .PARAMETER Resolution
            The result object being built: Variable and Provenance as ordered,
            case-insensitive dictionaries and Unresolved as an ArrayList.
 
        .PARAMETER Scope
            Name -> RAW value, the dictionary %Var% expansion and `when` matching
            read.
 
        .PARAMETER Name
            The variable to assign. A name starting with an underscore is
            refused: _HDT* is engine-owned. Assert-HDTRuleDocument
            holds that rule for rules.yaml, but the command line, a machine
            override and a setFrom script never pass through that validator, so
            the single writer holds it too.
 
        .PARAMETER Value
            The raw value. A string is expanded; an array is expanded element by
            element; a boolean, a number and anything else pass through untouched.
 
        .PARAMETER Source
            Which of the five sources supplied the value. A closed set:
            CommandLine, MachineOverride, Rule, RuleScript, GatheredFact,
            SequenceDefault. Closed because provenance is machine-readable - the
            console and ConvertTo-HDTReport switch on it.
 
        .PARAMETER Rule
            The rule name, for a rule or rule-script source.
 
        .PARAMETER RuleIndex
            The 1-based rule index. 0 where no rule was involved.
 
        .PARAMETER File
            The file the value came from: rules.yaml, the machine override, or the
            setFrom script. $null for the command line, facts and defaults, which
            have no file.
 
        .OUTPUTS
            System.Boolean. $true when it assigned, $false when the variable was
            already resolved.
 
        .EXAMPLE
            Add-HDTResolvedVariable -Resolution $resolution -Scope $scope `
                -Name 'HDTComputerName' -Value 'LT-%HDTSerialNumber%' `
                -Source 'Rule' -Rule 'Latitude naming' -RuleIndex 2 -File $document.Path
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '',
        Justification = 'Adds to an in-memory result object; it changes nothing outside the caller.')]
    [CmdletBinding()]
    [OutputType([bool])]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [object] $Resolution,

        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [System.Collections.IDictionary] $Scope,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string] $Name,

        [Parameter(Mandatory = $true)]
        [AllowNull()]
        [AllowEmptyString()]
        [AllowEmptyCollection()]
        [object] $Value,

        [Parameter(Mandatory = $true)]
        # Wizard IS ITS OWN SOURCE, NOT CommandLine WEARING A DISGUISE. They
        # arrive at the same precedence and beat the same things, but they
        # answer different questions afterwards: "somebody typed this at the
        # bench" and "the media was launched with this on its command line" are
        # different explanations for a machine's name, and DESIGN 3.1 exists so
        # provenance can tell them apart.
        [ValidateSet('CommandLine', 'Wizard', 'MachineOverride', 'Rule', 'RuleScript', 'GatheredFact', 'SequenceDefault')]
        [string] $Source,

        [Parameter()]
        [string] $Rule,

        [Parameter()]
        [int] $RuleIndex = 0,

        [Parameter()]
        [string] $File
    )

    Set-StrictMode -Version Latest
    $ErrorActionPreference = 'Stop'

    if ($Name.StartsWith('_')) {
        $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $File `
                    -Message ("'{0}' is engine-owned and cannot be assigned. A variable named _HDT* is set by the engine and is read-only." -f $Name)))
    }

    # First writer wins. Checked BEFORE expansion, so a value that was never
    # going to be used cannot report an unresolved token it never had to resolve.
    if ($Resolution.Provenance.Contains($Name)) {
        return $false
    }

    $expanded = $Value
    $changed = $false

    if ($Value -is [string]) {
        $expanded = Expand-HDTVariableToken -Value $Value -Scope $Scope `
            -Unresolved $Resolution.Unresolved -Chain @($Name) -Path $File
        $changed = ($expanded -cne $Value)
    } elseif ($Value -is [System.Collections.IList]) {
        $element = New-Object -TypeName System.Collections.ArrayList

        foreach ($item in @($Value)) {
            if ($item -is [string]) {
                $text = Expand-HDTVariableToken -Value $item -Scope $Scope `
                    -Unresolved $Resolution.Unresolved -Chain @($Name) -Path $File

                if ($text -cne $item) {
                    $changed = $true
                }

                [void] $element.Add($text)
            } else {
                [void] $element.Add($item)
            }
        }

        $expanded = @($element)
    }

    # The scope keeps the RAW value; the result keeps the expanded one.
    $Scope[$Name] = $Value
    $Resolution.Variable[$Name] = $expanded

    $Resolution.Provenance[$Name] = [pscustomobject] ([ordered] @{
            Name      = $Name
            Value     = $expanded
            Source    = $Source
            Rule      = $(if ([string]::IsNullOrEmpty($Rule)) { $null } else { $Rule })
            RuleIndex = $RuleIndex
            File      = $(if ([string]::IsNullOrEmpty($File)) { $null } else { $File })
            RawValue  = $Value
            Expanded  = $changed
            Order     = $Resolution.Provenance.Count + 1
        })

    return $true
}