| | | 1 | | # Portions derived from PowerShell-Yaml (https://github.com/cloudbase/powershell-yaml) |
| | | 2 | | # Copyright (c) 2016–2024 Cloudbase Solutions Srl |
| | | 3 | | # Licensed under the Apache License, Version 2.0 (Apache-2.0). |
| | | 4 | | # You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 |
| | | 5 | | # Modifications Copyright (c) 2025 Kestrun Contributors |
| | | 6 | | |
| | | 7 | | <# |
| | | 8 | | .SYNOPSIS |
| | | 9 | | Converts a YAML string to a PowerShell object or hashtable. |
| | | 10 | | .DESCRIPTION |
| | | 11 | | The ConvertFrom-KrYaml cmdlet converts a YAML string to a PowerShell object or |
| | | 12 | | hashtable. By default, it returns a PSCustomObject, but you can specify the |
| | | 13 | | -AsHashtable switch to get a hashtable instead. |
| | | 14 | | .PARAMETER Yaml |
| | | 15 | | The YAML string to convert. This parameter is mandatory and accepts input from the pipeline. |
| | | 16 | | .PARAMETER AllDocuments |
| | | 17 | | If specified, all documents in a multi-document YAML stream will be returned as an array. By default, only the first |
| | | 18 | | .PARAMETER UseMergingParser |
| | | 19 | | If specified, the YAML parser will support the merging key (<<) for merging mappings. |
| | | 20 | | This is useful for YAML documents that use anchors and aliases to merge mappings. |
| | | 21 | | .EXAMPLE |
| | | 22 | | $yaml = @" |
| | | 23 | | name: John |
| | | 24 | | age: 30 |
| | | 25 | | skills: |
| | | 26 | | - PowerShell |
| | | 27 | | - YAML |
| | | 28 | | "@ |
| | | 29 | | $obj = $yaml | ConvertFrom-KrYaml |
| | | 30 | | # Outputs a PSCustomObject with properties Name, Age, and Skills. |
| | | 31 | | .EXAMPLE |
| | | 32 | | $yaml = @" |
| | | 33 | | --- |
| | | 34 | | name: John |
| | | 35 | | age: 30 |
| | | 36 | | --- |
| | | 37 | | name: Jane Doe |
| | | 38 | | age: 25 |
| | | 39 | | "@ |
| | | 40 | | $objs = $yaml | ConvertFrom-KrYaml -AllDocuments |
| | | 41 | | # Outputs an array of two PSCustomObjects, one for each document in the YAML stream. |
| | | 42 | | .EXAMPLE |
| | | 43 | | $yaml = @" |
| | | 44 | | defaults: &defaults |
| | | 45 | | adapter: postgres |
| | | 46 | | host: localhost |
| | | 47 | | development: |
| | | 48 | | database: dev_db |
| | | 49 | | <<: *defaults |
| | | 50 | | test: |
| | | 51 | | database: test_db |
| | | 52 | | <<: *defaults |
| | | 53 | | "@ |
| | | 54 | | $obj = $yaml | ConvertFrom-KrYaml -UseMergingParser |
| | | 55 | | # Outputs a PSCustomObject with merged properties for 'development' and 'test' sections |
| | | 56 | | # using the 'defaults' anchor. |
| | | 57 | | $obj | Format-List |
| | | 58 | | .NOTES |
| | | 59 | | This cmdlet requires PowerShell 7.0 or later. |
| | | 60 | | It uses the Kestrun.Utilities.Yaml library for YAML deserialization. |
| | | 61 | | #> |
| | | 62 | | function ConvertFrom-KrYaml { |
| | | 63 | | [KestrunRuntimeApi('Everywhere')] |
| | | 64 | | [CmdletBinding()] |
| | | 65 | | param( |
| | | 66 | | [Parameter(Mandatory = $false, ValueFromPipeline = $true, Position = 0)] |
| | | 67 | | [string]$Yaml, |
| | | 68 | | [switch]$AllDocuments = $false, |
| | | 69 | | [switch]$UseMergingParser = $false |
| | | 70 | | ) |
| | | 71 | | |
| | | 72 | | begin { |
| | 1 | 73 | | $builder = [System.Text.StringBuilder]::new() |
| | | 74 | | } |
| | | 75 | | process { |
| | 1 | 76 | | if ($Yaml -is [string]) { |
| | 1 | 77 | | if ($builder.Length -gt 0) { |
| | 0 | 78 | | [void]$builder.Append("`n") |
| | | 79 | | } |
| | 1 | 80 | | [void]$builder.Append($Yaml) |
| | | 81 | | } |
| | | 82 | | } |
| | | 83 | | |
| | | 84 | | end { |
| | 1 | 85 | | $d = $builder.ToString() |
| | 1 | 86 | | if ([string]::IsNullOrEmpty($d)) { |
| | | 87 | | return |
| | | 88 | | } |
| | 1 | 89 | | $yamlStream = [Kestrun.Utilities.Yaml.YamlLoader]::GetYamlDocuments($d, $UseMergingParser) |
| | 1 | 90 | | $documents = $yamlStream.Documents |
| | 1 | 91 | | if (!$documents -or !$documents.Count) { |
| | | 92 | | return |
| | | 93 | | } |
| | 1 | 94 | | $firstRoot = $documents[0].RootNode |
| | | 95 | | |
| | | 96 | | # Extract datesAsStrings values (if present) from the parsed YAML object BEFORE conversion so we can restore the |
| | 1 | 97 | | $rawDatesAsStrings = $null |
| | 1 | 98 | | if ($firstRoot -is [System.Collections.IDictionary] -and $firstRoot.Keys -contains 'datesAsStrings') { |
| | 0 | 99 | | $seq = $firstRoot['datesAsStrings'] |
| | 0 | 100 | | if ($seq -is [System.Collections.IList]) { |
| | | 101 | | # Collect the original values as strings, preserving their lexical form |
| | 0 | 102 | | $rawDatesAsStrings = @() |
| | 0 | 103 | | foreach ($item in $seq) { |
| | 0 | 104 | | $rawDatesAsStrings += $item.ToString() |
| | | 105 | | } |
| | | 106 | | } |
| | | 107 | | } |
| | | 108 | | |
| | 1 | 109 | | if ($documents.Count -eq 1 -and -not $AllDocuments) { |
| | | 110 | | # Always request ordered conversion so original key order from YAML is preserved by default. |
| | 1 | 111 | | $single = [Kestrun.Utilities.Yaml.YamlTypeConverter]::ConvertYamlDocumentToPSObject($firstRoot, $true) |
| | 1 | 112 | | $single = Convert-DateTimeOffsetToDateTime $single |
| | 3 | 113 | | if ($rawDatesAsStrings -and ($single -is [System.Collections.IDictionary]) -and ($single.Keys -contains 'dat |
| | 0 | 114 | | $seq = $single['datesAsStrings'] |
| | 0 | 115 | | if ($seq -is [System.Collections.IList]) { |
| | 0 | 116 | | for ($i = 0; $i -lt $seq.Count -and $i -lt $rawDatesAsStrings.Count; $i++) { |
| | | 117 | | # Only replace if parser turned it into DateTime |
| | 0 | 118 | | if ($seq[$i] -is [datetime] -or $seq[$i] -is [DateTimeOffset]) { |
| | 0 | 119 | | $seq[$i] = $rawDatesAsStrings[$i] |
| | | 120 | | } |
| | | 121 | | } |
| | | 122 | | } |
| | | 123 | | } |
| | 1 | 124 | | return $single |
| | | 125 | | } |
| | 0 | 126 | | if (-not $AllDocuments) { |
| | | 127 | | # Always request ordered conversion so original key order from YAML is preserved by default. |
| | 0 | 128 | | $single = [Kestrun.Utilities.Yaml.YamlTypeConverter]::ConvertYamlDocumentToPSObject($firstRoot, $true) |
| | 0 | 129 | | $single = Convert-DateTimeOffsetToDateTime $single |
| | 0 | 130 | | if ($rawDatesAsStrings -and ($single -is [System.Collections.IDictionary]) -and ($single.Keys -contains 'dat |
| | 0 | 131 | | $seq = $single['datesAsStrings'] |
| | 0 | 132 | | if ($seq -is [System.Collections.IList]) { |
| | 0 | 133 | | for ($i = 0; $i -lt $seq.Count -and $i -lt $rawDatesAsStrings.Count; $i++) { |
| | 0 | 134 | | if ($seq[$i] -is [datetime] -or $seq[$i] -is [DateTimeOffset]) { |
| | 0 | 135 | | $seq[$i] = $rawDatesAsStrings[$i] |
| | | 136 | | } |
| | | 137 | | } |
| | | 138 | | } |
| | | 139 | | } |
| | 0 | 140 | | return $single |
| | | 141 | | } |
| | 0 | 142 | | $ret = @() |
| | 0 | 143 | | foreach ($i in $documents) { |
| | | 144 | | # Always preserve order in each document. |
| | 0 | 145 | | $val = [Kestrun.Utilities.Yaml.YamlTypeConverter]::ConvertYamlDocumentToPSObject($i.RootNode, $true) |
| | 0 | 146 | | $val = Convert-DateTimeOffsetToDateTime $val |
| | 0 | 147 | | if ($rawDatesAsStrings -and ($val -is [System.Collections.IDictionary]) -and ($val.Keys -contains 'datesAsSt |
| | 0 | 148 | | $seq = $val['datesAsStrings'] |
| | 0 | 149 | | if ($seq -is [System.Collections.IList]) { |
| | 0 | 150 | | for ($i2 = 0; $i2 -lt $seq.Count -and $i2 -lt $rawDatesAsStrings.Count; $i2++) { |
| | 0 | 151 | | if ($seq[$i2] -is [datetime] -or $seq[$i2] -is [DateTimeOffset]) { |
| | 0 | 152 | | $seq[$i2] = $rawDatesAsStrings[$i2] |
| | | 153 | | } |
| | | 154 | | } |
| | | 155 | | } |
| | | 156 | | } |
| | 0 | 157 | | $ret += $val |
| | | 158 | | } |
| | 0 | 159 | | return $ret |
| | | 160 | | } |
| | | 161 | | } |