Public/Yaml/ConvertFrom-KrYaml.ps1

# Portions derived from PowerShell-Yaml (https://github.com/cloudbase/powershell-yaml)
# Copyright (c) 2016–2024 Cloudbase Solutions Srl
# Licensed under the Apache License, Version 2.0 (Apache-2.0).
# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
# Modifications Copyright (c) 2025 Kestrun Contributors

<#
.SYNOPSIS
    Converts a YAML string to a PowerShell object or hashtable.
.DESCRIPTION
    The ConvertFrom-KrYaml cmdlet converts a YAML string to a PowerShell object or
    hashtable. By default, it returns a PSCustomObject, but you can specify the
    -AsHashtable switch to get a hashtable instead.
.PARAMETER Yaml
    The YAML string to convert. This parameter is mandatory and accepts input from the pipeline.
.PARAMETER AllDocuments
    If specified, all documents in a multi-document YAML stream will be returned as an array. By default, only the first document is returned.
.PARAMETER UseMergingParser
    If specified, the YAML parser will support the merging key (<<) for merging mappings.
    This is useful for YAML documents that use anchors and aliases to merge mappings.
.EXAMPLE
    $yaml = @"
    name: John
    age: 30
    skills:
      - PowerShell
      - YAML
    "@
    $obj = $yaml | ConvertFrom-KrYaml
    # Outputs a PSCustomObject with properties Name, Age, and Skills.
.EXAMPLE
    $yaml = @"
    ---
    name: John
    age: 30
    ---
    name: Jane Doe
    age: 25
    "@
    $objs = $yaml | ConvertFrom-KrYaml -AllDocuments
    # Outputs an array of two PSCustomObjects, one for each document in the YAML stream.
.EXAMPLE
    $yaml = @"
    defaults: &defaults
      adapter: postgres
      host: localhost
    development:
      database: dev_db
      <<: *defaults
    test:
      database: test_db
      <<: *defaults
    "@
    $obj = $yaml | ConvertFrom-KrYaml -UseMergingParser
    # Outputs a PSCustomObject with merged properties for 'development' and 'test' sections
    # using the 'defaults' anchor.
    $obj | Format-List
.NOTES
    This cmdlet requires PowerShell 7.0 or later.
    It uses the Kestrun.Utilities.Yaml library for YAML deserialization.
#>

function ConvertFrom-KrYaml {
    [KestrunRuntimeApi('Everywhere')]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $false, ValueFromPipeline = $true, Position = 0)]
        [string]$Yaml,
        [switch]$AllDocuments = $false,
        [switch]$UseMergingParser = $false
    )

    begin {
        $builder = [System.Text.StringBuilder]::new()
    }
    process {
        if ($Yaml -is [string]) {
            if ($builder.Length -gt 0) {
                [void]$builder.Append("`n")
            }
            [void]$builder.Append($Yaml)
        }
    }

    end {
        $d = $builder.ToString()
        if ([string]::IsNullOrEmpty($d)) {
            return
        }
        $yamlStream = [Kestrun.Utilities.Yaml.YamlLoader]::GetYamlDocuments($d, $UseMergingParser)
        $documents = $yamlStream.Documents
        if (!$documents -or !$documents.Count) {
            return
        }
        $firstRoot = $documents[0].RootNode

        # Extract datesAsStrings values (if present) from the parsed YAML object BEFORE conversion so we can restore them as strings
        $rawDatesAsStrings = $null
        if ($firstRoot -is [System.Collections.IDictionary] -and $firstRoot.Keys -contains 'datesAsStrings') {
            $seq = $firstRoot['datesAsStrings']
            if ($seq -is [System.Collections.IList]) {
                # Collect the original values as strings, preserving their lexical form
                $rawDatesAsStrings = @()
                foreach ($item in $seq) {
                    $rawDatesAsStrings += $item.ToString()
                }
            }
        }

        if ($documents.Count -eq 1 -and -not $AllDocuments) {
            # Always request ordered conversion so original key order from YAML is preserved by default.
            $single = [Kestrun.Utilities.Yaml.YamlTypeConverter]::ConvertYamlDocumentToPSObject($firstRoot, $true)
            $single = Convert-DateTimeOffsetToDateTime $single
            if ($rawDatesAsStrings -and ($single -is [System.Collections.IDictionary]) -and ($single.Keys -contains 'datesAsStrings')) {
                $seq = $single['datesAsStrings']
                if ($seq -is [System.Collections.IList]) {
                    for ($i = 0; $i -lt $seq.Count -and $i -lt $rawDatesAsStrings.Count; $i++) {
                        # Only replace if parser turned it into DateTime
                        if ($seq[$i] -is [datetime] -or $seq[$i] -is [DateTimeOffset]) {
                            $seq[$i] = $rawDatesAsStrings[$i]
                        }
                    }
                }
            }
            return $single
        }
        if (-not $AllDocuments) {
            # Always request ordered conversion so original key order from YAML is preserved by default.
            $single = [Kestrun.Utilities.Yaml.YamlTypeConverter]::ConvertYamlDocumentToPSObject($firstRoot, $true)
            $single = Convert-DateTimeOffsetToDateTime $single
            if ($rawDatesAsStrings -and ($single -is [System.Collections.IDictionary]) -and ($single.Keys -contains 'datesAsStrings')) {
                $seq = $single['datesAsStrings']
                if ($seq -is [System.Collections.IList]) {
                    for ($i = 0; $i -lt $seq.Count -and $i -lt $rawDatesAsStrings.Count; $i++) {
                        if ($seq[$i] -is [datetime] -or $seq[$i] -is [DateTimeOffset]) {
                            $seq[$i] = $rawDatesAsStrings[$i]
                        }
                    }
                }
            }
            return $single
        }
        $ret = @()
        foreach ($i in $documents) {
            # Always preserve order in each document.
            $val = [Kestrun.Utilities.Yaml.YamlTypeConverter]::ConvertYamlDocumentToPSObject($i.RootNode, $true)
            $val = Convert-DateTimeOffsetToDateTime $val
            if ($rawDatesAsStrings -and ($val -is [System.Collections.IDictionary]) -and ($val.Keys -contains 'datesAsStrings')) {
                $seq = $val['datesAsStrings']
                if ($seq -is [System.Collections.IList]) {
                    for ($i2 = 0; $i2 -lt $seq.Count -and $i2 -lt $rawDatesAsStrings.Count; $i2++) {
                        if ($seq[$i2] -is [datetime] -or $seq[$i2] -is [DateTimeOffset]) {
                            $seq[$i2] = $rawDatesAsStrings[$i2]
                        }
                    }
                }
            }
            $ret += $val
        }
        return $ret
    }
}