PsJsonQuery.psm1
function New-PsJsonQuery { <# .DESCRIPTION Returns a new PsJsonQuery object. .PARAMETER JsonFilePath JSON file to construct a new PsJsonQuery object with. .PARAMETER JsonObject JSON object to construct PsJsonQuery object with. This can be obtained from a JSON file by using something like: $JsonObject = Get-Content "example.json" | ConvertFrom-Json .Parameter IgnoreError (switch) Set if errors from PsJsonQuery object returned should be ignored. Can also set $Pq.IgnoreError property to $true or $false to edit. .Parameter IgnoreOutput (switch) Set if output from PsJsonQuery object returned should be ignored. Can also set $Pq.IgnoreOutput property to $true or $false to edit. .EXAMPLE Using a JSON file: $Pq = New-PsJsonQuery -JsonFilePath "example.json" Using a JSON object: $JsonObject = Get-Content "example.json" | ConvertFrom-Json $Pq = New-PsJsonQuery -JsonObject $JsonObject Other: $Pq = New-PsJsonQuery -JsonFilePath "example.json" -IgnoreError -IgnoreOutput #> param ( [Parameter(Mandatory=$true, ParameterSetName="File")] [string]$JsonFilePath, [Parameter(Mandatory=$true, ParameterSetName="Object")] [object]$JsonObject, [Parameter(Mandatory=$false, ParameterSetName="File")] [Parameter(Mandatory=$false, ParameterSetName="Object")] [switch]$IgnoreError, [Parameter(Mandatory=$false, ParameterSetName="File")] [Parameter(Mandatory=$false, ParameterSetName="Object")] [switch]$IgnoreOutput ) switch ($PSCmdlet.ParameterSetName) { "File" { $Pq = [PsJsonQuery]::new($JsonFilePath) } "Object" { $Pq = [PsJsonQuery]::new($JsonObject) } default { throw [System.ArgumentException]::new("Invalid argument") } } $Pq.IgnoreError = ($IgnoreError -eq $true) $Pq.IgnoreOutput = ($IgnoreOutput -eq $true) return $Pq } class PsJsonQuery { <# .DESCRIPTION Mimic jq functionality natively with PowerShell for instances when jq cannot be used. Supports basic jq queries in the same format as a normal jq query (ex: ".field1.field2.field3"). When querying for array elements, square brackets are required. Queries should use ".array.query[0]" format. .EXAMPLE # executing queries on an example.json $Pq = [PsJsonQuery]::new("example.json") $Array = $Pq.Query(".root.array[1]") $LeafNode = $Pq.Query(".root.leafNode") # get hashtable of leaf node paths (hashtable keys) and their values (hashtable values) $PathsHashtable = $Pq.Paths() # set a path and then save change to an output file called "example.modified.json" # the path being set must already exist in example.json for success $Pq.SetPath(".my.path.here", "newValue") $Pq.Save("example.modified.json") .NOTES JSON is output in a different order than the input, which is why save typically uses a different output file from the same input file. Errors from functions can be ignored by setting $Pq.IgnoreError = $true. Error/warning message output can be ignored by setting $Pq.IgnoreOutput = $true. #> [bool] $IgnoreError = $false [bool] $IgnoreOutput = $false [object] hidden $JsonObject = $null [hashtable] hidden $PathsHashtable = $null PsJsonQuery([string]$JsonFilePath) { if (-not (Test-Path $JsonFilePath)) { throw "Error: $JsonFilePath is not a valid file path" } $this.JsonObject = Get-Content $JsonFilePath | ConvertFrom-Json } PsJsonQuery([object]$JsonObject) { if ($null -eq $JsonObject) { throw "Error: JSON object passed is null" } $this.JsonObject = $JsonObject } [string] Query([string]$QueryPath) { <# .DESCRIPTION Returns JSON result of a query. Queries should be in the same format as a typical jq query, ex: ".field1.field2.field3". When accessing an array element directly, use [$index], such as in the example. Additionally, arrays can be filtered using a property in a query like ".root.array[].property" or ".root.array.property" - this is the only case when square brackets ([]) are optional. .PARAMETER QueryPath Path to query in same format as a typical jq query (".field1.field2.field3") .EXAMPLE $ArrayJSON = $Pq.Query(".root.array[0]") #> $PropertyPath = $this.ParseQueryPath($QueryPath) $Value = Invoke-Expression "`$this.JsonObject$PropertyPath" if ($null -eq $Value) { $ErrorMessage = "Cannot query `"$QueryPath`" when it does not already exist in the provided JSON" $this.HandleError($ErrorMessage) } return $Value | ConvertTo-Json -Depth 99 } [void] SetPath([string]$QueryPath, $Value) { <# .DESCRIPTION Sets the value of $QueryPath to $Value. The Query path should be in same format as described for Query(). To see changes, must call Save() function to write output to a file, otherwise they will only be present in $this.JsonObject. .PARAMETER QueryPath Path to query in same format as a typical jq query format (".field1.field2.field3") .PARAMETER Value Value to set $QueryPath to .EXAMPLE $Pq.SetPath(".root.array[0].faultDomains", 0) $Pq.Save("example.modified.json") #> # add quotes if string if ($Value -is [string]) { $Value = '"' + $Value + '"' } $PropertyPath = $this.ParseQueryPath($QueryPath) try { Invoke-Expression "`$this.JsonObject$PropertyPath = $Value" } catch { $ErrorMessage = "Cannot set `"$QueryPath`" when it does not already exist in the provided JSON" $this.HandleError($ErrorMessage) } } [hashtable] Paths() { <# .DESCRIPTION Returns a hashtable containing paths to leaf nodes as keys and the value at that leaf node as the value for that corresponding key. For example, using the following JSON: { "root": { "leafnode0": "myValue", "leafnode1": 0 } } this hashtable would contain the following key/value pairs: $this.PathsHashtable[".root.leafnode0"] = "myValue" $this.PathsHashtable["".root.leafnode1"] = 0 .EXAMPLE # add 1 to all integer values in hashtable $PathsHashtable = $Pq.Paths() foreach ($Path in $PathsHashtable.Keys) { if ($PathsHashtable[$Path] -is [int]) { $PathsHashtable[$Path]++ } } #> $this.PathsHashtable = @{} $this.PathsHelper($this.JsonObject, "") return $this.PathsHashtable } [Collections.Generic.List[string]] GetPathsToValue($Value) { <# .DESCRIPTION Returns a list of all paths to the given value. The value provided must be a leaf node of the JSON. Paths to keys will throw an error or give an incorrect result. .PARAMETER Value Value to obtain the paths to .EXAMPLE $Paths = $Jq.GetPathsToValue("myValue") #> # fill out $this.PathsHashtable $this.Paths() $MatchedPaths = [Collections.Generic.List[string]]::new() foreach ($Item in $this.PathsHashtable.GetEnumerator()) { if (($null -eq $Item.Value) -or ($null -eq $Value)) { if ($Item.Value -eq $Value) { $MatchedPaths.Add($Item.Name) } continue } # value is a match if $Item.Value is equal AND of the same type if (($Item.Value.GetType().Name -eq $Value.GetType().Name) -and ($Item.Value -eq $Value)) { $MatchedPaths.Add($Item.Name) } } if ($MatchedPaths.Count -eq 0) { $ErrorMessage = "Could not find a path to value: `"$Value`" in the provided JSON" $this.HandleError($ErrorMessage) } return $MatchedPaths } [void] Save([string]$OutputFilePath) { <# .DESCRIPTION Saves the contents of $this.JsonObject to a $OutputFilePath .PARAMETER OutputFilePath Path to output $this.JsonObject as JSON to .EXAMPLE $Pq.SetPath(".root.array[0].property", 0) $Pq.Save("example.modified.json") .NOTES $this.JsonObject is unordered so the output will contain all the same inputs/any updates that have been made using SetPath() or manually, but will be formatted differently. So, while the output file will look different, the contained information is not. #> $this.JsonObject | ConvertTo-Json -Depth 99 | Out-File -FilePath $OutputFilePath -Encoding "ASCII" } [void] hidden PathsHelper($JsonObject, [string]$Path) { <# .DESCRIPTION Performs a recursive depth-first search of $JsonObject to obtain all leaf nodes and their paths. Essentially looks at each NoteProperty in the $JsonObject passed to the function. If the object does not contain any note properties, then it is a leaf and is added to $this.PathsHashtable in the format described in Paths(). [System.Object[]] is a special case, because these arrays must be processed using their indices instead of properties and may potentially contain more objects with note properties. Their elements are all inspected recursively for note properties until leaf nodes are reached. The Path to each key is constructed with each function call by adding the key every time in "Path.Key" format and appending the index ("$Path[$index]") when necessary. .PARAMETER JsonObject JSON object that is being traversed to find paths to leaf nodes, leaf node values .PARAMETER Path Path that is appended to with each call of this function. Starts as "" #> # in case an object has a null value, which would fail # the operation to obtain the $NoteProperties if ($null -eq $JsonObject) { $this.PathsHashtable[$Path] = $JsonObject return } $NoteProperties = $JsonObject | Get-Member -MemberType NoteProperty | Select-Object -Property Name # if no note properties, add to hashtable and return since this is a leaf node if ($null -eq $NoteProperties) { $this.PathsHashtable[$Path] = $JsonObject return } foreach ($Property in $NoteProperties) { $Name = $Property.Name $Value = $JsonObject.$Name # inspect recursively # if array, process path using proper indexing if ($Value -is [System.Object[]]) { for ($i = 0; $i -lt $Value.Length; $i++) { $this.PathsHelper($Value[$i], "$Path.$Name[$i]") } } else { $this.PathsHelper($Value, "$Path.$Name") } } } [string] hidden ParseQueryPath([string]$QueryPath) { <# .DESCRIPTION Remove any instances of [], and otherwise use $QueryPath to directly to access hashtable elements. .PARAMETER QueryPath Path to parse for key values #> if (-not $QueryPath.StartsWith(".")) { $ErrorMessage = "Query `"$QueryPath`" must start with a `".`"" $this.HandleError($ErrorMessage) return "" } elseif ($QueryPath -eq ".") { return "" } else { return $QueryPath.Replace("[]", "") } } [void] hidden HandleError([string]$ErrorMessage) { <# .DESCRIPTION Output error/exception when necessary based on properties set. .PARAMETER ErrorMessage Message to display depending on error/output preferences. .EXAMPLE $ErrorMessage = "Error Occurred!" $this.HandleError($ErrorMessage) #> if (-not $this.IgnoreError) { $ErrorActionPreference = "Stop" throw $ErrorMessage } if (-not $this.IgnoreOutput) { Write-Verbose -Message $ErrorMessage -Verbose } } } |