Private/Format-JIMConfigurationDiff.ps1

# Copyright (c) Tetron Limited. All rights reserved.
# Licensed under the Tetron Commercial License. See LICENSE file in the project root.

function Get-JIMDiffColour {
    # Returns ANSI colour codes via $PSStyle (PowerShell 7.2+), or empty strings when $PSStyle is
    # unavailable or colour is disabled, so the rendered diff stays readable when captured or redirected.
    if ($PSStyle) {
        return [pscustomobject]@{
            Green = $PSStyle.Foreground.Green
            Red   = $PSStyle.Foreground.Red
            Amber = $PSStyle.Foreground.Yellow
            Dim   = $PSStyle.Foreground.BrightBlack
            Reset = $PSStyle.Reset
        }
    }

    return [pscustomobject]@{ Green = ''; Red = ''; Amber = ''; Dim = ''; Reset = '' }
}

function Format-JIMConfigurationDiffNode {
    # Recursively renders one node of a configuration diff tree as git-style coloured lines.
    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory)]
        [PSObject]$Node,

        [int]$Depth = 0,

        [Parameter(Mandatory)]
        [PSObject]$Style
    )

    $pad = ' ' * $Depth
    $changeType = [string]$Node.changeType
    $label = if ($Node.label) { $Node.label } else { $Node.key }

    if ($Node.nodeType -eq 'Scalar') {
        switch ($changeType) {
            'Added'   { "$($Style.Green)$pad+ $($label): $($Node.newValue)$($Style.Reset)" }
            'Removed' { "$($Style.Red)$pad- $($label): $($Node.oldValue)$($Style.Reset)" }
            'Modified' {
                if ($Node.isSecret) {
                    # Never render a secret's value; the keyed hash only tells us that it changed.
                    "$($Style.Amber)$pad~ $($label) (secret changed; value hidden)$($Style.Reset)"
                }
                else {
                    "$($Style.Red)$pad- $($label): $($Node.oldValue)$($Style.Reset)"
                    "$($Style.Green)$pad+ $($label): $($Node.newValue)$($Style.Reset)"
                }
            }
            # 'Unchanged' produces no output, like a git diff.
        }
        return
    }

    # Object / Collection node.
    if ($changeType -eq 'Unchanged') {
        return
    }

    if ($changeType -eq 'Added' -or $changeType -eq 'Removed') {
        if ($changeType -eq 'Added') {
            "$($Style.Green)$pad+ $label$($Style.Reset)"
        }
        else {
            "$($Style.Red)$pad- $label$($Style.Reset)"
        }
        foreach ($child in $Node.children) {
            Format-JIMConfigurationDiffNode -Node $child -Depth ($Depth + 1) -Style $Style
        }
        return
    }

    # Modified container: a dim context header, then only the changed descendants below it.
    "$($Style.Dim)$pad$label$($Style.Reset)"
    foreach ($child in $Node.children) {
        Format-JIMConfigurationDiffNode -Node $child -Depth ($Depth + 1) -Style $Style
    }
}

function Format-JIMConfigurationDiff {
    # Renders a configuration diff (as returned by the change-history detail/compare endpoints) as a
    # git-style coloured unified diff. Emits strings so the output renders coloured in the terminal but
    # remains plain text when captured or redirected.
    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory)]
        [PSObject]$Diff
    )

    $style = Get-JIMDiffColour

    $from = if ($null -ne $Diff.oldVersion) { "v$($Diff.oldVersion)" } else { '(new)' }
    $to = if ($null -ne $Diff.newVersion) { "v$($Diff.newVersion)" } else { '(current)' }
    "$($style.Dim)$($Diff.objectType) `"$($Diff.objectName)`" ($from -> $to)$($style.Reset)"

    $total = [int]$Diff.addedCount + [int]$Diff.removedCount + [int]$Diff.modifiedCount
    if ($total -eq 0) {
        "$($style.Dim)(no changes)$($style.Reset)"
        return
    }

    foreach ($child in $Diff.root.children) {
        Format-JIMConfigurationDiffNode -Node $child -Depth 0 -Style $style
    }
}