Support/ExecutionCore/Eigenverft.Manifested.Package.ExecutionCore.Json.ps1

<#
    Eigenverft.Manifested.Package.ExecutionCore.Json
    Generic JSON read/write helpers for module-owned documents.
#>


function Read-PackageJsonDocument {
<#
.SYNOPSIS
Reads a Package JSON document from disk.
 
.DESCRIPTION
Resolves a JSON file path, validates that it contains content, parses it, and
returns the resolved path together with the parsed document object.
 
.PARAMETER Path
Path to the JSON file that should be loaded.
 
.EXAMPLE
Read-PackageJsonDocument -Path .\Configuration\Internal\PackageConfig.json
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Path
    )

    $resolvedPath = (Resolve-Path -LiteralPath $Path -ErrorAction Stop).Path
    $rawContent = Get-Content -LiteralPath $resolvedPath -Raw -ErrorAction Stop
    if ([string]::IsNullOrWhiteSpace($rawContent)) {
        throw "Package JSON file '$resolvedPath' is empty."
    }

    try {
        $document = $rawContent | ConvertFrom-Json -ErrorAction Stop
    }
    catch {
        throw "Package JSON file '$resolvedPath' could not be parsed. $($_.Exception.Message)"
    }

    return [pscustomobject]@{
        Path     = $resolvedPath
        Document = $document
    }
}

function ConvertTo-PackageJsonEscapedString {
    [CmdletBinding()]
    param(
        [AllowNull()]
        [string]$Value
    )

    if ($null -eq $Value) {
        return '""'
    }

    $builder = [System.Text.StringBuilder]::new()
    $null = $builder.Append('"')
    foreach ($ch in $Value.ToCharArray()) {
        $code = [int][char]$ch
        $escaped = switch ($code) {
            8 { '\b'; break }
            9 { '\t'; break }
            10 { '\n'; break }
            12 { '\f'; break }
            13 { '\r'; break }
            34 { '\"'; break }
            92 { '\\'; break }
            default {
                if ($code -lt 32) {
                    '\u{0:x4}' -f $code
                    break
                }
                [string]$ch
                break
            }
        }
        $null = $builder.Append($escaped)
    }
    $null = $builder.Append('"')
    return $builder.ToString()
}

function ConvertTo-PackagePrettyJson {
    [CmdletBinding()]
    param(
        [AllowNull()]
        [object]$Value,

        [int]$Depth = 0
    )

    if ($null -eq $Value) {
        return 'null'
    }
    if ($Value -is [bool]) {
        if ($Value) {
            return 'true'
        }
        return 'false'
    }
    if ($Value -is [string] -or $Value -is [char] -or $Value -is [guid]) {
        return ConvertTo-PackageJsonEscapedString -Value ([string]$Value)
    }
    if ($Value -is [datetime]) {
        $utcDate = ([datetime]$Value).ToUniversalTime()
        $dateText = if (($utcDate.Ticks % [TimeSpan]::TicksPerSecond) -eq 0) {
            $utcDate.ToString('yyyy-MM-ddTHH:mm:ssZ', [Globalization.CultureInfo]::InvariantCulture)
        }
        else {
            $utcDate.ToString('o', [Globalization.CultureInfo]::InvariantCulture)
        }
        return ConvertTo-PackageJsonEscapedString -Value $dateText
    }
    if ($Value -is [byte] -or $Value -is [sbyte] -or
        $Value -is [int16] -or $Value -is [uint16] -or
        $Value -is [int] -or $Value -is [uint32] -or
        $Value -is [long] -or $Value -is [uint64] -or
        $Value -is [decimal]) {
        return ([System.Convert]::ToString($Value, [Globalization.CultureInfo]::InvariantCulture))
    }
    if ($Value -is [single] -or $Value -is [double]) {
        $doubleValue = [double]$Value
        if ([double]::IsNaN($doubleValue) -or [double]::IsInfinity($doubleValue)) {
            throw 'JSON cannot represent NaN or Infinity.'
        }
        return $doubleValue.ToString('R', [Globalization.CultureInfo]::InvariantCulture)
    }

    $indent = ' ' * ($Depth * 2)
    $childIndent = ' ' * (($Depth + 1) * 2)

    if ($Value -is [System.Collections.IDictionary]) {
        $properties = @(
            foreach ($key in @($Value.Keys)) {
                [pscustomobject]@{
                    Name  = [string]$key
                    Value = $Value[$key]
                }
            }
        )

        if ($properties.Count -eq 0) {
            return '{}'
        }

        $propertyParts = @(
            foreach ($property in @($properties)) {
                '{0}{1}: {2}' -f $childIndent, (ConvertTo-PackageJsonEscapedString -Value $property.Name), (ConvertTo-PackagePrettyJson -Value $property.Value -Depth ($Depth + 1))
            }
        )
        return '{' + [Environment]::NewLine + ($propertyParts -join (',' + [Environment]::NewLine)) + [Environment]::NewLine + $indent + '}'
    }
    if ($Value -is [System.Collections.IEnumerable] -and -not ($Value -is [string])) {
        $items = @(
            foreach ($item in $Value) {
                ConvertTo-PackagePrettyJson -Value $item -Depth ($Depth + 1)
            }
        )

        if ($items.Count -eq 0) {
            return '[]'
        }

        $itemParts = @(
            foreach ($itemJson in @($items)) {
                '{0}{1}' -f $childIndent, $itemJson
            }
        )
        return '[' + [Environment]::NewLine + ($itemParts -join (',' + [Environment]::NewLine)) + [Environment]::NewLine + $indent + ']'
    }

    $objectProperties = @(
        foreach ($property in @($Value.PSObject.Properties)) {
            if ($property.MemberType -notin @('NoteProperty', 'Property', 'AliasProperty')) {
                continue
            }
            [pscustomobject]@{
                Name  = [string]$property.Name
                Value = $property.Value
            }
        }
    )

    if ($objectProperties.Count -eq 0) {
        return '{}'
    }

    $objectParts = @(
        foreach ($property in @($objectProperties)) {
            '{0}{1}: {2}' -f $childIndent, (ConvertTo-PackageJsonEscapedString -Value $property.Name), (ConvertTo-PackagePrettyJson -Value $property.Value -Depth ($Depth + 1))
        }
    )
    return '{' + [Environment]::NewLine + ($objectParts -join (',' + [Environment]::NewLine)) + [Environment]::NewLine + $indent + '}'
}

function Save-PackageJsonDocument {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Path,

        [Parameter(Mandatory = $true)]
        [psobject]$Document
    )

    $resolvedPath = [System.IO.Path]::GetFullPath($Path)
    $directory = Split-Path -Parent $resolvedPath
    if (-not [string]::IsNullOrWhiteSpace($directory)) {
        $null = New-Item -ItemType Directory -Path $directory -Force
    }

    $temporaryPath = '{0}.{1}.tmp' -f $resolvedPath, ([guid]::NewGuid().ToString('N'))
    try {
        $utf8NoBom = [System.Text.UTF8Encoding]::new($false)
        [System.IO.File]::WriteAllText($temporaryPath, (ConvertTo-PackagePrettyJson -Value $Document), $utf8NoBom)
        Move-Item -LiteralPath $temporaryPath -Destination $resolvedPath -Force
    }
    finally {
        if (Test-Path -LiteralPath $temporaryPath -PathType Leaf) {
            Remove-Item -LiteralPath $temporaryPath -Force -ErrorAction SilentlyContinue
        }
    }
}