Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,18 @@ $config.name # example
$config.ports[0] # 80
```

Mapping and sequence documents render themselves as YAML through `ToString()`, so a
parsed value can be shown in its source notation:

```powershell
$config.ToString()
# "name": "example"
# "enabled": true
# "ports":
# - 80
# - 443
```

Use `-AsHashtable` for insertion-ordered dictionaries and mappings with complex,
non-string, empty, or case-colliding keys, and `-NoEnumerate` to keep a top-level
sequence as one pipeline record. Every document in a multi-document stream is
Expand Down Expand Up @@ -220,6 +232,8 @@ console:
Get-Help -Name ConvertFrom-Yaml -Examples
```

Normative specifications for planned capabilities live in [spec](spec/README.md).

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md).
54 changes: 54 additions & 0 deletions src/functions/private/Conversion/Add-YamlToStringMember.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
锘縡unction Add-YamlToStringMember {
<#
.SYNOPSIS
Tags a projected YAML document root so ToString renders YAML text.

.DESCRIPTION
Adds the PSModule.Yaml.Document type name and a ToString script method to a
projected mapping or sequence so callers can render the value back to YAML
text with ToString. Scalars are left untouched because overriding ToString on
a string, number, or date would change how ordinary values convert to text.

Rendering is deferred until ToString runs, so decorating a document costs one
member addition and never serializes eagerly. The script method reports the
value's current state, which means edits made after parsing are reflected.

.EXAMPLE
Add-YamlToStringMember -Value ([pscustomobject]@{ name = 'Ada' })

Tags the mapping so ToString returns "name": "Ada" instead of an empty string.

.LINK
https://psmodule.io/Yaml/Functions/Conversion/ConvertFrom-Yaml/
#>
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
'PSUseShouldProcessForStateChangingFunctions', '',
Justification = 'Decorates an in-memory projection result.'
)]
[CmdletBinding()]
[OutputType([void])]
param (
# The projected document value to decorate; scalars and null are ignored.
[Parameter(Mandatory)]
[AllowNull()]
[object] $Value
)

if (-not (Test-YamlDocumentSurface -Value $Value)) {
return
}

try {
$Value.PSObject.TypeNames.Insert(0, 'PSModule.Yaml.Document')
Add-Member -InputObject $Value -MemberType ScriptMethod -Name 'ToString' -Force `
-ErrorAction Stop -Value {
try {
(ConvertTo-Yaml -InputObject $this).TrimEnd("`n")
} catch {
$this.PSObject.BaseObject.GetType().FullName
}
}
} catch {
Write-Debug "Add-YamlToStringMember skipped a document root: $($_.Exception.Message)"
}
}
48 changes: 48 additions & 0 deletions src/functions/private/Conversion/Test-YamlDocumentSurface.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
锘縡unction Test-YamlDocumentSurface {
<#
.SYNOPSIS
Tests whether a projected value can carry a YAML ToString member.

.DESCRIPTION
Reports whether a projected value is a mapping or a sequence, which are the
shapes that gain a readable YAML rendering from ToString. Scalars, null, and
binary values are rejected so the module never changes how an ordinary string,
number, date, or byte array converts to text.

.EXAMPLE
Test-YamlDocumentSurface -Value ([pscustomobject]@{ name = 'Ada' })

Returns true because a mapping renders as a YAML block.

.EXAMPLE
Test-YamlDocumentSurface -Value 42

Returns false because a scalar keeps its own ToString.

.LINK
https://psmodule.io/Yaml/Functions/Conversion/ConvertFrom-Yaml/
#>
[CmdletBinding()]
[OutputType([bool])]
param (
# The projected value whose YAML rendering eligibility is tested.
[Parameter(Mandatory)]
[AllowNull()]
[object] $Value
)

if ($null -eq $Value -or $Value -is [System.DBNull]) {
return $false
}
if ($Value -is [System.Management.Automation.PSCustomObject]) {
return $true
}
if ($Value -is [System.Collections.IDictionary]) {
return $true
}
if ($Value -is [byte[]] -or $Value -is [string] -or $Value -is [char]) {
return $false
}

$Value -is [System.Collections.IEnumerable]
}
33 changes: 33 additions & 0 deletions src/functions/public/Conversion/Conversion.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,39 @@ One object is emitted per document, in document order. An empty document emits
`-NoEnumerate` applies per document, so a stream of two sequence documents writes
two records instead of one per item.

### Rendering a parsed value back to YAML

Parsed mappings and sequences carry the type name `PSModule.Yaml.Document` and a
`ToString()` that renders the value as YAML text, so a value can be inspected in
its source notation without calling `ConvertTo-Yaml` explicitly:

```powershell
$config = @'
name: example
ports: [80, 443]
'@ | ConvertFrom-Yaml

$config.ToString()
# "name": "example"
# "ports":
# - 80
# - 443

"$config" # same text through string interpolation
$config.name # example - property access is unchanged
```

`ToString()` reports the value's current state, so edits made after parsing appear
in the rendered text. Rendering is equivalent to `ConvertTo-Yaml`, which means it
describes the constructed value rather than the original source text: comments,
anchors, and the original scalar styles are not part of the output. Use
`Format-Yaml` when those representation details must survive.

Only mappings and sequences are decorated. Scalar documents keep their own
`ToString()`, so a parsed number, string, or date still converts to text the way
that type normally does. Nested values are not decorated either, because only the
document root is a document.

### Anchors and aliases

