Private/Compare-TLValue.ps1
|
function Compare-TLValue { <# .SYNOPSIS Structural diff of two normalized data trees. .DESCRIPTION Emits change records with the exact property path. Arrays of objects are matched by key (id/domain/skuPartNumber) so items are reported as Added/Removed while matched items are compared property by property. #> [CmdletBinding()] param( [AllowNull()] [object]$Reference, [AllowNull()] [object]$Difference, [string]$Path = '', [AllowNull()] [string]$Label = $null ) if ($null -eq $Reference -and $null -eq $Difference) { return } function Format-TLValueText { param([AllowNull()][object]$Value) if ($null -eq $Value) { return $null } if ($Value -is [string]) { return $Value } if ($Value -is [System.Collections.IDictionary]) { return (ConvertTo-Json -InputObject $Value -Compress -Depth 15) } if ($Value -is [System.Collections.IEnumerable]) { $items = @($Value) if ($items.Count -eq 0) { return $null } # empty array == absent return (ConvertTo-Json -InputObject $items -Compress -Depth 15) } return [string]$Value } function Get-TLObjectLabel { param([object]$Item, [string]$Fallback) if ($Item -is [System.Collections.IDictionary]) { foreach ($key in @('displayName', 'domain', 'name', '_tlPrincipalName', 'userPrincipalName', 'skuPartNumber', 'id')) { if ($Item.Contains($key) -and $Item[$key]) { return [string]$Item[$key] } } } return $Fallback } # --- dictionaries: recurse over the union of keys ------------------------- if ($Reference -is [System.Collections.IDictionary] -and $Difference -is [System.Collections.IDictionary]) { $keys = @(@($Reference.Keys) + @($Difference.Keys) | ForEach-Object { [string]$_ } | Sort-Object -Unique) foreach ($key in $keys) { $referenceValue = $(if ($Reference.Contains($key)) { $Reference[$key] } else { $null }) $differenceValue = $(if ($Difference.Contains($key)) { $Difference[$key] } else { $null }) $childPath = $(if ($Path) { "$Path.$key" } else { $key }) Compare-TLValue -Reference $referenceValue -Difference $differenceValue -Path $childPath -Label $Label } return } # --- arrays --------------------------------------------------------------- $referenceIsArray = ($Reference -is [System.Collections.IEnumerable]) -and ($Reference -isnot [string]) -and ($Reference -isnot [System.Collections.IDictionary]) $differenceIsArray = ($Difference -is [System.Collections.IEnumerable]) -and ($Difference -isnot [string]) -and ($Difference -isnot [System.Collections.IDictionary]) if ($referenceIsArray -and $differenceIsArray) { $referenceItems = @($Reference) $differenceItems = @($Difference) $probe = @($referenceItems + $differenceItems) | Where-Object { $null -ne $_ } | Select-Object -First 1 $keyName = $null if ($probe -is [System.Collections.IDictionary]) { foreach ($candidate in @('id', 'domain', 'skuPartNumber')) { if ($probe.Contains($candidate)) { $keyName = $candidate; break } } } if ($keyName) { $referenceById = [ordered]@{} foreach ($item in $referenceItems) { $referenceById[[string]$item[$keyName]] = $item } $differenceById = [ordered]@{} foreach ($item in $differenceItems) { $differenceById[[string]$item[$keyName]] = $item } foreach ($itemKey in $referenceById.Keys) { if (-not $differenceById.Contains($itemKey)) { [pscustomobject]@{ ChangeType = 'Removed' Path = $Path Label = Get-TLObjectLabel -Item $referenceById[$itemKey] -Fallback $itemKey ObjectId = $itemKey Before = Get-TLObjectLabel -Item $referenceById[$itemKey] -Fallback $itemKey After = $null } } } foreach ($itemKey in $differenceById.Keys) { if (-not $referenceById.Contains($itemKey)) { [pscustomobject]@{ ChangeType = 'Added' Path = $Path Label = Get-TLObjectLabel -Item $differenceById[$itemKey] -Fallback $itemKey ObjectId = $itemKey Before = $null After = Get-TLObjectLabel -Item $differenceById[$itemKey] -Fallback $itemKey } } } foreach ($itemKey in $referenceById.Keys) { if ($differenceById.Contains($itemKey)) { $itemLabel = Get-TLObjectLabel -Item $differenceById[$itemKey] -Fallback $itemKey Compare-TLValue -Reference $referenceById[$itemKey] -Difference $differenceById[$itemKey] -Path $Path -Label $itemLabel } } return } # scalar or unkeyed arrays: compare as a whole $referenceText = Format-TLValueText -Value $referenceItems $differenceText = Format-TLValueText -Value $differenceItems if ($referenceText -cne $differenceText) { [pscustomobject]@{ ChangeType = 'Changed' Path = $Path Label = $Label ObjectId = $null Before = $referenceText After = $differenceText } } return } # --- scalars / mixed types ------------------------------------------------ $referenceText = Format-TLValueText -Value $Reference $differenceText = Format-TLValueText -Value $Difference if ($referenceText -cne $differenceText) { [pscustomobject]@{ ChangeType = 'Changed' Path = $Path Label = $Label ObjectId = $null Before = $referenceText After = $differenceText } } } |