Private/Get-HDTStepProperty.ps1

function Get-HDTStepProperty {
    <#
        .SYNOPSIS
            Reads one property off a flattened step, expanded and coerced.
 
        .DESCRIPTION
            The step contract says a step is "a name, a type and a
            property bag". This is the one reader every HDT step type uses to
            get a value out of that bag, so "what does an absent property mean"
            has one answer rather than one per step type.
 
            Four things happen, in this order:
 
              1. the value is read by name, case-insensitively;
              2. absent, null or whitespace-only becomes -Default, UNCOERCED -
                 a default is what the author of the step type wrote, and it is
                 already the type they meant;
              3. -Expand runs Expand-HDTVariableToken against $Context.Variable,
                 because every property in the sample sequences is written
                 "%HDTOSImage%";
              4. -As coerces to String, Int, Long or Bool.
 
            AN UNRESOLVED TOKEN IS LEFT LITERAL (02-03's rule). A token that
            silently became '' is how a machine ends up named 'PC-'; a step that
            sees a literal '%HDTOSImage%' can say so by name.
 
            A VALUE THAT WILL NOT CONVERT IS A CONFIGURATION ERROR THAT NAMES
            THE STEP AND THE PROPERTY. An authoring mistake must read
 
              step 'Apply OS': index 'abc' is not a whole number.
 
            rather than 'Cannot convert value "abc" to type "System.Int32"'. The
            first sentence names the thing to edit; the second names a type
            system. It is thrown as HDTConfigurationError, which
            Get-HDTFailureClass classes as Configuration, so bad authoring is
            never retried.
 
            -As Bool PARSES RATHER THAN CASTS, and that is not pedantry:
            [bool] 'false' is $true in PowerShell, because every non-empty
            string is. A reader that cast would make 'setBootOrder: false' mean
            true, on a property whose whole purpose is to turn something off.
 
        .PARAMETER Step
            A flattened step from Import-HDTSequenceDocument. A step whose
            Property bag is $null is read as a step with no properties.
 
        .PARAMETER Name
            The property name, matched case-insensitively.
 
        .PARAMETER Default
            What an absent, null or whitespace-only property means. Returned as
            it is, without expansion or coercion.
 
        .PARAMETER Context
            A New-HDTExecutionContext context. Required by -Expand, which reads
            its Variable dictionary.
 
        .PARAMETER Expand
            Expand %Var% tokens in a string value.
 
        .PARAMETER As
            Coerce to String, Int, Long or Bool.
 
        .OUTPUTS
            System.Object - the value, expanded and coerced.
 
        .EXAMPLE
            Get-HDTStepProperty -Step $Step -Name 'os' -Context $Context -Expand
 
        .EXAMPLE
            Get-HDTStepProperty -Step $Step -Name 'minDiskGB' -Default 60 -As Long
    #>

    [CmdletBinding()]
    [OutputType([object])]
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [ValidateNotNull()]
        [object] $Step,

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

        [Parameter()]
        [AllowNull()]
        [object] $Default = $null,

        [Parameter()]
        [AllowNull()]
        [object] $Context,

        [Parameter()]
        [switch] $Expand,

        [Parameter()]
        [ValidateSet('String', 'Int', 'Long', 'Bool')]
        [string] $As
    )

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

    $property = $Step.Property

    $raw = $null
    if ($null -ne $property) {
        # The bag Import-HDTSequenceDocument builds is case-insensitive already,
        # but a hand-built one - a third party's flattener, or a test - may not
        # be, so the walk is done here rather than assumed of the dictionary.
        foreach ($key in @($property.Keys)) {
            if ([string] $key -eq $Name) {
                $raw = $property[$key]
                break
            }
        }
    }

    if ($null -eq $raw) {
        return $Default
    }

    if (($raw -is [string]) -and [string]::IsNullOrWhiteSpace($raw)) {
        return $Default
    }

    $value = $raw

    if ($Expand -and ($raw -is [string]) -and $null -ne $Context) {
        $value = Expand-HDTVariableToken -Value ([string] $raw) -Scope $Context.Variable
    }

    if ([string]::IsNullOrEmpty($As)) {
        return $value
    }

    $text = ([string] $value).Trim()

    if ($As -eq 'String') {
        return [string] $value
    }

    if ($As -eq 'Bool') {
        if ($value -is [bool]) {
            return [bool] $value
        }

        if (@('true', '1', 'yes') -contains $text.ToLowerInvariant()) {
            return $true
        }

        if (@('false', '0', 'no') -contains $text.ToLowerInvariant()) {
            return $false
        }

        $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -TargetObject $raw -Category InvalidData `
                    -Message ("step '{0}': {1} '{2}' is not true or false. A yes/no property takes true or false." -f
                        $Step.Name, $Name, $value)))
    }

    if ($As -eq 'Int') {
        $parsed = 0
        if ([int]::TryParse($text, [ref] $parsed)) {
            return [int] $parsed
        }

        $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -TargetObject $raw -Category InvalidData `
                    -Message ("step '{0}': {1} '{2}' is not a whole number." -f $Step.Name, $Name, $value)))
    }

    $parsedLong = [long] 0
    if ([long]::TryParse($text, [ref] $parsedLong)) {
        return [long] $parsedLong
    }

    $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -TargetObject $raw -Category InvalidData `
                -Message ("step '{0}': {1} '{2}' is not a whole number." -f $Step.Name, $Name, $value)))
}