Source/Classes/PSNode.ps1

<#
.SYNOPSIS
    PowerShell Object Node Class
 
.DESCRIPTION
    This class provides general properties and method to recursively
    iterate through to PowerShell Object Graph nodes.
 
## Usage
 
To create a root node, you might simply construct a `[PSNode]` instance from the root object:
 
```PowerShell
[PSNode]$MyObject
```
 
To ensure that the recursive properties and method are being up to date with the object depth,
it is imperative that any child node is created from the parent node when passed to any recursive
function using the `GetItemNodes()` or `GetItemNode(<index/key>)` methods:
 
```PowerShell
function MyRecursiveFunction([PSNode]$Node) {
    Write-Host $Node.GetPathName() '=' $Node.Value
    foreach ($ChildNode in $Node.GetItemNodes()) {
        MyRecursiveFunction($ChildNode)
    }
}
MyRecursiveFunction $MyObject
```
 
## properties
 
### `MaxDepth`
 
Defines the class wide (static) maximum node depth of the object.
If the maximum depth has been reached, a error will be thrown.
 
### `Depth`
 
The current depth of the node.
 
### `Index`
 
The item node index relative to parent list or array. (ReadOnly, do not set)
 
### `Key`
 
The item node key or property relative to parent dictionary or PowerShell object.
 
#>


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

Class PSNode {
    static [int]$DefaultMaxDepth = 10
    [Int]$MaxDepth = [PSNode]::DefaultMaxDepth
    $Key                                    # The dictionary key or property name of the node
    $Index                                  # This index of $this item
    [Int]$Depth
    [PSNode]$Parent
    [PSNode]$Root = $this
    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 $this.Root.MaxDepth) {
            Write-Warning "$($this.GetPathName) reached the maximum depth of $($this.Root.MaxDepth)."
        }
        elseif ($this.Structure -eq 'List') {
            $Node        = [PSNode]::new($this.Value[$Key])
            $Node.Index  = $Key
            $Node.Depth  = $this.Depth + 1
            $Node.Parent = $this
            $Node.Root   = $this.Root
            return $Node
        }
        elseif ($this.Structure -eq 'Dictionary') {
            $Node        = [PSNode]::new($this.Get($Key))
            $Node.Key    = $Key
            $Node.Depth  = $this.Depth + 1
            $Node.Parent = $this
            $Node.Root   = $this.Root
            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 $this.MaxDepth) {
            Write-Warning "$($this.Root.GetPathName) reached the maximum depth of $($this.Root.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
                $Node.Root   = $this.Root
                $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
                $Node.Root   = $this.Root
                $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
    }
}