ps/Modules/Alkami.PowerShell.Choco/TestFiles/Write-OrderedJson.ps1
2023-05-30 22:51:22 -07:00

183 lines
7.3 KiB
PowerShell

function Write-OrderedJson {
<#
.SYNOPSIS
Given a PSCustomObject, write it as pretty indented json WITH ORDERING for consistency
.PARAMETER JsonObject
An object that should be written as json. Should be a PSCustomObject or Hashtable
.PARAMETER InputPath
A path to a given file to reduce need to import file contents prior to running
.PARAMETER NoKeyReorder
[switch] Should avoid reordering keys
.PARAMETER Path
A filesystem path
#>
[CmdletBinding(DefaultParameterSetName = 'JsonObject')]
[OutputType([string])]
param (
# Setting to mandatory false because it can be null and that's ok, we just return null
[Parameter(Mandatory = $false, ValueFromPipeline = $true, ParameterSetName = 'JsonObject')]
$JsonObject,
[Parameter(Mandatory = $true, ParameterSetName = 'InputPath')]
$InputPath,
[switch]$NoKeyReorder,
[Parameter(Mandatory = $false)]
$Path
)
if ($PSCmdlet.ParameterSetName -eq 'InputPath') {
$JsonObject = (Get-Content -Path $InputPath -Raw)
}
if ($JsonObject -is [string]) {
$JsonObject = (ConvertFrom-Json $JsonObject)
} elseif ($JsonObject -is [Array]) {
$JsonObject = (ConvertFrom-Json ([string]::Join('',$JsonObject)))
}
#region innerFunctions
$stringBuilder = New-Object System.Text.StringBuilder
$quoteString = '"'
$commaString = ","
$OrderedKeys = !$NoKeyReorder
$primitiveMatch = 'byte|short|int32|long|sbyte|ushort|uint32|ulong|float|double|decimal|boolean'
function Get-JsonStringLeadsByDepth {
param (
$Depth = 0
)
$spacesString = [string]::new(" ", ($Depth + 1) * 2)
$shortSpacesString = ""
if ($Depth -gt 0) {
$shortSpacesString = [string]::new(" ", $Depth * 2)
}
return ($spacesString, $shortSpacesString)
}
function Build-JsonArrayNameValuePair {
param (
$JsonName,
$JsonValue,
$Depth = 0
)
$spacesString, $shortSpacesString = (Get-JsonStringLeadsByDepth -Depth $Depth)
if ($OrderedKeys) {
$JsonValue = $JsonValue | Sort-Object -Property Name,Key,Id
}
$stringBuilder.Append($shortSpacesString) | Out-Null
if (![string]::IsNullOrWhiteSpace($JsonName)) {
$stringBuilder.Append("$quoteString$JsonName$quoteString : ") | Out-Null
}
$stringBuilder.AppendLine("[") | Out-Null
if (($JsonValue[0].GetType().Name -match $primitiveMatch) -or ($JsonValue[0] -is [string])) {
$JsonValue = $JsonValue | Sort-Object
}
foreach ($iter in $JsonValue) {
if ($iter.GetType().Name -match $primitiveMatch) {
# Write it without quotes
$stringBuilder.AppendLine("$spacesString$iter$commaString") | Out-Null
} elseif ($iter -is [string]) {
# Write it with quotes
$stringBuilder.AppendLine("$spacesString$quoteString$iter$quoteString$commaString") | Out-Null
} else {
# Must be a complex object, but without a name, so write the value
Build-JsonObject -JsonName $null -JsonValue $iter -Depth ($Depth + 1)
}
}
$stringBuilder.AppendLine("$shortSpacesString]$commaString") | Out-Null
}
function Build-JsonObject {
param (
$JsonName,
$JsonValue,
$Depth = 0
)
$spacesString, $shortSpacesString = (Get-JsonStringLeadsByDepth -Depth $Depth)
$stringBuilder.Append($shortSpacesString) | Out-Null
if (![string]::IsNullOrWhiteSpace($JsonName)) {
$stringBuilder.Append("$quoteString$JsonName$quoteString : ") | Out-Null
}
$stringBuilder.AppendLine("{") | Out-Null
if (!(Test-IsCollectionNullOrEmpty $JsonValue.PSObject.Properties.Where({$_.MemberType -eq 'NoteProperty'}))) {
$JsonValue = $JsonValue.PSObject.Properties.Where({$_.MemberType -eq 'NoteProperty'})
}
if ($OrderedKeys) {
$JsonValue = $JsonValue | Sort-Object -Property Name, Key, Id
}
$keys = $JsonValue.Keys
if (Test-IsCollectionNullOrEmpty $keys) {
$keys = $JsonValue.Name
}
foreach ($key in $keys) {
$iter = $JsonValue[$key]
if ($null -eq $iter) {
$posit = $JsonValue.Where({$_.Name -eq $key})
if ($null -ne $posit) {
$iter = $posit.Value
}
}
if ($null -eq $iter) {
$stringBuilder.AppendLine("$spacesString$quoteString$key$quoteString : null$commaString") | Out-Null
} else {
if ($iter -ceq "False") { $iter = $false }
if ($iter -ceq "True") { $iter = $true }
if ($iter.GetType().Name -match $primitiveMatch) {
$stringBuilder.AppendLine("$spacesString$quoteString$key$quoteString : $($iter.ToString().ToLower())$commaString") | Out-Null
} elseif ($iter -is [string]) {
$stringBuilder.AppendLine("$spacesString$quoteString$key$quoteString : $quoteString$iter$quoteString$commaString") | Out-Null
} elseif ($iter.GetType().IsArray) {
# values are an array, so let's write the array values
Build-JsonArrayNameValuePair -JsonName $key -JsonValue $iter -Depth ($Depth + 1)
} else {
# It was not an array, or a primitive, so it must be _another_ object?
Build-JsonObject -JsonName $key -JsonValue $iter -Depth ($Depth + 1)
}
}
}
$stringBuilder.AppendLine("$shortSpacesString}$commaString") | Out-Null
}
#endregion innerFunctions
#region primitives should just be written as-is
if ($JsonObject -is [string]) {
return "$quoteString$JsonObject$quoteString"
} elseif ($JsonObject -is [bool]) {
return $JsonObject.ToString().ToLower()
} elseif ($JsonObject.GetType().Name -match $primitiveMatch) {
return $JsonObject.ToString()
#endregion primitives should just be written as-is
} elseif ($JsonObject.GetType().IsArray) {
Build-JsonArrayNameValuePair -JsonName $null -JsonValue $JsonObject
} else {
Build-JsonObject -JsonName $null -JsonValue $JsonObject
}
# postprocess to convert this string: ",(\r?\n?\s*?[\]\}])" to this string "$1" (mind the escaping tho)
# That string says "any comma followed by any newline character(s) and any number of spaces, followed by a closing brace (indicating the end of an array or object) should remove the comma but retain the rest of the string"
# This removes trailing commas in arrays and object notations
$regex = New-Object System.Text.RegularExpressions.Regex(",(\r?\n?\s*?[\]\}])")
$builtString = $stringBuilder.ToString()
$builtString = $regex.Replace($builtString, "`$1")
$builtString = $builtString.TrimEnd().TrimEnd(",")
if ([string]::IsNullOrWhiteSpace($Path)) {
Write-Host "Path: $path"
return $builtString
} else {
Set-Content -Path $Path -Value $builtString -Force | Out-Null
}
}