Source/Public/Test-ObjectGraph.ps1

<#
.SYNOPSIS
    Tests the properties of an object-graph.
 
.DESCRIPTION
    Tests an object-graph against a schema object by verifying that the properties of the object-graph
    meet the constrains defined in the schema object.
#>


# JsonSchema Properties
# Schema properties: [Newtonsoft.Json.Schema.JsonSchema]::New() | Get-Member
# https://www.newtonsoft.com/json/help/html/Properties_T_Newtonsoft_Json_Schema_JsonSchema.htm
# Enum PSSchemaName {
# Title
# Type
# PrimaryKey # Should be red before items
# Items
# }
function Test-ObjectGraph {
    [CmdletBinding(HelpUri='https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/Test-ObjectGraph.md')][OutputType([String])] param(

        [Parameter(Mandatory = $true, ValueFromPipeLine = $True)]
        $InputObject,

        [Parameter(Mandatory = $true, Position = 0)]
        $SchemaObject,

        [Switch]$IsValid,

        [Switch]$ShowAll,

        [Alias('Depth')][int]$MaxDepth = [PSNode]::DefaultMaxDepth
    )



    begin {


        # $NodeTypeCache = [System.Collections.Generic.Dictionary[String,NodeType]]::new()

        # $DefinitionOrder = @('Title', 'Type')

        function TestObject([PSNode]$ObjectNode, [PSMapNode]$SchemaNode, [Switch]$IsValid) {
            $Verbose = $VerbosePreference -in 'Stop', 'Continue', 'Inquire'
            if ($Verbose) { Write-Verbose $ObjectNode.Path }
            if (-not $IsValid) { $Violations = [Collections.Generic.List[Object]]::new() }
            $DefinitionNodes = @{}
            foreach ($ChildNode in $SchemaNode.ChildNodes) { $DefinitionNodes[$ChildNode.Name] = $ChildNode }
            foreach ($Name in @('Title', 'Type', 'List', 'Required', 'Optional')) {
                if (-not $DefinitionNodes.Contains($Name)) { continue }
                $DefinitionNode = $DefinitionNodes[$Name]
                $Definition = $DefinitionNode.Value
                if ($Verbose) { Write-Verbose " Testing: $Name = $Definition" }
                $Expected, $Actual = $Null # Setting the $Actual will break the iteration
                $Checked = [Collections.Generic.HashSet[int]]::new()
                switch ($DefinitionNode.Name) {
                    Type {
                        $Type = $Definition -as [Type]
                        if (-not $Type) {
                            Throw "Schema error at $($SchemaNode.Path).Type: Unknown type [$Definition] in schema definition"
                        }
                        $Invalid = $ObjectNode.Value -isnot $Type
                        if ($IsValid -and $Invalid) { return $false }
                        if ($ShowAll -or $Invalid) { $Expected = "[$Definition] type" }
                        if ($Invalid) { $Actual = "$($ObjectNode.Value) of type [$($ObjectNode.ValueType)]" }
                    }
                    { $_ -in 'List', 'Required', 'Optional' } {
                        $Invalid = ($ObjectNode.NodeStructure -ne $DefinitionNode.NodeStructure)
                        if ($IsValid -and $Invalid) { return $false }
                        if ($ShowAll -or $Invalid) { $Expected = "$($DefinitionNode.NodeStructure) structure" }
                        if ($Invalid) { $Actual = "$($ObjectNode.Value) with $($ObjectNode.NodeStructure) structure" }
                    }
                    List {
                        if ($ObjectNode -isnot [PSCollectionNode]) { continue } # $ObjectNode and $DefinitionNode are already tested equal structures
                        if ($Definition -is [HashTable]) {
                            Throw "Schema error at $($SchemaNode.Path).List: Testing a map list requires an ordered dictionary or PSCustomObject."
                        }
                        if ($ObjectNode.Value -is [HashTable]) {
                            $Expected = "an ordered dictionary or PSCustomObject"
                            $Actual = "'$($ObjectNode.Type)'"
                            break
                        }
                        $Index = 0
                        $DefinitionNodes = $DefinitionNode.ChildNodes
                        foreach ($ChildNode in $ObjectNode.ChildNodes) {
                            $DefinitionNode = $DefinitionNodes[$Index++]
                            $Invalid = $ObjectNode -is [PSMapNode] -and $ObjectNode.Name -ne $DefinitionNode.Name
                            if ($IsValid -and $Invalid) { return $false }
                            if ($ShowAll -or $Invalid) { $Expected = "node #$Index named '$($DefinitionNode.Name)'" }
                            if ($Invalid) { $Actual = "'$($ObjectNode.Name)'" }
                            else {
                                $TestObject = TestObject $ChildNode $DefinitionNode $IsValid
                                if ($null -ne $TestObject) { $null = $Checked.add($Index) }
                                else { if ($IsValid) { Return $false } else { $Violations.AddRange($TestObject) } }
                            }
                        }
                    }
                    Required {
                        if ($ObjectNode -is [PSListNode]) { # $ObjectNode and $DefinitionNode are already tested equal structures
                            foreach ($RequiredNode in $DefinitionNode.ChildNodes) {
                                $Index = 0
                                $TestObject = $null
                                foreach ($ChildNode in $ObjectNode.ChildNodes) {
                                    if ($ChildNode -notin $Checked) {
                                        $TestObject = TestObject $ChildNode $RequiredNode -IsValid
                                        if ($TestObject) { $null = $Checked.add($Index) }
                                    }
                                    $Index++
                                }
                                $Invalid = $null -eq $TestObject
                                if ($IsValid -and $Invalid) { return $false }
                                if ($ShowAll -or $Invalid) { $Expected = $($RequiredNode.Value) }
                                if ($Invalid) { $Actual = "$($RequiredNode.Name) = $($RequiredNode.Value) doesn't exist" }
                            }
                        }
                        elseif ($ObjectNode -is [PSMapNode]) {
                            $Index = 0
                            foreach ($RequiredNode in $DefinitionNode.ChildNodes) {
                                $KeyExists = $ObjectNode.Contains($RequiredNode.Name)
                                if ($KeyExists) {
                                    $null = $Checked.add($Index)
                                    $ChildNode = $ObjectNode.GetChildNode($RequiredNode.Name)
                                    $TestObject = TestObject $ChildNode $RequiredNode $IsValid
                                    if ($null -ne $TestObject) { if ($IsValid) { Return $false } else { $Violations.AddRange(@($TestObject)) } }
                                }
                                $Invalid = -Not $KeyExists
                                if ($IsValid -and $Invalid) { return $false }
                                if ($ShowAll -or $Invalid) { $Expected = "containing '$($RequiredNode.Name)'" }
                                if ($Invalid) { $Actual = "'$($RequiredNode.Name)' doesn't exist" }
                                $Index++
                            }
                        }
                    }
                    Optional {
                        if ($ObjectNode -is [PSListNode]) { # $ObjectNode and $DefinitionNode are already tested equal structures
                            foreach ($OptionalNode in $DefinitionNode.ChildNodes) {
                                $Index = 0
                                foreach ($ChildNode in $ObjectNode.ChildNodes) {
                                    if ($ChildNode -notin $Checked) {
                                        $TestObject = TestObject $ChildNode $OptionalNode -IsValid
                                        if ($TestObject) { $Checked.add($Index) }
                                    }
                                    $Index++
                                }
                            }
                        }
                        elseif ($ObjectNode -is [PSMapNode]) {
                            $Index = 0
                            foreach ($OptionalNode in $DefinitionNode.ChildNodes) {
                                if ($ObjectNode.Contains($OptionalNode.Name)) {
                                    $null = $Checked.add($Index)
                                    $ChildNode = $ObjectNode.GetChildNode($RequiredNode.Name)
                                    $TestObject = TestObject $ChildNode $OptionalNode $IsValid
                                    if ($null -ne $TestObject) { if ($IsValid) { Return $false } else { $Violations.AddRange(@($TestObject)) } }
                                }
                                $Index++
                            }
                        }
                        $Invalid = $Checked.Count -lt $ObjectNode.Count
                        if ($IsValid -and $Invalid) { return $false }
                        if ($ShowAll -or $Invalid) { $Expected = "optional node" }
                        if ($Invalid) {
                            $Index = 0
                            $RedundantNames = foreach ($Node in $ObjectNode.ChildNodes) {
                                if ($Index++ -in $Checked) { continue }
                                $Node.Name
                            } -Join ', '
                            $Actual = "the following nodes are not optional: $RedundantNames"
                        }
                    }
                }
                if ($Expected) {
                    Write-Host 123
                    $Violations.Add(
                        [PSCustomObject]@{
                            Path     = $ObjectNode.Path
                            Value    = "$($ObjectNode.Value)"
                            Expected = $Expected
                            Actual   = $Actual
                        }
                    )
                    return $Violations
                }
            }
            if ($Violations) { $Violations }
        }

        $SchemaNode = [PSNode]::ParseInput($SchemaObject)
    }

    process {
        $ObjectNode = [PSNode]::ParseInput($InputObject, $MaxDepth)
        TestObject $ObjectNode $SchemaNode $IsValid
    }
}