An alias to a collection projects to the **same object instance**, in both
Expand Down
11 changes: 11 additions & 0 deletions src/functions/public/Conversion/ConvertFrom-Yaml.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,20 @@ function ConvertFrom-Yaml {
Pipeline strings are joined with a line feed and parsed as one stream,
which supports Get-Content. Each YAML document is written separately.

Mapping and sequence documents carry the PSModule.Yaml.Document type name and
a ToString that renders the value as YAML text, so a parsed value can be shown
in its source notation. Scalar documents keep their own ToString.

.EXAMPLE
'name: Ada' | ConvertFrom-Yaml

Converts one mapping to a PSCustomObject.

.EXAMPLE
('name: Ada' | ConvertFrom-Yaml).ToString()

Renders the parsed mapping back to YAML text.

.EXAMPLE
Get-Content -Path '.\config.yaml' | ConvertFrom-Yaml -AsHashtable

Expand Down Expand Up @@ -117,9 +126,11 @@ function ConvertFrom-Yaml {

if ($isTopLevelSequence -and -not $NoEnumerate) {
foreach ($item in $value) {
Add-YamlToStringMember -Value $item
$PSCmdlet.WriteObject($item, $false)
}
} else {
Add-YamlToStringMember -Value $value
$PSCmdlet.WriteObject($value, $false)
}
}
Expand Down
105 changes: 105 additions & 0 deletions tests/ConvertFrom-Yaml.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -690,3 +690,108 @@ negativeZero: -0.0
{ ConvertFrom-Yaml -Yaml ("value: x{0}" -f [char] 0xD800) } | Should -Throw
}
}

Describe 'ConvertFrom-Yaml YAML document rendering' {
It 'renders a mapping back to YAML text with ToString' {
$result = ConvertFrom-Yaml -Yaml "name: Ada`nage: 36"

$result.ToString() | Should -Be "`"name`": `"Ada`"`n`"age`": 36"
}

It 'renders the same text through string interpolation' {
$result = ConvertFrom-Yaml -Yaml 'name: Ada'

"$result" | Should -Be $result.ToString()
}

It 'round-trips the rendered text back to an equal value' {
$result = ConvertFrom-Yaml -Yaml "name: Ada`nports:`n - 80`n - 443"
$roundTrip = ConvertFrom-Yaml -Yaml $result.ToString()

$roundTrip.name | Should -Be 'Ada'
$roundTrip.ports | Should -Be @(80, 443)
$roundTrip.ToString() | Should -Be $result.ToString()
}

It 'tags rendered document roots with the document type name' {
$result = ConvertFrom-Yaml -Yaml 'name: Ada'

$result.PSObject.TypeNames | Should -Contain 'PSModule.Yaml.Document'
}

It 'keeps property access, indexing, and formatting unchanged' {
$result = ConvertFrom-Yaml -Yaml "name: Ada`nports: [80, 443]`nnested:`n key: value"

$result.name | Should -Be 'Ada'
$result.ports[0] | Should -Be 80
$result.nested.key | Should -Be 'value'
@($result.PSObject.Properties.Name) | Should -Be @('name', 'ports', 'nested')
}

It 'renders ordered dictionaries produced by AsHashtable' {
$result = ConvertFrom-Yaml -Yaml 'name: Ada' -AsHashtable

$result | Should -BeOfType ([System.Collections.Specialized.OrderedDictionary])
$result.ToString() | Should -Be '"name": "Ada"'
}

It 'renders a sequence document held together by NoEnumerate' {
$result = ConvertFrom-Yaml -Yaml "- 1`n- 2" -NoEnumerate

$result.ToString() | Should -Be "- 1`n- 2"
}

It 'renders each enumerated top-level sequence item' {
$results = @(ConvertFrom-Yaml -Yaml "- name: Ada`n- name: Grace")

$results.Count | Should -Be 2
$results[0].ToString() | Should -Be '"name": "Ada"'
$results[1].ToString() | Should -Be '"name": "Grace"'
}

It 'renders each document of a multi-document stream separately' {
$results = @(ConvertFrom-Yaml -Yaml "name: Ada`n---`nname: Grace")

$results.Count | Should -Be 2
$results[0].ToString() | Should -Be '"name": "Ada"'
$results[1].ToString() | Should -Be '"name": "Grace"'
}

It 'reflects edits made after parsing' {
$result = ConvertFrom-Yaml -Yaml 'name: Ada'
$result.name = 'Grace'

$result.ToString() | Should -Be '"name": "Grace"'
}

It 'leaves scalar documents converting to text as themselves' -ForEach @(
@{ Yaml = '42'; Expected = '42' }
@{ Yaml = 'true'; Expected = 'True' }
@{ Yaml = 'plain text'; Expected = 'plain text' }
) {
(ConvertFrom-Yaml -Yaml $Yaml).ToString() | Should -Be $Expected
}

It 'leaves an empty document as null' {
ConvertFrom-Yaml -Yaml '---' | Should -BeNullOrEmpty
}

It 'does not emit the rendering members as YAML content' {
$result = ConvertFrom-Yaml -Yaml 'name: Ada'
$yaml = ConvertTo-Yaml -InputObject $result

$yaml | Should -Not -Match 'ToString'
$yaml | Should -Not -Match 'PSModule\.Yaml\.Document'
}

It 'renders documents parsed from a file by Import-Yaml' {
$path = Join-Path ([System.IO.Path]::GetTempPath()) "yaml-tostring-$([guid]::NewGuid()).yaml"
try {
Set-Content -LiteralPath $path -Value "name: Ada" -Encoding utf8
(Import-Yaml -Path $path).ToString() | Should -Be '"name": "Ada"'
} finally {
Remove-Item -LiteralPath $path -Force -ErrorAction SilentlyContinue
}
}
}

Loading