Source/Public/Compare-ObjectGraph.ps1
<#
.SYNOPSIS Compare Object Graph .DESCRIPTION Deep compares two Object Graph and lists the differences between them. .PARAMETER InputObject The input object that will be compared with the reference object (see: [-Reference] parameter). > [!NOTE] > Multiple input object might be provided via the pipeline. > The common PowerShell behavior is to unroll any array (aka list) provided by the pipeline. > To avoid a list of (root) objects to unroll, use the **comma operator**: ,$InputObject | Compare-ObjectGraph $Reference. .PARAMETER Reference The reference that is used to compared with the input object (see: [-InputObject] parameter). .PARAMETER PrimaryKey If supplied, dictionaries (including PSCustomObject or Component Objects) in a list are matched based on the values of the `-PrimaryKey` supplied. .PARAMETER IsEqual If set, the cmdlet will return a boolean (`$true` or `$false`). As soon a Discrepancy is found, the cmdlet will immediately stop comparing further properties. .PARAMETER MatchCase Unless the `-MatchCase` switch is provided, string values are considered case insensitive. > [!NOTE] > Dictionary keys are compared based on the `$Reference`. > if the `$Reference` is an object (PSCustomObject or component object), the key or name comparison > is case insensitive otherwise the comparer supplied with the dictionary is used. .PARAMETER MatchType Unless the `-MatchType` switch is provided, a loosely (inclusive) comparison is done where the `$Reference` object is leading. Meaning `$Reference -eq $InputObject`: '1.0' -eq 1.0 # $false 1.0 -eq '1.0' # $true (also $false if the `-MatchType` is provided) .PARAMETER MatchOrder By default, items in a list and dictionary (including properties of an PSCustomObject or Component Object) are matched independent of the order. If the `-MatchOrder` switch is supplied the index of the concerned item (or property) is matched. > [!NOTE] > A `[HashTable]` type is unordered by design and therefore, regardless the `-MatchOrder` switch, the order > of the `[HashTable]` are always ignored. > [!NOTE] > Regardless of the `-MatchOrder` switch, indexed (defined by the [PrimaryKey] parameter) dictionaries (including PSCustomObject or Component Objects) in a list are matched independent of the order. .PARAMETER MaxDepth The maximal depth to recursively compare each embedded property (default: 10). #> function Compare-ObjectGraph { [CmdletBinding()] param( [Parameter(Mandatory=$true, ValueFromPipeLine = $true)] $InputObject, [Parameter(Mandatory=$true, Position=0)] $Reference, [String[]]$PrimaryKey, [Switch]$IsEqual, [Switch]$MatchCase, [Switch]$MatchType, [Switch]$MatchOrder, [Alias('Depth')][int]$MaxDepth = 10 ) begin { $ReferenceNode = [PSNode]::new($Reference) $ReferenceNode.MaxDepth = $MaxDepth function CompareObject([PSNode]$ReferenceNode, [PSNode]$ObjectNode, [Switch]$IsEqual = $IsEqual) { if ($MatchType) { if ($ObjectNode.Type -ne $ReferenceNode.Type) { if ($IsEqual) { return $false } [PSCustomObject]@{ Path = $ObjectNode.GetPathName() Discrepancy = 'Type' InputObject = $ObjectNode.Type Reference = $ReferenceNode.Type } } } if ($ObjectNode.Structure -ne $ReferenceNode.Structure) { if ($IsEqual) { return $false } [PSCustomObject]@{ Path = $ObjectNode.GetPathName() Discrepancy = 'Structure' InputObject = $ObjectNode.Structure Reference = $ReferenceNode.Structure } } elseif ($ObjectNode.Structure -eq 'Scalar') { $NotEqual = if ($MatchCase) { $ReferenceNode.Value -cne $ObjectNode.Value } else { $ReferenceNode.Value -cne $ObjectNode.Value } if ($NotEqual) { # $ReferenceNode dictates the type if ($IsEqual) { return $false } [PSCustomObject]@{ Path = $ObjectNode.GetPathName() Discrepancy = 'Value' InputObject = $ObjectNode.Value Reference = $ReferenceNode.Value } } } else { if ($ObjectNode.get_Count() -ne $ReferenceNode.get_Count()) { if ($IsEqual) { return $false } [PSCustomObject]@{ Path = $ObjectNode.GetPathName() Discrepancy = 'Size' InputObject = $ObjectNode.get_Count() Reference = $ReferenceNode.get_Count() } } if ($ObjectNode.Structure -eq 'List') { $ObjectItems = $ObjectNode.GetItemNodes() $ReferenceItems = $ReferenceNode.GetItemNodes() if ($ObjectItems.Count) { $ObjectIndices = [Collections.Generic.List[Int]]$ObjectItems.Index } else { $ObjectIndices = @() } if ($ReferenceItems.Count) { $ReferenceIndices = [Collections.Generic.List[Int]]$ReferenceItems.Index } else { $ReferenceIndices = @() } if ($PrimaryKey) { $ObjectDictionaries = [Collections.Generic.List[Int]]$ObjectItems.where{ $_.Structure -eq 'Dictionary' }.Index if ($ObjectDictionaries.Count) { $ReferenceDictionaries = [Collections.Generic.List[Int]]$ReferenceItems.where{ $_.Structure -eq 'Dictionary' }.Index if ($ReferenceDictionaries.Count) { foreach ($Key in $PrimaryKey) { foreach($ObjectIndex in @($ObjectDictionaries)) { $ObjectItem = $ObjectItems[$ObjectIndex] foreach ($ReferenceIndex in $ReferenceDictionaries) { $ReferenceItem = $ReferenceItems[$ReferenceIndex] if ($ReferenceItem.Get($Key) -eq $ObjectItem.Get($Key)) { if (CompareObject -Reference $ReferenceItem -Object $ObjectItem -IsEqual) { $null = $ObjectDictionaries.Remove($ObjectIndex) $Null = $ReferenceDictionaries.Remove($ReferenceIndex) $null = $ObjectIndices.Remove($ObjectIndex) $Null = $ReferenceIndices.Remove($ReferenceIndex) break # Only match a single node } } } } foreach ($Key in $PrimaryKey) { # in case of any single leftovers where the key value doesn't match if($ObjectDictionaries.Count -eq 1 -and $ReferenceDictionaries.Count -eq 1) { $ObjectItem = $ObjectItems[$ObjectDictionaries[0]] $ReferenceItem = $ReferenceItems[$ReferenceDictionaries[0]] $Compare = CompareObject -Reference $ReferenceItem -Object $ObjectItem -IsEqual:$IsEqual if ($Compare -eq $false) { return $Compare } elseif ($Compare -ne $true) { $Compare } $ObjectDictionaries.Clear() $ReferenceDictionaries.Clear() $null = $ObjectIndices.Remove($ObjectDictionaries[0]) $Null = $ReferenceIndices.Remove($ReferenceDictionaries[0]) } } } } } } foreach($ObjectIndex in @($ObjectIndices)) { $ObjectItem = $ObjectItems[$ObjectIndex] foreach ($ReferenceIndex in $ReferenceIndices) { $ReferenceItem = $ReferenceItems[$ReferenceIndex] if (CompareObject -Reference $ReferenceItem -Object $ObjectItem -IsEqual) { if ($MatchOrder -and $ObjectItem.Index -ne $ReferenceItem.Index) { if ($IsEqual) { return $false } [PSCustomObject]@{ Path = $ReferenceNode.GetPathName() Discrepancy = 'Index' InputObject = $ObjectItem.Index Reference = $ReferenceItem.Index } } $null = $ObjectIndices.Remove($ObjectIndex) $Null = $ReferenceIndices.Remove($ReferenceIndex) break # Only match a single node } } } for ($i = 0; $i -lt [math]::max($ObjectIndices.Count, $ReferenceIndices.Count); $i++) { $ObjectIndex = if ($i -lt $ObjectIndices.Count) { $ObjectIndices[$i] } $ReferenceIndex = if ($i -lt $ReferenceIndices.Count) { $ReferenceIndices[$i] } $ObjectItem = if ($Null -ne $ObjectIndex) { $ObjectItems[$ObjectIndex] } $ReferenceItem = if ($Null -ne $ReferenceIndex) { $ReferenceItems[$ReferenceIndex] } if ($Null -eq $ObjectItem) { # if ($IsEqual) { never happens as the size already differs [PSCustomObject]@{ Path = $ReferenceNode.GetPathName() + "[$ReferenceIndex]" Discrepancy = 'Value' InputObject = $Null Reference = if ($ReferenceItem -eq 'Scalar') { $ReferenceItem.Value } else { "[$($ReferenceItem.Type)]" } } } elseif ($Null -eq $ReferenceItem) { # if ($IsEqual) { never happens as the size already differs [PSCustomObject]@{ Path = $ObjectNode.GetPathName() + "[$ObjectIndex]" Discrepancy = 'Value' InputObject = if ($ObjectItem -eq 'Scalar') { $ObjectItem.Value } else { "[$($ObjectItem.Type)]" } Reference = $Null } } else { $Compare = CompareObject -Reference $ReferenceItem -Object $ObjectItem -IsEqual:$IsEqual if ($Compare -eq $false) { return $Compare } elseif ($Compare -ne $true) { $Compare } } } } elseif ($ObjectNode.Structure -eq 'Dictionary') { $Found = [HashTable]::new() # (Case sensitive) $Order = if ($MatchOrder -and $ReferenceNode.Type.Name -ne 'HashTable') { [HashTable]::new() } $Index = 0 if ($Order) { $ReferenceNode.Get_Keys().foreach{ $Order[$_] = $Index++ } } $Index = 0 foreach ($ObjectItem in $ObjectNode.GetItemNodes()) { if ($ReferenceNode.Contains($ObjectItem.Key)) { $ReferenceItem = $ReferenceNode.GetItemNode($ObjectItem.Key) $Found[$ReferenceItem.Key] = $true if ($Order -and $Order[$ReferenceItem.Key] -ne $Index) { if ($IsEqual) { return $false } [PSCustomObject]@{ Path = $ObjectItem.GetPathName() Discrepancy = 'Index' InputObject = $Index Reference = $Order[$ReferenceItem.Key] } } $Compare = CompareObject -Reference $ReferenceItem -Object $ObjectItem -IsEqual:$IsEqual if ($Compare -eq $false) { return $Compare } elseif ($Compare -ne $true) { $Compare } } else { if ($IsEqual) { return $false } [PSCustomObject]@{ Path = $ObjectItem.GetPathName() Discrepancy = 'Exists' InputObject = $true Reference = $false } } $Index++ } $ReferenceNode.get_Keys().foreach{ if (-not $Found.Contains($_)) { if ($IsEqual) { return $false } [PSCustomObject]@{ Path = $ReferenceNode.GetItemNode($_).GetPathName() Discrepancy = 'Exists' InputObject = $false Reference = $true } } } } } if ($IsEqual) { return $true } } } process { CompareObject $ReferenceNode $InputObject } } |