Private/ConvertTo-NSPRecipeValue.ps1

$script:RecipeParameterTypes = @('String', 'Version', 'Bool', 'Json', 'Text')

function ConvertTo-NSPPSLiteral {
    # Single-quoted PowerShell string literal. Doubling ' is the only escape a single-quoted
    # string has; the "smart" single quotes PowerShell also treats as delimiters are doubled too.
    param([AllowEmptyString()][string]$Value)
    # Built with [char] codes, not literal characters, so this file stays ASCII.
    $quoteClass = '[' + [char]0x27 + [char]0x2018 + [char]0x2019 + [char]0x201A + [char]0x201B + ']'
    $escaped = [regex]::Replace($Value, $quoteClass, { param($m) $m.Value + $m.Value })
    return "'" + $escaped + "'"
}

function ConvertTo-NSPRecipeText {
    # Raw single-line text for a comment.
    param([AllowEmptyString()][string]$Value)
    if ($Value -match "[\r\n]") { throw "A Text value must be a single line: '$Value'." }
    if ($Value -match '#>|<#') { throw "A Text value may not contain '<#' or '#>': '$Value'." }
    return $Value
}

function ConvertTo-NSPRecipeValue {
    # Validates one recipe parameter value and returns @{ Raw; Rendered }.
    param(
        [Parameter(Mandatory)][object]$Parameter,
        [AllowNull()][object]$Value,
        [switch]$IsEmpty
    )

    $name = $Parameter.Name
    switch ($Parameter.Type) {
        'String' {
            $s = if ($IsEmpty) { '' } else { [string]$Value }
            if ($s -match "[\r\n]") { throw "Parameter '$name' must be a single line." }
            if ($Parameter.Pattern -and -not $IsEmpty -and $s -notmatch $Parameter.Pattern) {
                throw "Parameter '$name' value '$s' does not match $($Parameter.Pattern)."
            }
            return @{ Raw = $s; Rendered = (ConvertTo-NSPPSLiteral $s) }
        }
        'Version' {
            $s = if ($IsEmpty) { '' } else { [string]$Value }
            $parsed = $null
            if (-not $IsEmpty -and -not [version]::TryParse($s, [ref]$parsed)) { throw "Parameter '$name' value '$s' is not a version." }
            return @{ Raw = $s; Rendered = (ConvertTo-NSPPSLiteral $s) }
        }
        'Bool' {
            $b = if ($IsEmpty) { $false } elseif ($Value -is [string]) { $Value -match '^(?i)(true|1|yes|y)$' } else { [bool]$Value }
            return @{ Raw = [string]$b; Rendered = $(if ($b) { '$true' } else { '$false' }) }
        }
        'Json' {
            if ($IsEmpty) { return @{ Raw = ''; Rendered = '' } }
            if ($Value -is [string]) {
                try { $null = ConvertFrom-Json -InputObject $Value -ErrorAction Stop }
                catch { throw "Parameter '$name' is not valid JSON: $($_.Exception.Message)" }
                $json = $Value.Trim()
            } else {
                # -InputObject keeps a single-element array an array (piping would unroll it).
                $json = ConvertTo-Json -InputObject $Value -Depth 20
            }
            $json = ($json -replace "`r`n", "`n") -replace "`n", "`r`n"
            # A line starting with '@ would end the here-string early. ConvertTo-Json never emits
            # one (newlines inside strings are escaped), but a hand-written string could.
            if ($json -match "(?m)^'@") { throw "Parameter '$name' JSON has a line starting with '@, which would end the here-string it is embedded in." }
            return @{ Raw = $json; Rendered = $json }
        }
        'Text' {
            $s = if ($IsEmpty) { '' } else { [string]$Value }
            return @{ Raw = $s; Rendered = (ConvertTo-NSPRecipeText $s) }
        }
    }
}