Private/Json.ps1
|
# JSON in and out, and the small data helpers the rest of the module shares. # # Reading goes through System.Text.Json rather than ConvertFrom-Json, which turns any string that looks like a # date into a DateTime (PowerShell before 7.5 has no way to stop it). The module sends template-form properties # back to the engine it read them from, and a date string that came back as another spelling of the same # instant would read as a change. # # Convention: a helper that answers an array answers it as ONE value (return , $array), so an array of one # element stays an array. Never wrap such a call in @(): that nests the array. function Get-VMetricDictionaryKey { # The key of a dictionary that matches a name, exactly first and then ignoring case; $null when none does. [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory)] [System.Collections.IDictionary] $Dictionary, [Parameter(Mandatory)] [string] $Name ) $match = $null foreach ($key in $Dictionary.Keys) { $text = [string]$key if ([string]::Equals($text, $Name, [System.StringComparison]::Ordinal)) { return $text } if ($null -eq $match -and [string]::Equals($text, $Name, [System.StringComparison]::OrdinalIgnoreCase)) { $match = $text } } return $match } function Get-VMetricMember { # A property of an object or a key of a dictionary, ignoring case; $null when it has none. Safe under strict # mode. An array value comes back as one array (see the convention above). [CmdletBinding()] param( [AllowNull()] [object] $InputObject, [Parameter(Mandatory)] [string] $Name ) if ($null -eq $InputObject) { return $null } $value = $null if ($InputObject -is [System.Collections.IDictionary]) { $key = Get-VMetricDictionaryKey -Dictionary $InputObject -Name $Name if ($null -eq $key) { return $null } $value = $InputObject[$key] } else { $property = $InputObject.psobject.Properties[$Name] if ($null -eq $property) { return $null } $value = $property.Value } if ($null -eq $value) { return $null } if ($value -is [array]) { return , $value } return $value } function ConvertTo-VMetricArray { # Any value as an array: $null is empty, a list is copied, anything else is one element. [CmdletBinding()] [OutputType([object[]])] param( [AllowNull()] [object] $InputObject ) if ($null -eq $InputObject) { return , @() } if ($InputObject -is [string] -or $InputObject -is [System.Collections.IDictionary]) { return , @($InputObject) } if ($InputObject -is [System.Collections.IEnumerable]) { $list = [System.Collections.Generic.List[object]]::new() foreach ($item in $InputObject) { $list.Add($item) } return , $list.ToArray() } return , @($InputObject) } function Test-VMetricObjectLike { # True for a dictionary or a PSCustomObject: something with named values. [CmdletBinding()] [OutputType([bool])] param( [AllowNull()] [object] $InputObject ) if ($null -eq $InputObject) { return $false } if ($InputObject -is [System.Collections.IDictionary]) { return $true } return ($InputObject -is [System.Management.Automation.PSObject] -and $InputObject.psobject.BaseObject -is [System.Management.Automation.PSCustomObject]) } function ConvertFrom-VMetricJson { # JSON text as PSCustomObjects (or ordered dictionaries with -AsHashtable), strings kept as strings, # integers as Int64. [CmdletBinding()] param( [Parameter(Mandatory)] [AllowNull()] [AllowEmptyString()] [string] $Json, [switch] $AsHashtable ) if ([string]::IsNullOrWhiteSpace($Json)) { return $null } $options = [System.Text.Json.JsonDocumentOptions]::new() $options.MaxDepth = 256 $options.AllowTrailingCommas = $true $options.CommentHandling = [System.Text.Json.JsonCommentHandling]::Skip try { $document = [System.Text.Json.JsonDocument]::Parse($Json, $options) } catch { $inner = $_.Exception while ($inner.InnerException) { $inner = $inner.InnerException } throw (New-VMetricException -Message "The text is not valid JSON: $($inner.Message)" -Code 'InvalidJson' ` -Category InvalidData -InnerException $_.Exception) } $hashtable = $AsHashtable.IsPresent $convert = { param([System.Text.Json.JsonElement] $Element) switch ($Element.ValueKind) { 'Object' { $map = [ordered]@{} foreach ($property in $Element.EnumerateObject()) { $map[$property.Name] = & $convert $property.Value } if ($hashtable) { return $map } return [pscustomobject]$map } 'Array' { $list = [System.Collections.Generic.List[object]]::new() foreach ($item in $Element.EnumerateArray()) { $list.Add((& $convert $item)) } return , $list.ToArray() } 'String' { return $Element.GetString() } 'Number' { $long = 0L if ($Element.TryGetInt64([ref] $long)) { return $long } $decimal = 0.0d if ($Element.TryGetDecimal([ref] $decimal)) { return $decimal } return $Element.GetDouble() } 'True' { return $true } 'False' { return $false } } return $null } try { $root = & $convert $document.RootElement } finally { $document.Dispose() } if ($root -is [array]) { return , $root } return $root } function ConvertTo-VMetricWireValue { # A deep copy ready for ConvertTo-Json: a SecureString becomes its text (this is the only place the module # reads one), a switch a boolean, an enum its name, and every container a plain ordered dictionary or # array, dropping the notes PowerShell attaches to strings read from files. [CmdletBinding()] param( [AllowNull()] [object] $InputObject, # For text a person reads: a SecureString becomes *** instead of its text. [switch] $MaskSecureString ) $mask = $MaskSecureString.IsPresent $toWire = { param($Value) if ($null -eq $Value) { return $null } if ($Value -is [System.Security.SecureString]) { if ($mask) { return '***' } return [System.Net.NetworkCredential]::new('', $Value).Password } if ($Value -is [string]) { return $Value.ToString() } if ($Value -is [System.Management.Automation.SwitchParameter]) { return $Value.IsPresent } if ($Value -is [enum] -or $Value -is [guid] -or $Value -is [char] -or $Value -is [uri]) { return $Value.ToString() } if ($Value -is [System.ValueType]) { return $Value } if ($Value -is [System.Collections.IDictionary]) { $map = [System.Collections.Specialized.OrderedDictionary]::new() foreach ($key in $Value.Keys) { $map[[string]$key] = & $toWire $Value[$key] } return $map } if ($Value -is [System.Management.Automation.PSObject] -and $Value.psobject.BaseObject -is [System.Management.Automation.PSCustomObject]) { $map = [System.Collections.Specialized.OrderedDictionary]::new() foreach ($property in $Value.psobject.Properties) { if ($property.MemberType -eq 'NoteProperty') { $map[$property.Name] = & $toWire $property.Value } } return $map } if ($Value -is [System.Collections.IEnumerable]) { $list = [System.Collections.Generic.List[object]]::new() foreach ($item in $Value) { $list.Add((& $toWire $item)) } return , $list.ToArray() } return $Value.ToString() } $wire = & $toWire $InputObject if ($wire -is [array]) { return , $wire } return $wire } function ConvertTo-VMetricJson { # Compact JSON for a request body. The text holds any secret the body carries: it goes to the transport and # nowhere else (never to verbose, debug or an error record). [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory)] [AllowNull()] [object] $InputObject ) $wire = ConvertTo-VMetricWireValue -InputObject $InputObject ConvertTo-Json -InputObject $wire -Depth 100 -Compress } function Copy-VMetricData { # A deep, mutable copy: objects become ordered dictionaries, arrays new arrays, values stay as they are # (a SecureString included; it is converted only when the body is serialized). [CmdletBinding()] param( [AllowNull()] [object] $InputObject ) $copy = { param($Value) if ($null -eq $Value) { return $null } if ($Value -is [string] -or $Value -is [System.ValueType] -or $Value -is [System.Security.SecureString]) { return $Value } if ($Value -is [System.Collections.IDictionary]) { $map = [ordered]@{} foreach ($key in $Value.Keys) { $map[[string]$key] = & $copy $Value[$key] } return $map } if ($Value -is [System.Management.Automation.PSObject] -and $Value.psobject.BaseObject -is [System.Management.Automation.PSCustomObject]) { $map = [ordered]@{} foreach ($property in $Value.psobject.Properties) { if ($property.MemberType -eq 'NoteProperty') { $map[$property.Name] = & $copy $property.Value } } return $map } if ($Value -is [System.Collections.IEnumerable]) { $list = [System.Collections.Generic.List[object]]::new() foreach ($item in $Value) { $list.Add((& $copy $item)) } return , $list.ToArray() } return $Value } $result = & $copy $InputObject if ($result -is [array]) { return , $result } return $result } function ConvertTo-VMetricPSObject { # Ordered dictionaries (deeply) as PSCustomObjects, for output. [CmdletBinding()] param( [AllowNull()] [object] $InputObject ) $convert = { param($Value) if ($null -eq $Value -or $Value -is [string] -or $Value -is [System.ValueType] -or $Value -is [System.Security.SecureString]) { return $Value } if ($Value -is [System.Collections.IDictionary]) { $map = [ordered]@{} foreach ($key in $Value.Keys) { $map[[string]$key] = & $convert $Value[$key] } return [pscustomobject]$map } if ($Value -is [System.Management.Automation.PSObject] -and $Value.psobject.BaseObject -is [System.Management.Automation.PSCustomObject]) { return $Value } if ($Value -is [System.Collections.IEnumerable]) { $list = [System.Collections.Generic.List[object]]::new() foreach ($item in $Value) { $list.Add((& $convert $item)) } return , $list.ToArray() } return $Value } $result = & $convert $InputObject if ($result -is [array]) { return , $result } return $result } function Format-VMetricValue { # A value as a what-if line shows it: JSON, so an empty string, null and a number read differently. [CmdletBinding()] [OutputType([string])] param( [AllowNull()] [object] $InputObject ) if ($null -eq $InputObject) { return 'null' } $text = ConvertTo-Json -InputObject (ConvertTo-VMetricWireValue -InputObject $InputObject -MaskSecureString) -Depth 20 -Compress return (ConvertTo-VMetricSafeText -Text $text) } |