Source/Classes/PSNode.ps1

[Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssignments', 'Name', Justification = 'False positive')]
param()
enum Construction { Undefined; Scalar; List; Dictionary; Object }

Class PSNode {
    static [int]$MaxDepth       # Set the MaxDepth in the Begin of your cmdlet: [PSNode]::MaxDepth = $MaxDepth
    $Key                        # The dictionary key or property name of the node
    $Index                      # This index of $this item
    [Int]$Depth
    [PSNode]$Parent
    hidden $Path
    hidden $PathName

    $Value
    [Type]$Type
    [Construction]$Construction
    [Construction]$Structure

    PSNode($Object) {
        if ($Object -is [PSNode]) { $this.Value = $Object.Value } else { $this.Value = $Object }
        if ($Null -ne $Object)    { $this.Type = $Object.GetType() }
        $this.Construction =
            if ($Object -is [Management.Automation.PSCustomObject]) { 'Object' }
        elseif ($Object -is [ComponentModel.Component])             { 'Object' }
        elseif ($Object -is [Collections.IDictionary])              { 'Dictionary' }
        elseif ($Object -is [Collections.ICollection])              { 'List' }
        else                                                            { 'Scalar' }
        $this.Structure = if ($this.Construction -le 'Dictionary') { $this.Construction } else { 'Dictionary' }
    }

    [Array]GetPath() {
        if ($Null -eq $this.Path) {
            if     ($Null -ne $this.Index) { $this.Path = $this.Parent.GetPath() + $this.Index }
            elseif ($Null -ne $this.Key)   { $this.Path = $this.Parent.GetPath() + $this.Key }
            else                           { $this.Path = @() }
        }
        return $this.Path
    }

    [String]GetPathName() {
        if ($Null -eq $this.PathName) {
            if ($Null -eq $this.Parent) {
                $this.PathName = ''
            }
            elseif ($Null -ne $this.Key) {
                $Name =
                    if     ($this.Key -is [ValueType])        { "$($this.Key)" }
                    elseif ($this.Key -isnot [String])        { "[$($this.Key.GetType())]'$($this.Key)'" }
                    elseif ($this.Key -Match '^[_,a-z]+\w*$') { "$($this.Key)" }
                    else                                      { "$($this.Key)'" }
                $this.PathName = "$($this.Parent.GetPathName()).$Name"
            }
            elseif ($Null -ne $this.Index) {
                $this.PathName = "$($this.Parent.GetPathName())[$($this.Index)]"
            }
            else { Write-Error 'Should not happen' }
        }
        return $this.PathName
    }

    [Bool]Contains($Name) {
        if ($this.Construction -eq 'Object') { return $Null -ne $this.Value.PSObject.Properties[$Name] }
        elseif ($this.Construction -in 'List', 'Dictionary') { return $this.Value.Contains($Name) }
        else { return $false }
    }

    [Object]Get($Name) {
        switch ($this.Construction) {
            Object     { return $this.Value.PSObject.Properties[$Name].Value }
            Dictionary { return $this.Value[$Name] }
            List       { return $this.Value[$Name] }
        }
        return [Management.Automation.Internal.AutomationNull]::Value
    }

    Set($Name, $Value) {
        switch ($this.Construction) {
            Object     { $this.Value.PSObject.Properties[$Name].Value = $Value } # Doesn't create new properties
            Dictionary { $this.Value[$Name] = $Value }
            List       { $this.Value[$Name] = $Value }
        }
    }

    [PSNode]GetItemNode($Key) {
        if ($this.Structure -eq 'Scalar') { Write-Error "Expected collection" }
        elseif ($this.Depth -ge [PSNode]::MaxDepth) {
            Write-Warning "$($this.GetPathName) reached the maximum depth of $([PSNode]::MaxDepth)."
        }
        elseif ($this.Structure -eq 'List') {
            $Node        = [PSNode]::new($this.Value[$Key])
            $Node.Index  = $Key
            $Node.Depth  = $this.Depth + 1
            $Node.Parent = $this
            return $Node
        }
        elseif ($this.Structure -eq 'Dictionary') {
            $Node        = [PSNode]::new($this.Get($Key))
            $Node.Key    = $Key
            $Node.Depth  = $this.Depth + 1
            $Node.Parent = $this
            return $Node
        }
        return $null
    }

    [PSNode[]]GetItemNodes() {
        $ItemNodes = [Collections.Generic.List[PSNode]]::new()
        if ($this.Structure -eq 'Scalar') { Write-Error "Expected collection" }
        elseif ($this.Depth -ge [PSNode]::MaxDepth) {
            Write-Warning "$($this.GetPathName) reached the maximum depth of $([PSNode]::MaxDepth)."
        }
        elseif ($this.Structure -eq 'List') {
            for ($i = 0; $i -lt $this.Value.Count; $i++) {
                $Node        = [PSNode]::new($this.Value[$i])
                $Node.Index  = $i
                $Node.Depth  = $this.Depth + 1
                $Node.Parent = $this
                $ItemNodes.Add($Node)
            }
        }
        elseif ($this.Structure -eq 'Dictionary') {
            if ($this.Construction -eq 'Object') { $Items = $this.Value.PSObject.Properties }
            else                                 { $Items = $this.Value.GetEnumerator() }
            $i = 0
            $Items.foreach{
                $Node        = [PSNode]::new($_.Value)
                $Node.Key    = $_.Name
                $Node.Depth  = $this.Depth + 1
                $Node.Parent = $this
                $ItemNodes.Add($Node)
            }
        }
        return $ItemNodes
    }

    [Int]get_Count() {
        switch ($this.Construction) {
            Object     { return @($this.Value.PSObject.Properties).Count }
            Dictionary { return $this.Value.get_Count() }
            List       { return $this.Value.get_Count() }
        }
        return 0
    }

    [Array]get_Keys() {
        switch ($this.Construction) {
            Object     { return $this.Value.PSObject.Properties.Name }
            Dictionary { return $this.Value.get_Keys() }
            List       { return 0..($this.Value.Length - 1) }
        }
        return [Management.Automation.Internal.AutomationNull]::Value
    }

    [Array]get_Values() {
        switch ($this.Construction) {
            Object     { return $this.Value.PSObject.Properties.Value }
            Dictionary { return $this.Value.get_Values() }
            List       { return $this.Value }
        }
        return [Management.Automation.Internal.AutomationNull]::Value
    }
}