diff --git a/.gitignore b/.gitignore index ae3e075..f65fb06 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ src/obj src/bin +src/*/obj +src/*/bin diff --git a/build.ps1 b/build.ps1 index b5e850c..c3f8ca2 100644 --- a/build.ps1 +++ b/build.ps1 @@ -15,13 +15,22 @@ $here = Split-Path -Parent $MyInvocation.MyCommand.Path -dotnet build --configuration Release $here/src/ +dotnet build --configuration Release $here/src/PowerShellYamlSerialization +dotnet build --configuration Release $here/src/PowerShellYamlTypes -$destinations = @("netstandard2.1", "net47") +function Copy-Assemblies { + param( + $module + ) -foreach ($item in $destinations) { - $src = Join-Path $here "src" "bin" "Release" $item "PowerShellYamlSerializer.dll" - $dst = Join-Path $here "lib" $item "PowerShellYamlSerializer.dll" + $destinations = @("netstandard2.1", "net47") + foreach ($item in $destinations) { + $src = Join-Path $here "src" $module "bin" "Release" $item "$module.dll" + $dst = Join-Path $here "lib" $item "$module.dll" - Copy-Item -Force $src $dst + Copy-Item -Force $src $dst + } } + +Copy-Assemblies -module "PowerShellYamlSerialization" +Copy-Assemblies -module "PowerShellYamlTypes" diff --git a/lib/net47/PowerShellYamlSerialization.dll b/lib/net47/PowerShellYamlSerialization.dll new file mode 100644 index 0000000..872fa9a Binary files /dev/null and b/lib/net47/PowerShellYamlSerialization.dll differ diff --git a/lib/net47/PowerShellYamlTypes.dll b/lib/net47/PowerShellYamlTypes.dll new file mode 100644 index 0000000..fa56ae4 Binary files /dev/null and b/lib/net47/PowerShellYamlTypes.dll differ diff --git a/lib/netstandard2.1/PowerShellYamlSerialization.dll b/lib/netstandard2.1/PowerShellYamlSerialization.dll new file mode 100644 index 0000000..4df47d2 Binary files /dev/null and b/lib/netstandard2.1/PowerShellYamlSerialization.dll differ diff --git a/lib/netstandard2.1/PowerShellYamlSerializer.dll b/lib/netstandard2.1/PowerShellYamlSerializer.dll deleted file mode 100644 index 605ed3f..0000000 Binary files a/lib/netstandard2.1/PowerShellYamlSerializer.dll and /dev/null differ diff --git a/lib/netstandard2.1/PowerShellYamlTypes.dll b/lib/netstandard2.1/PowerShellYamlTypes.dll new file mode 100644 index 0000000..b5058d8 Binary files /dev/null and b/lib/netstandard2.1/PowerShellYamlTypes.dll differ diff --git a/powershell-yaml.psd1 b/powershell-yaml.psd1 index f4cb887..c0d53a2 100644 --- a/powershell-yaml.psd1 +++ b/powershell-yaml.psd1 @@ -52,7 +52,7 @@ Description = 'Powershell module for serializing and deserializing YAML' PowerShellVersion = '5.0' # Functions to export from this module -FunctionsToExport = "ConvertTo-Yaml","ConvertFrom-Yaml" +FunctionsToExport = "ConvertTo-Yaml","ConvertFrom-Yaml","ConvertFrom-YamlToClass" AliasesToExport = "cfy","cty" } diff --git a/powershell-yaml.psm1 b/powershell-yaml.psm1 index 5d8a75d..f413e14 100644 --- a/powershell-yaml.psm1 +++ b/powershell-yaml.psm1 @@ -33,9 +33,11 @@ function Invoke-LoadFile { ) $global:powershellYamlDotNetAssemblyPath = Join-Path $assemblyPath "YamlDotNet.dll" - $serializerAssemblyPath = Join-Path $assemblyPath "PowerShellYamlSerializer.dll" + $serializerAssemblyPath = Join-Path $assemblyPath "PowerShellYamlSerialization.dll" $yamlAssembly = [Reflection.Assembly]::LoadFile($powershellYamlDotNetAssemblyPath) $serializerAssembly = [Reflection.Assembly]::LoadFile($serializerAssemblyPath) + $yamlTypes = Join-Path $assemblyPath "PowerShellYamlTypes.dll" + [Reflection.Assembly]::LoadFrom($yamlTypes) | Out-Null if ($PSVersionTable['PSEdition'] -eq 'Core') { # Register the AssemblyResolve event to load dependencies manually. This seems to be needed only on @@ -43,16 +45,16 @@ function Invoke-LoadFile { [System.AppDomain]::CurrentDomain.add_AssemblyResolve({ param ($sender, $e) $pth = $powershellYamlDotNetAssemblyPath - # Load YamlDotNet if it's requested by PowerShellYamlSerializer. Ignore other requests as they might + # Load YamlDotNet if it's requested by PowerShellYamlSerialization. Ignore other requests as they might # originate from other assemblies that are not part of this module and which might have different # versions of the module that they need to load. - if ($e.Name -like "*YamlDotNet*" -and $e.RequestingAssembly -like "*PowerShellYamlSerializer*" ) { + if ($e.Name -like "*YamlDotNet*" -and $e.RequestingAssembly -like "*PowerShellYamlSerialization*" ) { return [System.Reflection.Assembly]::LoadFile($powershellYamlDotNetAssemblyPath) } return $null }) - # Load the StringQuotingEmitter from PowerShellYamlSerializer to force the resolver handler to fire once. + # Load the StringQuotingEmitter from PowerShellYamlSerialization to force the resolver handler to fire once. # This will load the YamlDotNet assembly and expand the global variable $powershellYamlDotNetAssemblyPath. # We then remove it to avoid polluting the global scope. # This is an ugly hack I am not happy with. @@ -344,6 +346,132 @@ function Convert-PSObjectToGenericObject { return $data } +function Get-Serializer { + Param( + [Parameter(Mandatory=$true)][SerializationOptions]$Options + ) + + $builder = $yamlDotNetAssembly.GetType("YamlDotNet.Serialization.SerializerBuilder")::new() + + if ($Options.HasFlag([SerializationOptions]::Roundtrip)) { + $builder = $builder.EnsureRoundtrip() + } + if ($Options.HasFlag([SerializationOptions]::DisableAliases)) { + $builder = $builder.DisableAliases() + } + if ($Options.HasFlag([SerializationOptions]::EmitDefaults)) { + $builder = $builder.EmitDefaults() + } + if ($Options.HasFlag([SerializationOptions]::JsonCompatible)) { + $builder = $builder.JsonCompatible() + } + if ($Options.HasFlag([SerializationOptions]::DefaultToStaticType)) { + $resolver = $yamlDotNetAssembly.GetType("YamlDotNet.Serialization.TypeResolvers.StaticTypeResolver")::new() + $builder = $builder.WithTypeResolver($resolver) + } + if ($Options.HasFlag([SerializationOptions]::WithIndentedSequences)) { + $builder = $builder.WithIndentedSequences() + } + + $omitNull = $Options.HasFlag([SerializationOptions]::OmitNullValues) + + $stringQuoted = $stringQuotedAssembly.GetType("StringQuotingEmitter") + $util = $stringQuotedAssembly.GetType("BuilderUtils") + $builder = $util::BuildSerializer($builder, $omitNull) + + return $builder.Build() +} + +function Resolve-AttributeOverrides { + Param( + $deserializerBuilder, + [System.Type]$theType + ) + + foreach ($attr in $theType.GetProperties()) { + $custom = $attr.GetCustomAttributes($true) + if ($custom -eq $null) { + continue + } + $shouldRecurse = $false + foreach ($customAttr in $custom) { + switch($customAttr.TypeId.Name) { + "PowerShellYamlPropertyAliasAttribute" { + if ($customAttr.YamlName -eq "") { + break + } + $alias = $yamlDotNetAssembly.GetType("YamlDotNet.Serialization.YamlMemberAttribute")::new() + $alias.Alias = $custom.YamlName + $deserializerBuilder = $deserializerBuilder.WithAttributeOverride($theType, $attr.Name, $alias) + break + } + "PowerShellYamlSerializable" { + $shouldRecurse = $customAttr.ShouldRecurse + break + } + } + } + if ($shouldRecurse -eq $true) { + $nestedType = $null + if ($attr.PropertyType -eq [string]) { + $nestedType = [string] + } else { + $instance = [System.Activator]::CreateInstance($attr.PropertyType) + $nestedType = $instance.GetType() + } + return (Resolve-AttributeOverrides -deserializer $deserializerBuilder -theType $nestedType) + } + } + return $deserializerBuilder +} + + +function Get-DeserializerBuilder { + $deserializerBuilder = $yamlDotNetAssembly.GetType("YamlDotNet.Serialization.DeserializerBuilder")::new() + $stringQuoted = $stringQuotedAssembly.GetType("BuilderUtils") + $deserializerBuilder = $stringQuoted::BuildDeserializer($deserializerBuilder) + return $deserializerBuilder +} + +function ConvertFrom-YamlToClass { + [CmdletBinding()] + Param( + [Parameter(Mandatory=$true, ValueFromPipeline=$true)] + [string]$Yaml, + [System.Type]$Type + ) + + BEGIN { + $yamlArray = @() + } + + PROCESS { + if($Yaml -is [string]) { + $yamlArray += $Yaml + } + } + + END { + if($yamlArray.Count -eq 0) { + return + } + $deserializerBuilder = Get-DeserializerBuilder + $deserializerBuilder = Resolve-AttributeOverrides -deserializerBuilder $deserializerBuilder -theType $Type + $deserializer = $deserializerBuilder.Build() + + if ($yamlArray.Count -eq 1) { + $deserialized = $deserializer.Deserialize($yamlArray[0], $Type) + return $deserialized + } + + $ret = @() + foreach($i in $yamlArray) { + $ret += $deserializer.Deserialize($i, $Type) + } + return $ret + } +} + function ConvertFrom-Yaml { [CmdletBinding()] Param( @@ -385,41 +513,6 @@ function ConvertFrom-Yaml { } } -function Get-Serializer { - Param( - [Parameter(Mandatory=$true)][SerializationOptions]$Options - ) - - $builder = $yamlDotNetAssembly.GetType("YamlDotNet.Serialization.SerializerBuilder")::new() - - if ($Options.HasFlag([SerializationOptions]::Roundtrip)) { - $builder = $builder.EnsureRoundtrip() - } - if ($Options.HasFlag([SerializationOptions]::DisableAliases)) { - $builder = $builder.DisableAliases() - } - if ($Options.HasFlag([SerializationOptions]::EmitDefaults)) { - $builder = $builder.EmitDefaults() - } - if ($Options.HasFlag([SerializationOptions]::JsonCompatible)) { - $builder = $builder.JsonCompatible() - } - if ($Options.HasFlag([SerializationOptions]::DefaultToStaticType)) { - $resolver = $yamlDotNetAssembly.GetType("YamlDotNet.Serialization.TypeResolvers.StaticTypeResolver")::new() - $builder = $builder.WithTypeResolver($resolver) - } - if ($Options.HasFlag([SerializationOptions]::WithIndentedSequences)) { - $builder = $builder.WithIndentedSequences() - } - - $omitNull = $Options.HasFlag([SerializationOptions]::OmitNullValues) - - $stringQuoted = $stringQuotedAssembly.GetType("StringQuotingEmitter") - $builder = $stringQuoted::Add($builder, $omitNull) - - return $builder.Build() -} - function ConvertTo-Yaml { [CmdletBinding(DefaultParameterSetName = 'NoOptions')] Param( @@ -493,7 +586,10 @@ function ConvertTo-Yaml { } } + New-Alias -Name cfy -Value ConvertFrom-Yaml New-Alias -Name cty -Value ConvertTo-Yaml Export-ModuleMember -Function ConvertFrom-Yaml,ConvertTo-Yaml -Alias cfy,cty +Export-ModuleMember -Function ConvertFrom-YamlToClass + diff --git a/src/PowerShellYamlSerializer.csproj b/src/PowerShellYamlSerialization/PowerShellYamlSerialization.csproj similarity index 100% rename from src/PowerShellYamlSerializer.csproj rename to src/PowerShellYamlSerialization/PowerShellYamlSerialization.csproj diff --git a/src/PowerShellYamlSerializer.cs b/src/PowerShellYamlSerialization/serializer.cs similarity index 85% rename from src/PowerShellYamlSerializer.cs rename to src/PowerShellYamlSerialization/serializer.cs index 1a9df4f..855bdba 100644 --- a/src/PowerShellYamlSerializer.cs +++ b/src/PowerShellYamlSerialization/serializer.cs @@ -63,9 +63,16 @@ public bool Accepts(Type type) { public object ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) { - // We don't really need to do any custom deserialization. - var deserializedObject = rootDeserializer(typeof(IDictionary)) as IDictionary; - return deserializedObject; + var psObject = new PSObject(); + parser.Consume(); + + while (parser.TryConsume(out var scalar)) { + var key = scalar.Value; + var value = rootDeserializer(typeof(object)); + psObject.Properties.Add(new PSNoteProperty(key, value)); + } + parser.Consume(); + return psObject; } public void WriteYaml(IEmitter emitter, object value, Type type, ObjectSerializer serializer) { @@ -111,14 +118,16 @@ public override void Emit(ScalarEventInfo eventInfo, IEmitter emitter) { eventInfo.Style = ScalarStyle.DoubleQuoted; } else if (val.IndexOf('\n') > -1) { eventInfo.Style = ScalarStyle.Literal; - } + } break; } base.Emit(eventInfo, emitter); } - // objectGraphVisitor, w => w.OnTop() - public static SerializerBuilder Add(SerializerBuilder builder, bool omitNullValues = false) { +} + +class BuilderUtils { + public static SerializerBuilder BuildSerializer(SerializerBuilder builder, bool omitNullValues = false) { builder = builder .WithEventEmitter(next => new StringQuotingEmitter(next)) .WithTypeConverter(new BigIntegerTypeConverter()) @@ -129,4 +138,12 @@ public static SerializerBuilder Add(SerializerBuilder builder, bool omitNullValu } return builder; } -} + + public static DeserializerBuilder BuildDeserializer(DeserializerBuilder builder) { + builder = builder + .WithTypeConverter(new BigIntegerTypeConverter()) + .WithTypeConverter(new PSObjectTypeConverter(false)); + return builder; + } + +} \ No newline at end of file diff --git a/src/PowerShellYamlTypes/PowerShellYamlTypes.csproj b/src/PowerShellYamlTypes/PowerShellYamlTypes.csproj new file mode 100644 index 0000000..2fd285a --- /dev/null +++ b/src/PowerShellYamlTypes/PowerShellYamlTypes.csproj @@ -0,0 +1,7 @@ + + + + netstandard2.1;net47 + + + diff --git a/src/PowerShellYamlTypes/Types.cs b/src/PowerShellYamlTypes/Types.cs new file mode 100644 index 0000000..0c9aa30 --- /dev/null +++ b/src/PowerShellYamlTypes/Types.cs @@ -0,0 +1,24 @@ +using System; + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property, Inherited = false, AllowMultiple = false)] +public class PowerShellYamlSerializable : Attribute +{ + public bool ShouldRecurse { get; } + + public PowerShellYamlSerializable(bool shouldRecurse) + { + ShouldRecurse = shouldRecurse; + } +} + + +[AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)] +public class PowerShellYamlPropertyAliasAttribute : Attribute +{ + public string YamlName { get; } + + public PowerShellYamlPropertyAliasAttribute(string yamlName) + { + YamlName = yamlName; + } +}