diff --git a/lib/netstandard2.0/PowerShellYamlSerializer.dll b/lib/netstandard2.0/PowerShellYamlSerializer.dll index a27bb44..25b37f7 100644 Binary files a/lib/netstandard2.0/PowerShellYamlSerializer.dll and b/lib/netstandard2.0/PowerShellYamlSerializer.dll differ diff --git a/powershell-yaml.psm1 b/powershell-yaml.psm1 index 684548f..dfecf6a 100644 --- a/powershell-yaml.psm1 +++ b/powershell-yaml.psm1 @@ -123,6 +123,9 @@ function Convert-ValueToProperType { $intTypes = @([int], [long]) if ([string]::IsNullOrEmpty($Node.Tag) -eq $false) { switch ($Node.Tag) { + '!' { + return $Node.Value + } 'tag:yaml.org,2002:str' { return $Node.Value } @@ -310,6 +313,20 @@ function Convert-OrderedHashtableToDictionary { return $Data } +function Convert-GenericOrderedDictionaryToOrderedDictionary { + param( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [System.Object]$Data + ) + # Convert System.Collections.Generic ordered dictionaries to System.Collections.Specialized.OrderedDictionary + # to preserve key order when serializing with YamlDotNet + $ordered = [System.Collections.Specialized.OrderedDictionary]::new() + foreach ($key in $Data.Keys) { + $ordered[$key] = Convert-PSObjectToGenericObject $Data[$key] + } + return $ordered +} + function Convert-ListToGenericList { param( [Parameter(Mandatory = $false, ValueFromPipeline = $true)] @@ -333,13 +350,38 @@ function Convert-PSObjectToGenericObject { } $dataType = $data.GetType() + + # Check for OrderedDictionary types first (before generic IDictionary check) if (([System.Collections.Specialized.OrderedDictionary].IsAssignableFrom($dataType))) { return Convert-OrderedHashtableToDictionary $data - } elseif (([System.Collections.IDictionary].IsAssignableFrom($dataType))) { + } + + # Check for System.Collections.Generic ordered dictionary types + # These need to be converted to OrderedDictionary to preserve key order in YamlDotNet + if ($dataType.IsGenericType) { + $genericDef = $dataType.GetGenericTypeDefinition() + $genericName = $genericDef.FullName + + # Handle System.Collections.Generic.OrderedDictionary + if ($genericName -eq 'System.Collections.Generic.OrderedDictionary`2') { + return Convert-GenericOrderedDictionaryToOrderedDictionary $data + } + + # Handle System.Collections.Generic.SortedDictionary + if ($genericName -eq 'System.Collections.Generic.SortedDictionary`2') { + return Convert-GenericOrderedDictionaryToOrderedDictionary $data + } + } + + # Generic IDictionary handling (for Hashtable, Dictionary, etc.) + if (([System.Collections.IDictionary].IsAssignableFrom($dataType))) { return Convert-HashtableToDictionary $data - } elseif (([System.Collections.IList].IsAssignableFrom($dataType))) { + } + + if (([System.Collections.IList].IsAssignableFrom($dataType))) { return Convert-ListToGenericList $data } + return $data } @@ -357,9 +399,7 @@ function ConvertFrom-Yaml { $d = '' } process { - if ($Yaml -is [string]) { - $d += $Yaml + "`n" - } + $d += $Yaml + "`n" } end { @@ -435,8 +475,7 @@ function ConvertTo-Yaml { [Parameter(ParameterSetName = 'NoOptions')] [switch]$JsonCompatible, - [switch]$UseFlowStyle, - + [switch]$KeepArray, [switch]$Force @@ -450,7 +489,7 @@ function ConvertTo-Yaml { } } end { - if ($d -eq $null -or $d.Count -eq 0) { + if ($null -eq $d -or $d.Count -eq 0) { return } if ($d.Count -eq 1 -and !($KeepArray)) { @@ -465,11 +504,8 @@ function ConvertTo-Yaml { if ((Test-Path $OutFile) -and !$Force) { throw 'Target file already exists. Use -Force to overwrite.' } - $wrt = New-Object 'System.IO.StreamWriter' $OutFile - } else { - $wrt = New-Object 'System.IO.StringWriter' } - + if ($PSCmdlet.ParameterSetName -eq 'NoOptions') { $Options = 0 if ($JsonCompatible) { @@ -478,18 +514,25 @@ function ConvertTo-Yaml { } } + if ($OutFile) { + $wrt = New-Object 'System.IO.StreamWriter' $OutFile + } else { + $wrt = New-Object 'System.IO.StringWriter' + } + try { $serializer = Get-Serializer $Options $serializer.Serialize($wrt, $norm) - } catch { - $_ + + if ($OutFile) { + return + } else { + return $wrt.ToString() + } } finally { - $wrt.Close() - } - if ($OutFile) { - return - } else { - return $wrt.ToString() + if ($null -ne $wrt) { + $wrt.Dispose() + } } } } diff --git a/src/PowerShellYamlSerializer.cs b/src/PowerShellYamlSerializer.cs index dbb4fb2..b0f9433 100644 --- a/src/PowerShellYamlSerializer.cs +++ b/src/PowerShellYamlSerializer.cs @@ -33,6 +33,26 @@ public override bool EnterMapping(IObjectDescriptor key, IObjectDescriptor value } } +internal static class PSObjectHelper { + /// + /// Unwraps a PSObject to its BaseObject if the BaseObject is not a PSCustomObject. + /// + /// The object to potentially unwrap + /// The type of the unwrapped object + /// The unwrapped object if it was a PSObject wrapping a non-PSCustomObject, otherwise the original object + public static object UnwrapIfNeeded(object obj, out Type unwrappedType) { + if (obj is PSObject psObj && psObj.BaseObject != null) { + var baseType = psObj.BaseObject.GetType(); + if (baseType != typeof(System.Management.Automation.PSCustomObject)) { + unwrappedType = baseType; + return psObj.BaseObject; + } + } + unwrappedType = obj?.GetType(); + return obj; + } +} + public class BigIntegerTypeConverter : IYamlTypeConverter { public bool Accepts(Type type) { return typeof(BigInteger).IsAssignableFrom(type); @@ -76,7 +96,7 @@ public void WriteYaml(IEmitter emitter, object value, Type type, ObjectSerialize emitter.Emit(new MappingStart(AnchorName.Empty, TagName.Empty, true, mappingStyle)); foreach (DictionaryEntry entry in hObj) { if(entry.Value == null) { - if (this.omitNullValues == true) { + if (this.omitNullValues) { continue; } serializer(entry.Key, entry.Key.GetType()); @@ -84,18 +104,8 @@ public void WriteYaml(IEmitter emitter, object value, Type type, ObjectSerialize continue; } serializer(entry.Key, entry.Key.GetType()); - var objType = entry.Value.GetType(); - var val = entry.Value; - if (entry.Value is PSObject nestedObj) { - var nestedType = nestedObj.BaseObject.GetType(); - if (nestedType != typeof(System.Management.Automation.PSCustomObject)) { - objType = nestedObj.BaseObject.GetType(); - val = nestedObj.BaseObject; - } - serializer(val, objType); - } else { - serializer(entry.Value, entry.Value.GetType()); - } + var unwrapped = PSObjectHelper.UnwrapIfNeeded(entry.Value, out var unwrappedType); + serializer(unwrapped, unwrappedType); } emitter.Emit(new MappingEnd()); } @@ -124,7 +134,9 @@ public object ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeseria public void WriteYaml(IEmitter emitter, object value, Type type, ObjectSerializer serializer) { var psObj = (PSObject)value; - if (!typeof(IDictionary).IsAssignableFrom(psObj.BaseObject.GetType()) && !typeof(PSCustomObject).IsAssignableFrom(psObj.BaseObject.GetType())) { + if (psObj.BaseObject != null && + !typeof(IDictionary).IsAssignableFrom(psObj.BaseObject.GetType()) && + !typeof(PSCustomObject).IsAssignableFrom(psObj.BaseObject.GetType())) { serializer(psObj.BaseObject, psObj.BaseObject.GetType()); return; } @@ -132,24 +144,15 @@ public void WriteYaml(IEmitter emitter, object value, Type type, ObjectSerialize emitter.Emit(new MappingStart(AnchorName.Empty, TagName.Empty, true, mappingStyle)); foreach (var prop in psObj.Properties) { if (prop.Value == null) { - if (this.omitNullValues == true) { + if (this.omitNullValues) { continue; } serializer(prop.Name, prop.Name.GetType()); emitter.Emit(new Scalar(AnchorName.Empty, "tag:yaml.org,2002:null", "", ScalarStyle.Plain, true, false)); } else { serializer(prop.Name, prop.Name.GetType()); - var objType = prop.Value.GetType(); - var val = prop.Value; - if (prop.Value is PSObject nestedPsObj) { - var nestedType = nestedPsObj.BaseObject.GetType(); - if (nestedType != typeof(System.Management.Automation.PSCustomObject)) { - objType = nestedPsObj.BaseObject.GetType(); - val = nestedPsObj.BaseObject; - } - } - serializer(val, objType); - + var unwrapped = PSObjectHelper.UnwrapIfNeeded(prop.Value, out var unwrappedType); + serializer(unwrapped, unwrappedType); } } emitter.Emit(new MappingEnd()); @@ -197,7 +200,7 @@ public override void Emit(MappingStartEventInfo eventInfo, IEmitter emitter) { public override void Emit(SequenceStartEventInfo eventInfo, IEmitter emitter){ eventInfo.Style = SequenceStyle.Flow; - nextEmitter.Emit(eventInfo, emitter); + base.Emit(eventInfo, emitter); } } @@ -206,11 +209,11 @@ public FlowStyleSequenceEmitter(IEventEmitter next): base(next) {} public override void Emit(SequenceStartEventInfo eventInfo, IEmitter emitter){ eventInfo.Style = SequenceStyle.Flow; - nextEmitter.Emit(eventInfo, emitter); + base.Emit(eventInfo, emitter); } } -class BuilderUtils { +public class BuilderUtils { public static SerializerBuilder BuildSerializer( SerializerBuilder builder, bool omitNullValues = false, @@ -218,7 +221,7 @@ public static SerializerBuilder BuildSerializer( bool useSequenceFlowStyle = false, bool jsonCompatible = false) { - if (jsonCompatible == true) { + if (jsonCompatible) { useFlowStyle = true; useSequenceFlowStyle = true; } @@ -228,14 +231,14 @@ public static SerializerBuilder BuildSerializer( .WithTypeConverter(new BigIntegerTypeConverter()) .WithTypeConverter(new IDictionaryTypeConverter(omitNullValues, useFlowStyle)) .WithTypeConverter(new PSObjectTypeConverter(omitNullValues, useFlowStyle)); - if (omitNullValues == true) { + if (omitNullValues) { builder = builder .WithEmissionPhaseObjectGraphVisitor(args => new NullValueGraphVisitor(args.InnerVisitor)); } - if (useFlowStyle == true) { + if (useFlowStyle) { builder = builder.WithEventEmitter(next => new FlowStyleAllEmitter(next)); } - if (useSequenceFlowStyle == true) { + if (useSequenceFlowStyle) { builder = builder.WithEventEmitter(next => new FlowStyleSequenceEmitter(next)); }