src/Write-ScoutJsonStream.ps1
|
#Requires -Version 7.0 Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' <# .SYNOPSIS Write a top-level JSON object without materialising the complete JSON document in memory. .DESCRIPTION PowerShell's ConvertTo-Json returns one string containing the entire document. Large Scout estates can therefore hold the source objects, the serializer's object graph, and a second multi-hundred-megabyte string at the same time. This writer serializes top-level collections one element at a time and atomically replaces the destination only after the document closes. Individual resource rows still use ConvertTo-Json -Depth so PowerShell/JToken adaptation and the existing JSON contract remain unchanged. Peak serializer memory is bounded by the largest single row rather than the complete inventory. #> function Write-ScoutJsonStream { [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory)] [AllowNull()] $InputObject, [Parameter(Mandatory)] [string]$Path, [ValidateRange(1, 100)] [int]$Depth = 100 ) $parent = Split-Path -Parent $Path if (-not [string]::IsNullOrWhiteSpace($parent) -and -not (Test-Path -LiteralPath $parent)) { $null = New-Item -Path $parent -ItemType Directory -Force } $temporaryPath = '{0}.{1}.tmp' -f $Path, [guid]::NewGuid().ToString('N') $utf8NoBom = [System.Text.UTF8Encoding]::new($false) $writer = $null function ConvertTo-ScoutJsonFragment { param([AllowNull()]$Value) return ConvertTo-Json -InputObject $Value -Depth $Depth -Compress } function Test-ScoutJsonStreamCollection { param([AllowNull()]$Value) if ($null -eq $Value -or $Value -is [string] -or $Value -is [System.Collections.IDictionary]) { return $false } if ($Value -is [System.Management.Automation.PSCustomObject]) { return $false } if ($Value.GetType().FullName -eq 'Newtonsoft.Json.Linq.JObject') { return $false } return $Value -is [System.Collections.IEnumerable] } try { $writer = [System.IO.StreamWriter]::new($temporaryPath, $false, $utf8NoBom, 65536) $writer.WriteLine('{') $properties = @([System.Management.Automation.PSObject]::AsPSObject($InputObject).PSObject.Properties) for ($propertyIndex = 0; $propertyIndex -lt $properties.Count; $propertyIndex++) { $property = $properties[$propertyIndex] $nameJson = ConvertTo-ScoutJsonFragment -Value ([string]$property.Name) $writer.Write(' {0}: ' -f $nameJson) if (Test-ScoutJsonStreamCollection -Value $property.Value) { $writer.WriteLine('[') $itemIndex = 0 foreach ($item in $property.Value) { if ($itemIndex -gt 0) { $writer.WriteLine(',') } $writer.Write(' ') $writer.Write((ConvertTo-ScoutJsonFragment -Value $item)) $itemIndex++ } if ($itemIndex -gt 0) { $writer.WriteLine() } $writer.Write(' ]') } else { $writer.Write((ConvertTo-ScoutJsonFragment -Value $property.Value)) } if ($propertyIndex -lt ($properties.Count - 1)) { $writer.Write(',') } $writer.WriteLine() } $writer.WriteLine('}') $writer.Flush() $writer.Dispose() $writer = $null [System.IO.File]::Move($temporaryPath, $Path, $true) return $Path } catch { if ($null -ne $writer) { $writer.Dispose() } if (Test-Path -LiteralPath $temporaryPath) { Remove-Item -LiteralPath $temporaryPath -Force -ErrorAction SilentlyContinue } throw } } |