Skip to content

Commit 0726eef

Browse files
Flow style and round tripping fixes
Signed-off-by: Gabriel Adrian Samfira <gsamfira@cloudbasesolutions.com>
1 parent 1c2111b commit 0726eef

18 files changed

Lines changed: 1611 additions & 108 deletions

.github/workflows/ci.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
strategy:
2020
fail-fast: false
2121
matrix:
22-
os: [windows-latest]
22+
os: [windows-2022, windows-latest]
2323

2424
steps:
2525
- uses: actions/checkout@v3
@@ -40,8 +40,8 @@ jobs:
4040
strategy:
4141
fail-fast: false
4242
matrix:
43-
os: [macos-latest, ubuntu-latest, windows-latest]
44-
43+
os: [macos-latest, ubuntu-latest, windows-2022, windows-latest]
44+
4545
steps:
4646
- uses: actions/checkout@v3
4747
- name: Install modules

README.md

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -14,37 +14,6 @@ This module is available for installation via [Powershell Gallery](http://www.po
1414
Install-Module powershell-yaml
1515
```
1616

17-
### Basic Usage (PowerShell 5.1+)
18-
19-
```powershell
20-
Import-Module powershell-yaml
21-
22-
$yaml = ConvertTo-Yaml @{"key" = "value"}
23-
$obj = ConvertFrom-Yaml $yaml
24-
```
25-
26-
### Typed Class Mode - v2 (PowerShell 7.0+)
27-
28-
For advanced features like typed class serialization with full metadata preservation:
29-
30-
```powershell
31-
Import-Module powershell-yaml
32-
33-
# v2 cmdlets are automatically available
34-
ConvertFrom-YamlTyped -Yaml $yaml -As ([MyClass])
35-
ConvertTo-YamlTyped -InputObject $obj
36-
```
37-
38-
**v2 Features:**
39-
40-
- Define YAML models as pure PowerShell classes inheriting from `YamlBase`
41-
- Full round-trip preservation of comments, quoting styles, and YAML tags
42-
- AssemblyLoadContext isolation prevents YamlDotNet version conflicts
43-
- Cmdlets: `ConvertFrom-YamlTyped`, `ConvertTo-YamlTyped`
44-
- Works on PowerShell 5.1+ (throws helpful error if used on <7.0)
45-
46-
See [EXAMPLES.md](EXAMPLES.md) for detailed usage examples of both modes.
47-
4817
## ConvertTo-Yaml
4918

5019
```powershell
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
#!/usr/bin/env pwsh
2+
# Copyright 2016-2026 Cloudbase Solutions Srl
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
5+
# not use this file except in compliance with the License. You may obtain
6+
# a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
# License for the specific language governing permissions and limitations
14+
# under the License.
15+
#
16+
# Test classes for deep nesting with custom converters
17+
18+
using namespace PowerShellYaml
19+
using namespace System
20+
using namespace System.Collections.Generic
21+
22+
# Custom type: IP Address (renamed to avoid conflict with System.Net.IPAddress)
23+
class CustomIPAddress {
24+
[byte]$Octet1 = 0
25+
[byte]$Octet2 = 0
26+
[byte]$Octet3 = 0
27+
[byte]$Octet4 = 0
28+
29+
CustomIPAddress() {
30+
$this.Octet1 = 0
31+
$this.Octet2 = 0
32+
$this.Octet3 = 0
33+
$this.Octet4 = 0
34+
}
35+
36+
[string] ToString() {
37+
return "$($this.Octet1).$($this.Octet2).$($this.Octet3).$($this.Octet4)"
38+
}
39+
}
40+
41+
# Custom converter for IP Address
42+
class IPAddressConverter : YamlConverter {
43+
[bool] CanHandle([string]$tag, [Type]$targetType) {
44+
return $targetType -eq [CustomIPAddress]
45+
}
46+
47+
[object] ConvertFromYaml([object]$data, [string]$tag, [Type]$targetType) {
48+
$ip = [CustomIPAddress]@{}
49+
50+
if ($data -is [string]) {
51+
# Parse string format: "192.168.1.1"
52+
$octets = $data -split '\.'
53+
if ($octets.Length -ne 4) {
54+
throw [FormatException]::new("Invalid IP address format: $data")
55+
}
56+
$ip.Octet1 = [byte]$octets[0]
57+
$ip.Octet2 = [byte]$octets[1]
58+
$ip.Octet3 = [byte]$octets[2]
59+
$ip.Octet4 = [byte]$octets[3]
60+
}
61+
elseif ($data -is [System.Collections.Generic.Dictionary[string, object]]) {
62+
# Parse dictionary format
63+
if ($data.ContainsKey('a')) { $ip.Octet1 = [byte]$data['a'] }
64+
if ($data.ContainsKey('b')) { $ip.Octet2 = [byte]$data['b'] }
65+
if ($data.ContainsKey('c')) { $ip.Octet3 = [byte]$data['c'] }
66+
if ($data.ContainsKey('d')) { $ip.Octet4 = [byte]$data['d'] }
67+
}
68+
69+
return $ip
70+
}
71+
72+
[object] ConvertToYaml([object]$value) {
73+
$ip = [CustomIPAddress]$value
74+
return @{
75+
Value = $ip.ToString()
76+
Tag = '!ipaddr'
77+
}
78+
}
79+
}
80+
81+
# Custom type: Duration (simple time span)
82+
class Duration {
83+
[int]$Hours = 0
84+
[int]$Minutes = 0
85+
[int]$Seconds = 0
86+
87+
Duration() {
88+
$this.Hours = 0
89+
$this.Minutes = 0
90+
$this.Seconds = 0
91+
}
92+
93+
[string] ToString() {
94+
return "$($this.Hours)h$($this.Minutes)m$($this.Seconds)s"
95+
}
96+
}
97+
98+
# Custom converter for Duration
99+
class DurationConverter : YamlConverter {
100+
[bool] CanHandle([string]$tag, [Type]$targetType) {
101+
return $targetType -eq [Duration]
102+
}
103+
104+
[object] ConvertFromYaml([object]$data, [string]$tag, [Type]$targetType) {
105+
$duration = [Duration]::new()
106+
107+
if ($data -is [string]) {
108+
# Parse string format: "2h30m15s"
109+
if ($data -match '^(\d+)h(\d+)m(\d+)s$') {
110+
$duration.Hours = [int]$Matches[1]
111+
$duration.Minutes = [int]$Matches[2]
112+
$duration.Seconds = [int]$Matches[3]
113+
}
114+
else {
115+
throw [FormatException]::new("Invalid duration format: $data")
116+
}
117+
}
118+
elseif ($data -is [System.Collections.Generic.Dictionary[string, object]]) {
119+
if ($data.ContainsKey('hours')) { $duration.Hours = [int]$data['hours'] }
120+
if ($data.ContainsKey('minutes')) { $duration.Minutes = [int]$data['minutes'] }
121+
if ($data.ContainsKey('seconds')) { $duration.Seconds = [int]$data['seconds'] }
122+
}
123+
124+
return $duration
125+
}
126+
127+
[object] ConvertToYaml([object]$value) {
128+
$duration = [Duration]$value
129+
return @{
130+
Value = $duration.ToString()
131+
Tag = '!duration'
132+
}
133+
}
134+
}
135+
136+
# Level 3: Server configuration (deepest level with converters)
137+
class ServerConfig : YamlBase {
138+
[string]$Hostname = ""
139+
140+
[YamlConverter("IPAddressConverter")]
141+
[CustomIPAddress]$Address = $null
142+
143+
[int]$Port = 0
144+
145+
[YamlConverter("DurationConverter")]
146+
[Duration]$Timeout = $null
147+
}
148+
149+
# Level 2: Database configuration (middle level with converters)
150+
class DatabaseConfig : YamlBase {
151+
[string]$Name = ""
152+
153+
[YamlConverter("IPAddressConverter")]
154+
[CustomIPAddress]$Host = $null
155+
156+
[int]$Port = 0
157+
158+
[ServerConfig]$PrimaryServer = $null
159+
[ServerConfig]$ReplicaServer = $null
160+
161+
[YamlConverter("DurationConverter")]
162+
[Duration]$ConnectionTimeout = $null
163+
}
164+
165+
# Level 1: Application configuration (top level)
166+
class ApplicationConfig : YamlBase {
167+
[string]$AppName = ""
168+
[string]$Environment = ""
169+
170+
[DatabaseConfig]$Database = $null
171+
172+
[YamlConverter("IPAddressConverter")]
173+
[CustomIPAddress]$ApiGateway = $null
174+
175+
[YamlConverter("DurationConverter")]
176+
[Duration]$RequestTimeout = $null
177+
}

Tests/MappingStyleClasses.ps1

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#!/usr/bin/env pwsh
2+
# Copyright 2016-2026 Cloudbase Solutions Srl
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
5+
# not use this file except in compliance with the License. You may obtain
6+
# a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
# License for the specific language governing permissions and limitations
14+
# under the License.
15+
#
16+
17+
using namespace PowerShellYaml
18+
19+
# Test classes for mapping style
20+
class Address : YamlBase {
21+
[string]$Street = ""
22+
[string]$City = ""
23+
[string]$Zip = ""
24+
}
25+
26+
class Person : YamlBase {
27+
[string]$Name = ""
28+
[int]$Age = 0
29+
[Address]$Address = $null
30+
}
31+
32+
class Company : YamlBase {
33+
[string]$Name = ""
34+
[Person]$Ceo = $null
35+
[Person[]]$Employees = @()
36+
}

Tests/TypedYamlTestClasses.ps1

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,3 +111,78 @@ class ComplexConfig : YamlBase {
111111
if ($data.ContainsKey('max-connections')) { $this.MaxConnections = [int]$data['max-connections'] }
112112
}
113113
}
114+
115+
class SimpleConfigWithArray : YamlBase {
116+
[string]$Name
117+
[int]$Port = 8080
118+
[string[]]$Tags = @()
119+
120+
[Dictionary[string, object]] ToDictionary() {
121+
$dict = [Dictionary[string, object]]::new()
122+
$dict['name'] = $this.Name
123+
$dict['port'] = $this.Port
124+
$dict['tags'] = $this.Tags
125+
return $dict
126+
}
127+
128+
[void] FromDictionary([Dictionary[string, object]]$data) {
129+
if ($data.ContainsKey('name')) { $this.Name = [string]$data['name'] }
130+
if ($data.ContainsKey('port')) { $this.Port = [int]$data['port'] }
131+
if ($data.ContainsKey('tags')) {
132+
$val = $data['tags']
133+
if ($val -eq $null) {
134+
$this.Tags = @()
135+
} else {
136+
$this.Tags = @($val)
137+
}
138+
}
139+
}
140+
}
141+
142+
class ServerInfo : YamlBase {
143+
[string]$Name
144+
[int]$Port
145+
146+
[Dictionary[string, object]] ToDictionary() {
147+
$dict = [Dictionary[string, object]]::new()
148+
$dict['name'] = $this.Name
149+
$dict['port'] = $this.Port
150+
return $dict
151+
}
152+
153+
[void] FromDictionary([Dictionary[string, object]]$data) {
154+
if ($data.ContainsKey('name')) { $this.Name = [string]$data['name'] }
155+
if ($data.ContainsKey('port')) { $this.Port = [int]$data['port'] }
156+
}
157+
}
158+
159+
class ConfigWithServers : YamlBase {
160+
[ServerInfo[]]$Servers = @()
161+
[DatabaseConfig]$Database
162+
163+
[Dictionary[string, object]] ToDictionary() {
164+
$dict = [Dictionary[string, object]]::new()
165+
$dict['servers'] = $this.Servers
166+
if ($this.Database) {
167+
$dict['database'] = $this.Database
168+
}
169+
return $dict
170+
}
171+
172+
[void] FromDictionary([Dictionary[string, object]]$data) {
173+
if ($data.ContainsKey('servers')) {
174+
$val = $data['servers']
175+
if ($val -is [System.Collections.IList]) {
176+
$this.Servers = @($val | ForEach-Object {
177+
$server = [ServerInfo]::new()
178+
$server.FromDictionary($_)
179+
$server
180+
})
181+
}
182+
}
183+
if ($data.ContainsKey('database')) {
184+
$this.Database = [DatabaseConfig]::new()
185+
$this.Database.FromDictionary($data['database'])
186+
}
187+
}
188+
}

0 commit comments

Comments
 (0)