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 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 MatchObjectOrder Whether a list (or array) is treated as ordered is defined by the `$Reference`. Unless the `-MatchObjectOrder` switch is provided, the order of an object array (`@(...)` aka `Object[]`) is presumed unordered. This means that `Compare-ObjectGraph` cmdlet will try to match each item of an `$InputObject` list which each item in the `$Reference` list. If there is a single discrepancy on each side, the properties will be compared deeper, otherwise a list with different items will be returned. .PARAMETER IgnoreArrayOrder Whether a list (or array) is treated as ordered is defined by the `$Reference`. Unless the `-IgnoreArrayOrder` switch is provided, the order of an array (e.g. `[String[]]('a', b', 'c')`, excluding an object array, see: [-MatchObjectOrder]), is presumed ordered. .PARAMETER IgnoreListOrder Whether a list is treated as ordered is defined by the `$Reference`. Unless the `-IgnoreListOrder` switch is provided, the order of a list (e.g. `[Collections.Generic.List[Int]](1, 2, 3)`), is presumed ordered. .PARAMETER IgnoreDictionaryOrder Whether a dictionary is treated as ordered is defined by the `$Reference`. Unless the `-IgnoreDictionaryOrder` switch is provided, the order of a dictionary is presumed ordered. > [!WARNING] > A `[HashTable]` type is unordered by design and therefore the order of a `$Reference` hash table > in always ignored .PARAMETER IgnorePropertyOrder Whether the properties are treated as ordered is defined by the `$Reference`. Unless the `-IgnorePropertyOrder` switch is provided, the property order is presumed ordered. .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, [Switch]$IsEqual, [Switch]$MatchCase, [Switch]$MatchType, [Switch]$MatchObjectOrder, [Switch]$IgnoreArrayOrder, [Switch]$IgnoreListOrder, [Switch]$IgnoreDictionaryOrder, [Switch]$IgnorePropertyOrder, [Alias('Depth')][int]$MaxDepth = 10 ) begin { [PSNode]::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() $MatchOrder = $ObjectNode.get_Count() -eq 0 -or $ReferenceNode.get_Count() -eq 0 -or ($ObjectNode.get_Count() -eq 1 -and $ReferenceNode.get_Count() -eq 1) -or $( if ($ReferenceNode.Type.Name -eq 'Object[]') { $MatchObjectOrder } elseif ($ReferenceNode.Value -eq [Array]) { -not $IgnoreArrayOrder } else { -not $IgnoreListOrder} ) if ($MatchOrder) { $Min = [Math]::Min($ObjectNode.get_Count(), $ReferenceNode.get_Count()) $Max = [Math]::Max($ObjectNode.get_Count(), $ReferenceNode.get_Count()) for ($Index = 0; $Index -lt $Max; $Index++) { if ($Index -lt $Min) { $Compare = CompareObject -Reference $ReferenceItems[$Index] -Object $ObjectItems[$Index] -IsEqual:$IsEqual if ($Compare -eq $false) { return $Compare } elseif ($Compare -ne $true) { $Compare } } elseif ($Index -ge $ObjectNode.get_Count()) { if ($IsEqual) { return $false } [PSCustomObject]@{ Path = $ReferenceItems[$Index].GetPathName() # ($ObjectItem doesn't exist) Discrepancy = 'Exists' InputObject = $false Reference = $true } } else { if ($IsEqual) { return $false } [PSCustomObject]@{ Path = $ObjectItems[$Index].GetPathName() Discrepancy = 'Exists' InputObject = $true Reference = $false } } } } else { $ObjectLinks = [system.collections.generic.dictionary[int,int]]::new() $ReferenceLinks = [system.collections.generic.dictionary[int,int]]::new() foreach($ObjectItem in $ObjectItems) { $Found = $Null foreach($ReferenceItem in $ReferenceItems) { if (-not $ReferenceLinks.ContainsKey($ReferenceItem.Index)) { $Found = CompareObject -Reference $ReferenceItem -Object $ObjectItem -IsEqual if ($Found) { $ReferenceLinks[$ReferenceItem.Index] = $ObjectItem.Index break # Link only one reference item } } } if ($Found) { $ObjectLinks[$ObjectItem.Index] = $ReferenceItem.Index } elseif ($IsEqual) { return $false } } $MissingObjects = $ObjectItems.get_Count() - $ObjectLinks.get_Count() $MissingReferences = $ReferenceItems.get_Count() - $ReferenceLinks.get_Count() $Equal = -not $MissingObjects -and -not $MissingReferences if ($IsEqual) { return $Equal } elseif ($Equal) { return } if ($MissingObjects -eq 1 -and $MissingReferences -eq 1) { $ObjectExcept = ([int[]][Linq.Enumerable]::Except([int[]]$ObjectItems.Index, [int[]]$ObjectLinks.get_Keys()))[0] $ReferenceExcept = ([int[]][Linq.Enumerable]::Except([int[]]$ReferenceItems.Index, [int[]]$ReferenceLinks.get_Keys()))[0] CompareObject -Reference $ReferenceItems[$ReferenceExcept] -Object $ObjectItems[$ObjectExcept] -IsEqual:$IsEqual } else { $Max = [Math]::Max($ObjectNode.get_Count(), $ReferenceNode.get_Count()) for ($Index = 0; $Index -lt $Max; $Index++) { if ($Index -ge $ObjectItems.get_Count()) { [PSCustomObject]@{ Path = $ReferenceNode.GetPathName() + "[$Index]" Discrepancy = 'Exists' InputObject = $false Reference = $true } } elseif ($Index -ge $ReferenceItems.get_Count()) { [PSCustomObject]@{ Path = $ObjectNode.GetPathName() + "[$Index]" Discrepancy = 'Exists' InputObject = $true Reference = $false } } elseif ($Index -notin $ObjectLinks.get_Keys()) { [PSCustomObject]@{ Path = $ReferenceNode.GetPathName() + "[$Index]" Discrepancy = 'Linked' InputObject = $false Reference = $ReferenceLinks[$Index] } } elseif ($Index -notin $ReferenceLinks.get_Keys()) { [PSCustomObject]@{ Path = $ObjectNode.GetPathName() + "[$Index]" Discrepancy = 'Linked' InputObject = $ObjectLinks[$Index] Reference = $false } } } } } } elseif ($ObjectNode.Structure -eq 'Dictionary') { $Found = [HashTable]::new() # (Case sensitive) $MatchOrder = $ReferenceNode.Type.Name -ne 'HashTable' -and $( if ($ReferenceNode.Construction -eq 'Object') { -not $IgnorePropertyOrder } else { -not $IgnoreDictionaryOrder } ) $Order = if ($MatchOrder) { [HashTable]::new() } $Index = 0 if ($MatchOrder) { $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 ($MatchOrder -and $Order[$ReferenceItem.Key] -ne $Index) { if ($IsEqual) { return $false } [PSCustomObject]@{ Path = $ObjectItem.GetPathName() Discrepancy = 'Order' 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 $Reference $InputObject } } |