public/maester/drift/Compare-MtJsonObject.ps1
|
class MtPropertyDifference { [string]$PropertyName [object]$ExpectedValue [object]$ActualValue [string]$Description [string]$Reason MtPropertyDifference([string]$PropertyName, [object]$ExpectedValue, [object]$ActualValue, [string]$Description, [string]$Reason) { $this.PropertyName = $PropertyName $this.ExpectedValue = $ExpectedValue $this.ActualValue = $ActualValue $this.Description = $Description $this.Reason = $Reason } } <# .SYNOPSIS Compares two PowerShell objects (typically JSON objects) and returns a list of differences. .DESCRIPTION The Compare-MtJsonObject function recursively compares two objects (such as those imported from JSON) and returns an array of differences. It supports comparison of nested objects, arrays, and allows exclusion of specific properties via the Settings parameter. The function is useful for configuration drift detection, regression testing, or validating changes between baseline and current states. .PARAMETER Baseline The reference object to compare against (e.g., the expected or original state). .PARAMETER Current The object to compare to the baseline (e.g., the actual or new state). .PARAMETER Path (Optional) The property path being compared. Used internally for recursion and reporting. .PARAMETER Settings (Optional) An object that may contain an ExcludeProperties property (array of property names to skip). .OUTPUTS [MtPropertyDifference[]] Returns an array of objects describing each difference found. .EXAMPLE # Compare two JSON files and output the differences $baseline = Get-Content -Raw -Path 'baseline.json' | ConvertFrom-Json $current = Get-Content -Raw -Path 'current.json' | ConvertFrom-Json $diffs = Compare-MtJsonObject -Baseline $baseline -Current $current $diffs | Format-Table .EXAMPLE # Exclude specific properties from comparison $settings = [PSCustomObject]@{ ExcludeProperties = @('timestamp', 'lastModified') } $diffs = Compare-MtJsonObject -Baseline $baseline -Current $current -Settings $settings .NOTES Author: Stephan van Rooij @svrooij Date: 2025-06-26 .LINK https://maester.dev/docs/commands/Compare-MtJsonObject #> function Compare-MtJsonObject { [OutputType([MtPropertyDifference[]])] param ( [Parameter(Mandatory = $true)] [object]$Baseline, [Parameter(Mandatory = $true)] [object]$Current, [Parameter(Mandatory = $false)] [string]$Path = "", [Parameter(Mandatory = $false)] [object]$Settings = $null ) $differences = @() # Extract ExcludeProperties from settings if present $excludeProperties = @() if ($Settings -and $Settings.PSObject.Properties.Match('ExcludeProperties')) { $excludeProperties = $Settings.ExcludeProperties } if ($null -eq $Baseline) { $differences += [MtPropertyDifference]::new($Path, $null, $Current, "Baseline is null at path: $Path", "NullBaseline") return $differences } if ($null -eq $Current) { $differences += [MtPropertyDifference]::new($Path, $Baseline, $null, "Current is null at path: $Path", "NullCurrent") return $differences } if (-not ($Baseline -is [System.Collections.IDictionary] -or $Baseline -is [PSCustomObject])) { return $differences } $properties = if ($Baseline -is [System.Collections.IDictionary]) { $Baseline.Keys } else { $Baseline.PSObject.Properties.Name } foreach ($property in $properties) { if ($excludeProperties -contains $property) { continue } # Skip excluded properties $currentPath = if ([string]::IsNullOrEmpty($Path)) { $property } else { "$Path.$property" } if (($Current -is [System.Collections.IDictionary] -and -not $Current.ContainsKey($property)) -or ($Current -is [PSCustomObject] -and -not $Current.PSObject.Properties.Name.Contains($property))) { $expected = if ($Baseline -is [System.Collections.IDictionary]) { $Baseline[$property] } else { $Baseline.$property } $differences += [MtPropertyDifference]::new($currentPath, $expected, $null, "Property not found: $currentPath", "MissingProperty") continue } $baselineValue = if ($Baseline -is [System.Collections.IDictionary]) { $Baseline[$property] } else { $Baseline.$property } $currentValue = if ($Current -is [System.Collections.IDictionary]) { $Current[$property] } else { $Current.$property } if ($null -eq $baselineValue -and $null -eq $currentValue) { Write-Verbose "Both baseline and current values are null for property: $currentPath" } elseif ($null -eq $baselineValue -or $null -eq $currentValue) { $differences += [MtPropertyDifference]::new($currentPath, $baselineValue, $currentValue, "One of the values is null at path: $currentPath", "NullValue") } elseif (($baselineValue -is [System.Collections.IDictionary] -or $baselineValue -is [PSCustomObject]) -and ($currentValue -is [System.Collections.IDictionary] -or $currentValue -is [PSCustomObject])) { $differences += Compare-MtJsonObject -Baseline $baselineValue -Current $currentValue -Path $currentPath -Settings $Settings -ErrorAction SilentlyContinue } elseif ($baselineValue -is [Array] -and $currentValue -is [Array]) { if ($baselineValue.Count -ne $currentValue.Count) { $differences += [MtPropertyDifference]::new($currentPath, $baselineValue.Count, $currentValue.Count, "Array size mismatch at $($currentPath)", "ArraySizeMismatch") } else { for ($i = 0; $i -lt $baselineValue.Count; $i++) { $itemPath = "$currentPath[$i]" if (($baselineValue[$i] -is [System.Collections.IDictionary] -or $baselineValue[$i] -is [PSCustomObject]) -and ($currentValue[$i] -is [System.Collections.IDictionary] -or $currentValue[$i] -is [PSCustomObject])) { $differences += Compare-MtJsonObject -Baseline $baselineValue[$i] -Current $currentValue[$i] -Path $itemPath -Settings $Settings -ErrorAction SilentlyContinue } elseif ($baselineValue[$i] -ne $currentValue[$i]) { $differences += [MtPropertyDifference]::new($itemPath, $baselineValue[$i], $currentValue[$i], "Value mismatch at $($itemPath)", "ValueMismatch") } } } } elseif ($baselineValue -ne $currentValue) { $differences += [MtPropertyDifference]::new($currentPath, $baselineValue, $currentValue, "Value mismatch at $($currentPath)", "ValueMismatch") } } return $differences } |