Private/DataShape.ps1

# The Data Shape: what a Kind's Gatherer promises to hand its Judge.
#
# ADR-0002 leaves Gatherers untested, and that is acceptable while they decide nothing. It
# has a quieter way to fail than a Gatherer that judges: one that renames a property. The
# Judge asks for the old name with Get-DataProperty, is handed $null, and does what it was
# written to do with $null - says "not available" about a machine that answered. To a
# Technician that reads as an honest gap. It is a defect of Gutcheck. ADR-0011.
#
# So every Kind declares, as $script:<Kind>DataShape, the top-level properties of what its
# Gatherer returns, each as one of:
#
# 'Answered' present, one value, and answered on any working Windows machine
# 'Value' present, one value, which may be $null where Windows did not answer
# 'List' present, any number of values - none, one, or a list - or $null
# 'Optional' may be left out altogether, by a path of the Gatherer that returns early
#
# A Run holds the data to the shape at the Kind boundary, and says so in a Finding where a
# property is missing or a list stands where one value was promised. It does not say so for
# an 'Answered' property that is $null: that is how a Gatherer correctly reports that
# Windows did not answer, and the Judge already says it. The suite, on a machine it knows
# to work, asks for that too (-Strict).

function Get-DataShapeMismatch {
    <#
    .SYNOPSIS
        Where gathered data falls short of the Data Shape of its Kind. Pure.
    .DESCRIPTION
        One mismatch per property, naming it and its Shortfall: Missing (not on the data at
        all), List (a list where one value was promised) or, with -Strict, Unanswered (an
        'Answered' property that is $null). Properties the shape does not declare are not
        looked at: the Run stamps CheckName onto the data, and a Gatherer may carry more.
    #>

    [CmdletBinding()]
    [OutputType([psobject])]
    param(
        [Parameter(Mandatory)][hashtable]$Shape,
        [AllowNull()]$Data,
        [switch]$Strict
    )

    foreach ($name in @($Shape.Keys | Sort-Object)) {
        $declared = "$($Shape[$name])"
        if (-not (Test-DataPropertyPresent -InputObject $Data -Name $name)) {
            if ($declared -ne 'Optional') { New-DataShapeMismatch -Property $name -Shortfall Missing }
            continue
        }
        $value = Get-DataProperty -InputObject $Data -Name $name
        if ($declared -in @('Answered', 'Value') -and $value -is [Collections.IList]) {
            New-DataShapeMismatch -Property $name -Shortfall List
        }
        elseif ($Strict -and $declared -eq 'Answered' -and ($null -eq $value -or "$value" -eq '')) {
            New-DataShapeMismatch -Property $name -Shortfall Unanswered
        }
    }
}

function Test-DataPropertyPresent {
    [CmdletBinding()]
    [OutputType([bool])]
    param([AllowNull()]$InputObject, [Parameter(Mandatory)][string]$Name)

    if ($null -eq $InputObject) { return $false }
    if ($InputObject -is [Collections.IDictionary]) { return $InputObject.Contains($Name) }
    [bool]$InputObject.PSObject.Properties[$Name]
}

function New-DataShapeMismatch {
    [CmdletBinding()]
    [OutputType([psobject])]
    param(
        [Parameter(Mandatory)][string]$Property,
        [Parameter(Mandatory)][ValidateSet('Missing', 'List', 'Unanswered')][string]$Shortfall
    )
    [pscustomobject]@{
        PSTypeName = 'Gutcheck.DataShapeMismatch'
        Property   = $Property
        Shortfall  = $Shortfall
    }
}