diff --git a/.gitattributes b/.gitattributes index 0699b39b2..8d9cf194f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,3 @@ *.sh text eol=lf -*.cs text eol=crlf \ No newline at end of file +*.cs text eol=crlf +*.fs text eol=crlf \ No newline at end of file diff --git a/README.md b/README.md index 71d916a07..82a949be4 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ The library has now been successfully used in multiple projects and is considere * netstandard 2.0 * netstandard 2.1 * .NET 6.0 -* .NET 7.0 +* .NET 8.0 * .NET Framework 4.7 ## Quick start diff --git a/YamlDotNet.Analyzers.StaticGenerator/ClassObject.cs b/YamlDotNet.Analyzers.StaticGenerator/ClassObject.cs index adaa04d41..207be17a4 100644 --- a/YamlDotNet.Analyzers.StaticGenerator/ClassObject.cs +++ b/YamlDotNet.Analyzers.StaticGenerator/ClassObject.cs @@ -32,7 +32,9 @@ public class ClassObject public string GuidSuffix { get; } public bool IsArray { get; } public bool IsDictionary { get; } + public bool IsDictionaryOverride { get; } public bool IsList { get; } + public bool IsListOverride { get; } public ITypeSymbol ModuleSymbol { get; } public List OnDeserializedMethods { get; } public List OnDeserializingMethods { get; } @@ -41,7 +43,13 @@ public class ClassObject public List PropertySymbols { get; } public string SanitizedClassName { get; } - public ClassObject(string sanitizedClassName, ITypeSymbol moduleSymbol, bool isDictionary = false, bool isList = false, bool isArray = false) + public ClassObject(string sanitizedClassName, + ITypeSymbol moduleSymbol, + bool isDictionary = false, + bool isList = false, + bool isArray = false, + bool isListOverride = false, + bool isDictionaryOverride = false) { FieldSymbols = new List(); PropertySymbols = new List(); @@ -50,12 +58,14 @@ public ClassObject(string sanitizedClassName, ITypeSymbol moduleSymbol, bool isD IsDictionary = isDictionary; IsList = isList; IsArray = isArray; + IsListOverride = isListOverride; ModuleSymbol = moduleSymbol; OnDeserializedMethods = new List(); OnDeserializingMethods = new List(); OnSerializedMethods = new List(); OnSerializingMethods = new List(); SanitizedClassName = sanitizedClassName; + IsDictionaryOverride = isDictionaryOverride; } } } diff --git a/YamlDotNet.Analyzers.StaticGenerator/EnumMappings.cs b/YamlDotNet.Analyzers.StaticGenerator/EnumMappings.cs new file mode 100644 index 000000000..93a1fc9f2 --- /dev/null +++ b/YamlDotNet.Analyzers.StaticGenerator/EnumMappings.cs @@ -0,0 +1,39 @@ +// This file is part of YamlDotNet - A .NET library for YAML. +// Copyright (c) Antoine Aubry and contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using Microsoft.CodeAnalysis; + +namespace YamlDotNet.Analyzers.StaticGenerator +{ + public class EnumMappings + { + public ITypeSymbol Type { get; set; } + public string ActualName { get; set; } + public string EnumMemberValue { get; set; } + + public EnumMappings(ITypeSymbol type, string actualName, string enumMemberValue) + { + ActualName = actualName; + EnumMemberValue = enumMemberValue; + Type = type; + } + } +} diff --git a/YamlDotNet.Analyzers.StaticGenerator/File.cs b/YamlDotNet.Analyzers.StaticGenerator/File.cs index dd8b5dbea..1d1d2c005 100644 --- a/YamlDotNet.Analyzers.StaticGenerator/File.cs +++ b/YamlDotNet.Analyzers.StaticGenerator/File.cs @@ -52,6 +52,6 @@ public void UnIndent() _unindent(); } - public abstract void Write(ClassSyntaxReceiver classSyntaxReceiver); + public abstract void Write(SerializableSyntaxReceiver classSyntaxReceiver); } } diff --git a/YamlDotNet.Analyzers.StaticGenerator/ObjectAccessorFileGenerator.cs b/YamlDotNet.Analyzers.StaticGenerator/ObjectAccessorFileGenerator.cs index e44d860d6..830d49bc7 100644 --- a/YamlDotNet.Analyzers.StaticGenerator/ObjectAccessorFileGenerator.cs +++ b/YamlDotNet.Analyzers.StaticGenerator/ObjectAccessorFileGenerator.cs @@ -32,9 +32,9 @@ public ObjectAccessorFileGenerator(Action Write, Action indent, Ac { } - public override void Write(ClassSyntaxReceiver classSyntaxReceiver) + public override void Write(SerializableSyntaxReceiver syntaxReceiver) { - foreach (var o in classSyntaxReceiver.Classes) + foreach (var o in syntaxReceiver.Classes) { var classObject = o.Value; Write($"class {classObject.SanitizedClassName}_{classObject.GuidSuffix} : YamlDotNet.Serialization.IObjectAccessor"); diff --git a/YamlDotNet.Analyzers.StaticGenerator/ClassSyntaxReceiver.cs b/YamlDotNet.Analyzers.StaticGenerator/SerializableSyntaxReceiver.cs similarity index 71% rename from YamlDotNet.Analyzers.StaticGenerator/ClassSyntaxReceiver.cs rename to YamlDotNet.Analyzers.StaticGenerator/SerializableSyntaxReceiver.cs index a9cedb9d8..f1548bb13 100644 --- a/YamlDotNet.Analyzers.StaticGenerator/ClassSyntaxReceiver.cs +++ b/YamlDotNet.Analyzers.StaticGenerator/SerializableSyntaxReceiver.cs @@ -27,15 +27,24 @@ namespace YamlDotNet.Analyzers.StaticGenerator { - public class ClassSyntaxReceiver : ISyntaxContextReceiver + public class SerializableSyntaxReceiver : ISyntaxContextReceiver { public List Log { get; } = new(); public Dictionary Classes { get; } = new Dictionary(); + public Dictionary> EnumMappings { get; } = new Dictionary>(SymbolEqualityComparer.Default); public INamedTypeSymbol? YamlStaticContextType { get; set; } public void OnVisitSyntaxNode(GeneratorSyntaxContext context) { - if (context.Node is ClassDeclarationSyntax classDeclarationSyntax) + if (context.Node is EnumDeclarationSyntax enumDeclarationSyntax) + { + var enumSymbol = context.SemanticModel.GetDeclaredSymbol(enumDeclarationSyntax)!; + if (enumSymbol.GetAttributes().Any(attribute => attribute.AttributeClass?.ToDisplayString() == "YamlDotNet.Serialization.YamlSerializableAttribute")) + { + HandleEnum(enumSymbol); + } + } + else if (context.Node is ClassDeclarationSyntax classDeclarationSyntax) { var classSymbol = context.SemanticModel.GetDeclaredSymbol(classDeclarationSyntax)!; if (classSymbol.GetAttributes().Any()) @@ -53,7 +62,10 @@ public void OnVisitSyntaxNode(GeneratorSyntaxContext context) foreach (var type in types.OfType()) { - AddSerializableClass(type); + if (type.TypeKind == TypeKind.Class) + { + AddSerializableClass(type); + } } } @@ -98,6 +110,7 @@ private void AddSerializableClass(INamedTypeSymbol? classSymbol) { classObject.PropertySymbols.Add(propertySymbol); CheckForSupportedGeneric(propertySymbol.Type); + HandleEnum(propertySymbol.Type); } } else if (member is IFieldSymbol fieldSymbol) @@ -106,6 +119,7 @@ private void AddSerializableClass(INamedTypeSymbol? classSymbol) { classObject.FieldSymbols.Add(fieldSymbol); CheckForSupportedGeneric(fieldSymbol.Type); + HandleEnum(fieldSymbol.Type); } } else if (member is IMethodSymbol methodSymbol) @@ -163,6 +177,46 @@ private void CheckForSupportedGeneric(ITypeSymbol type) Classes.Add(sanitizedTypeName, new ClassObject(sanitizedTypeName, (INamedTypeSymbol)type, isList: true)); CheckForSupportedGeneric(((INamedTypeSymbol)type).TypeArguments[0]); } + // Overrides for lists + else if (typeName.StartsWith("System.Collections.Generic.ICollection") || + typeName.StartsWith("System.Collections.Generic.IEnumerable") || + typeName.StartsWith("System.Collections.Generic.IList") || + typeName.StartsWith("System.Collections.Generic.IReadOnlyCollection") || + typeName.StartsWith("System.Collections.Generic.IReadOnlyList")) + { + Classes.Add(sanitizedTypeName, new ClassObject(sanitizedTypeName, (INamedTypeSymbol)type, isListOverride: true)); + CheckForSupportedGeneric(((INamedTypeSymbol)type).TypeArguments[0]); + } + else if (typeName.StartsWith("System.Collections.Generic.IReadOnlyDictionary")) + { + Classes.Add(sanitizedTypeName, new ClassObject(sanitizedTypeName, (INamedTypeSymbol)type, isDictionaryOverride: true)); + CheckForSupportedGeneric(((INamedTypeSymbol)type).TypeArguments[1]); + } + } + + private void HandleEnum(ITypeSymbol type) + { + if (type.TypeKind == TypeKind.Enum) + { + var enumMembers = type.GetMembers(); + var mappings = new List(); + foreach (var member in enumMembers) + { + if (member.Kind == SymbolKind.Field) + { + var enumMember = member.GetAttributes().FirstOrDefault(x => x.AttributeClass!.ToDisplayString() == "System.Runtime.Serialization.EnumMemberAttribute"); + var memberName = member.Name!; + var memberValue = memberName; + if (enumMember != null) + { + var argument = enumMember.NamedArguments.FirstOrDefault(x => x.Key == "Value"); + memberValue = (string)argument.Value.Value!; + } + mappings.Add(new EnumMappings(type, memberName, memberValue)); + } + } + this.EnumMappings[type] = mappings; + } } } } diff --git a/YamlDotNet.Analyzers.StaticGenerator/StaticContextFile.cs b/YamlDotNet.Analyzers.StaticGenerator/StaticContextFile.cs index eb4be5db4..1eba1f712 100644 --- a/YamlDotNet.Analyzers.StaticGenerator/StaticContextFile.cs +++ b/YamlDotNet.Analyzers.StaticGenerator/StaticContextFile.cs @@ -30,14 +30,14 @@ public StaticContextFile(Action write, Action indent, Action unind { } - public override void Write(ClassSyntaxReceiver classSyntaxReceiver) + public override void Write(SerializableSyntaxReceiver syntaxReceiver) { - Write($"public partial class {classSyntaxReceiver.YamlStaticContextType?.Name ?? "StaticContext"} : YamlDotNet.Serialization.StaticContext"); + Write($"public partial class {syntaxReceiver.YamlStaticContextType?.Name ?? "StaticContext"} : YamlDotNet.Serialization.StaticContext"); Write("{"); Indent(); Write("private readonly YamlDotNet.Serialization.ObjectFactories.StaticObjectFactory _objectFactory;"); Write("private readonly YamlDotNet.Serialization.ITypeResolver _typeResolver;"); Write("private readonly YamlDotNet.Serialization.ITypeInspector _typeInspector;"); - Write($"public {classSyntaxReceiver.YamlStaticContextType?.Name ?? "StaticContext"}()"); + Write($"public {syntaxReceiver.YamlStaticContextType?.Name ?? "StaticContext"}()"); Write("{"); Indent(); Write("_objectFactory = new StaticObjectFactory();"); Write("_typeResolver = new StaticTypeResolver(this);"); @@ -48,26 +48,19 @@ public override void Write(ClassSyntaxReceiver classSyntaxReceiver) Write("public override YamlDotNet.Serialization.ITypeResolver GetTypeResolver() => _typeResolver;"); Write("public override bool IsKnownType(Type type)"); Write("{"); Indent(); - foreach (var o in classSyntaxReceiver.Classes) + foreach (var o in syntaxReceiver.Classes) { var classObject = o.Value; - if (classObject.IsArray) - { - Write($"if (type == typeof({classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)})) return true;"); - } - else if (classObject.IsList) - { - Write($"if (type == typeof({classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)})) return true;"); - } - else if (classObject.IsDictionary) + if (classObject.IsArray || classObject.IsList || classObject.IsDictionary || classObject.IsDictionaryOverride || classObject.IsListOverride) { Write($"if (type == typeof({classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)})) return true;"); } else { Write($"if (type == typeof({o.Value.ModuleSymbol.GetFullName().Replace("?", string.Empty)})) return true;"); - //always support a array, list and dictionary of the type + //always support an array, ienumerable, list and dictionary of the type Write($"if (type == typeof({classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)}[])) return true;"); + Write($"if (type == typeof(System.Collections.Generic.IEnumerable<{classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)}>)) return true;"); Write($"if (type == typeof(System.Collections.Generic.List<{classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)}>)) return true;"); Write($"if (type == typeof(System.Collections.Generic.Dictionary)) return true;"); } diff --git a/YamlDotNet.Analyzers.StaticGenerator/StaticObjectFactoryFile.cs b/YamlDotNet.Analyzers.StaticGenerator/StaticObjectFactoryFile.cs index 01dafdfbb..677d6c9b3 100644 --- a/YamlDotNet.Analyzers.StaticGenerator/StaticObjectFactoryFile.cs +++ b/YamlDotNet.Analyzers.StaticGenerator/StaticObjectFactoryFile.cs @@ -32,17 +32,33 @@ public StaticObjectFactoryFile(Action write, Action indent, Action { } - public override void Write(ClassSyntaxReceiver classSyntaxReceiver) + public override void Write(SerializableSyntaxReceiver syntaxReceiver) { Write($"class StaticObjectFactory : YamlDotNet.Serialization.ObjectFactories.StaticObjectFactory"); Write("{"); Indent(); Write("public override object Create(Type type)"); Write("{"); Indent(); - foreach (var o in classSyntaxReceiver.Classes.Where(c => !c.Value.IsArray)) + foreach (var o in syntaxReceiver.Classes.Where(c => !c.Value.IsArray)) { var classObject = o.Value; - Write($"if (type == typeof({classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)})) return new {classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)}();"); + if (o.Value.IsListOverride) + { + Write($"if (type == typeof({classObject.ModuleSymbol.GetNamespace()}.{classObject.ModuleSymbol.Name}<{((INamedTypeSymbol)classObject.ModuleSymbol).TypeArguments[0].GetFullName().Replace("?", string.Empty)}>)) return new System.Collections.Generic.List<{((INamedTypeSymbol)classObject.ModuleSymbol).TypeArguments[0].GetFullName().Replace("?", string.Empty)}>();"); + } + else if (o.Value.IsDictionaryOverride) + { + var keyType = ((INamedTypeSymbol)classObject.ModuleSymbol).TypeArguments[0].GetFullName().Replace("?", string.Empty); + var valueType = ((INamedTypeSymbol)classObject.ModuleSymbol).TypeArguments[1].GetFullName().Replace("?", string.Empty); + //Write("/* this is a dictionary override: "); + //Write($"if (type == typeof({classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)})) return new System.Collections.Dictionary<{keyType}, {valueType}>();"); + //Write("*/"); + Write($"if (type == typeof({classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)})) return new System.Collections.Generic.Dictionary<{keyType}, {valueType}>();"); + } + else + { + Write($"if (type == typeof({classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)})) return new {classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)}();"); + } //always support a list and dictionary of the type Write($"if (type == typeof(System.Collections.Generic.List<{classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)}>)) return new System.Collections.Generic.List<{classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)}>();"); Write($"if (type == typeof(System.Collections.Generic.Dictionary)) return new System.Collections.Generic.Dictionary();"); @@ -54,7 +70,7 @@ public override void Write(ClassSyntaxReceiver classSyntaxReceiver) Write("public override Array CreateArray(Type type, int count)"); Write("{"); Indent(); - foreach (var o in classSyntaxReceiver.Classes) + foreach (var o in syntaxReceiver.Classes) { var classObject = o.Value; if (classObject.IsArray) @@ -71,10 +87,10 @@ public override void Write(ClassSyntaxReceiver classSyntaxReceiver) Write("public override bool IsDictionary(Type type)"); Write("{"); Indent(); - foreach (var o in classSyntaxReceiver.Classes) + foreach (var o in syntaxReceiver.Classes) { var classObject = o.Value; - if (classObject.IsDictionary) + if (classObject.IsDictionary || classObject.IsDictionaryOverride) { Write($"if (type == typeof({o.Value.ModuleSymbol.GetFullName().Replace("?", string.Empty)})) return true;"); } @@ -91,7 +107,7 @@ public override void Write(ClassSyntaxReceiver classSyntaxReceiver) Write("public override bool IsArray(Type type)"); Write("{"); Indent(); - foreach (var o in classSyntaxReceiver.Classes) + foreach (var o in syntaxReceiver.Classes) { var classObject = o.Value; if (classObject.IsArray) @@ -109,10 +125,10 @@ public override void Write(ClassSyntaxReceiver classSyntaxReceiver) Write("public override bool IsList(Type type)"); Write("{"); Indent(); - foreach (var o in classSyntaxReceiver.Classes) + foreach (var o in syntaxReceiver.Classes) { var classObject = o.Value; - if (classObject.IsList) + if (classObject.IsList || classObject.IsListOverride) { Write($"if (type == typeof({classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)})) return true;"); } @@ -120,6 +136,9 @@ public override void Write(ClassSyntaxReceiver classSyntaxReceiver) { //always support a list of the type Write($"if (type == typeof(System.Collections.Generic.List<{classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)}>)) return true;"); + + //we'll make ienumerables lists. + Write($"if (type == typeof(System.Collections.Generic.IEnumerable<{classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)}>)) return true;"); } } Write("return false;"); @@ -127,10 +146,10 @@ public override void Write(ClassSyntaxReceiver classSyntaxReceiver) Write("public override Type GetKeyType(Type type)"); Write("{"); Indent(); - foreach (var o in classSyntaxReceiver.Classes) + foreach (var o in syntaxReceiver.Classes) { var classObject = o.Value; - if (classObject.IsDictionary) + if (classObject.IsDictionary || classObject.IsDictionaryOverride) { var keyType = "object"; var type = (INamedTypeSymbol)classObject.ModuleSymbol; @@ -142,7 +161,7 @@ public override void Write(ClassSyntaxReceiver classSyntaxReceiver) Write($"if (type == typeof({classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)})) return typeof({keyType});"); } - else if (!classObject.IsArray && !classObject.IsList) + else if (!classObject.IsArray && !classObject.IsList && !classObject.IsListOverride) { //always support a dictionary of the type Write($"if (type == typeof(System.Collections.Generic.Dictionary)) return typeof({classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)});"); @@ -156,20 +175,20 @@ public override void Write(ClassSyntaxReceiver classSyntaxReceiver) Write("public override Type GetValueType(Type type)"); Write("{"); Indent(); - foreach (var o in classSyntaxReceiver.Classes) + foreach (var o in syntaxReceiver.Classes) { var classObject = o.Value; - if (!(classObject.IsList || classObject.IsDictionary || classObject.IsArray)) + if (!(classObject.IsList || classObject.IsDictionary || classObject.IsDictionaryOverride || classObject.IsArray || classObject.IsListOverride)) { continue; } string valueType; - if (classObject.IsDictionary) + if (classObject.IsDictionary || classObject.IsDictionaryOverride) { valueType = ((INamedTypeSymbol)classObject.ModuleSymbol).TypeArguments[1].GetFullName().Replace("?", string.Empty); } - else if (classObject.IsList) + else if (classObject.IsList || classObject.IsListOverride) { valueType = ((INamedTypeSymbol)classObject.ModuleSymbol).TypeArguments[0].GetFullName().Replace("?", string.Empty); } @@ -181,11 +200,12 @@ public override void Write(ClassSyntaxReceiver classSyntaxReceiver) Write($"if (type == typeof({classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)})) return typeof({valueType});"); } - //always support array, list and dictionary of all types - foreach (var o in classSyntaxReceiver.Classes) + //always support array, list, dictionary and Ienumerables of all types + foreach (var o in syntaxReceiver.Classes) { var classObject = o.Value; Write($"if (type == typeof({classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)}[])) return typeof({classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)});"); + Write($"if (type == typeof(System.Collections.Generic.IEnumerable<{classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)}>)) return typeof({classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)});"); Write($"if (type == typeof(System.Collections.Generic.List<{classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)}>)) return typeof({classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)});"); Write($"if (type == typeof(System.Collections.Generic.Dictionary)) return typeof({classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)});"); } @@ -193,20 +213,20 @@ public override void Write(ClassSyntaxReceiver classSyntaxReceiver) Write("if (type == typeof(System.Collections.Generic.Dictionary)) return typeof(object);"); Write("throw new ArgumentOutOfRangeException(\"Unknown type: \" + type.ToString());"); UnIndent(); Write("}"); - WriteExecuteMethod(classSyntaxReceiver, "ExecuteOnDeserializing", (c) => c.OnDeserializingMethods); - WriteExecuteMethod(classSyntaxReceiver, "ExecuteOnDeserialized", (c) => c.OnDeserializedMethods); - WriteExecuteMethod(classSyntaxReceiver, "ExecuteOnSerializing", (c) => c.OnSerializingMethods); - WriteExecuteMethod(classSyntaxReceiver, "ExecuteOnSerialized", (c) => c.OnSerializedMethods); + WriteExecuteMethod(syntaxReceiver, "ExecuteOnDeserializing", (c) => c.OnDeserializingMethods); + WriteExecuteMethod(syntaxReceiver, "ExecuteOnDeserialized", (c) => c.OnDeserializedMethods); + WriteExecuteMethod(syntaxReceiver, "ExecuteOnSerializing", (c) => c.OnSerializingMethods); + WriteExecuteMethod(syntaxReceiver, "ExecuteOnSerialized", (c) => c.OnSerializedMethods); UnIndent(); Write("}"); } - private void WriteExecuteMethod(ClassSyntaxReceiver classSyntaxReceiver, string methodName, Func> selector) + private void WriteExecuteMethod(SerializableSyntaxReceiver syntaxReceiver, string methodName, Func> selector) { Write($"public override void {methodName}(object value)"); Write("{"); Indent(); Write("if (value == null) return;"); Write("var type = value.GetType();"); - foreach (var o in classSyntaxReceiver.Classes) + foreach (var o in syntaxReceiver.Classes) { var classObject = o.Value; var methods = selector(classObject); diff --git a/YamlDotNet.Analyzers.StaticGenerator/StaticPropertyDescriptorFile.cs b/YamlDotNet.Analyzers.StaticGenerator/StaticPropertyDescriptorFile.cs index 34d16cb29..3df3f24cc 100644 --- a/YamlDotNet.Analyzers.StaticGenerator/StaticPropertyDescriptorFile.cs +++ b/YamlDotNet.Analyzers.StaticGenerator/StaticPropertyDescriptorFile.cs @@ -30,7 +30,7 @@ public StaticPropertyDescriptorFile(Action write, Action indent, A { } - public override void Write(ClassSyntaxReceiver classSyntaxReceiver) + public override void Write(SerializableSyntaxReceiver syntaxReceiver) { Write("class StaticPropertyDescriptor : YamlDotNet.Serialization.IPropertyDescriptor"); Write("{"); Indent(); @@ -40,7 +40,10 @@ public override void Write(ClassSyntaxReceiver classSyntaxReceiver) Write("public string Name { get; }"); Write("public bool CanWrite { get; }"); Write("public Type Type { get; }"); + Write("public Type ConverterType { get; }"); Write("public Type TypeOverride { get; set; }"); + Write("public bool AllowNulls { get; set; }"); + Write("public bool Required { get; }"); Write("public int Order { get; set; }"); Write("public YamlDotNet.Core.ScalarStyle ScalarStyle { get; set; }"); Write("public T GetCustomAttribute() where T : Attribute"); @@ -61,7 +64,7 @@ public override void Write(ClassSyntaxReceiver classSyntaxReceiver) Write("{"); Indent(); Write("_accessor.Set(Name, target, value);"); UnIndent(); Write("}"); - Write("public StaticPropertyDescriptor(YamlDotNet.Serialization.ITypeResolver typeResolver, YamlDotNet.Serialization.IObjectAccessor accessor, string name, bool canWrite, Type type, Attribute[] attributes)"); + Write("public StaticPropertyDescriptor(YamlDotNet.Serialization.ITypeResolver typeResolver, YamlDotNet.Serialization.IObjectAccessor accessor, string name, bool canWrite, Type type, Attribute[] attributes, bool allowNulls, bool isRequired, Type converterType)"); Write("{"); Indent(); Write("this._typeResolver = typeResolver;"); Write("this._accessor = accessor;"); @@ -69,7 +72,10 @@ public override void Write(ClassSyntaxReceiver classSyntaxReceiver) Write("this.Name = name;"); Write("this.CanWrite = canWrite;"); Write("this.Type = type;"); + Write("this.ConverterType = converterType;"); Write("this.ScalarStyle = YamlDotNet.Core.ScalarStyle.Any;"); + Write("this.AllowNulls = allowNulls;"); + Write("this.Required = isRequired;"); UnIndent(); Write("}"); UnIndent(); Write("}"); } diff --git a/YamlDotNet.Analyzers.StaticGenerator/StaticTypeInspectorFile.cs b/YamlDotNet.Analyzers.StaticGenerator/StaticTypeInspectorFile.cs index 336b6f12f..cb9a72d06 100644 --- a/YamlDotNet.Analyzers.StaticGenerator/StaticTypeInspectorFile.cs +++ b/YamlDotNet.Analyzers.StaticGenerator/StaticTypeInspectorFile.cs @@ -21,6 +21,7 @@ using System; using System.Collections.Immutable; +using System.Linq; using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -29,11 +30,14 @@ namespace YamlDotNet.Analyzers.StaticGenerator { public class StaticTypeInspectorFile : File { + private readonly GeneratorExecutionContext context; + public StaticTypeInspectorFile(Action Write, Action indent, Action unindent, GeneratorExecutionContext context) : base(Write, indent, unindent, context) { + this.context = context; } - public override void Write(ClassSyntaxReceiver classSyntaxReceiver) + public override void Write(SerializableSyntaxReceiver syntaxReceiver) { Write("public class StaticTypeInspector : YamlDotNet.Serialization.ITypeInspector"); Write("{"); Indent(); @@ -47,7 +51,7 @@ public override void Write(ClassSyntaxReceiver classSyntaxReceiver) #region GetProperties Write("public IEnumerable GetProperties(Type type, object container)"); Write("{"); Indent(); - foreach (var o in classSyntaxReceiver.Classes) + foreach (var o in syntaxReceiver.Classes) { var classObject = o.Value; Write($"if (type == typeof({classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)}))"); @@ -57,11 +61,11 @@ public override void Write(ClassSyntaxReceiver classSyntaxReceiver) Write("{"); Indent(); foreach (var field in classObject.FieldSymbols) { - WritePropertyDescriptor(field.Name, field.Type, !field.IsReadOnly, field.GetAttributes(), ','); + WritePropertyDescriptor(field.Name, field.Type, !field.IsReadOnly, field.GetAttributes(), field.IsRequired, ','); } foreach (var property in classObject.PropertySymbols) { - WritePropertyDescriptor(property.Name, property.Type, property.SetMethod == null, property.GetAttributes(), ','); + WritePropertyDescriptor(property.Name, property.Type, property.SetMethod == null, property.GetAttributes(), property.IsRequired, ','); } UnIndent(); Write("};"); UnIndent(); Write("}"); @@ -71,9 +75,9 @@ public override void Write(ClassSyntaxReceiver classSyntaxReceiver) #endregion #region GetProperty - Write("public YamlDotNet.Serialization.IPropertyDescriptor GetProperty(Type type, object container, string name, bool ignoreUnmatched)"); + Write("public YamlDotNet.Serialization.IPropertyDescriptor GetProperty(Type type, object container, string name, bool ignoreUnmatched, bool caseInsensitivePropertyMatching)"); Write("{"); Indent(); - foreach (var o in classSyntaxReceiver.Classes) + foreach (var o in syntaxReceiver.Classes) { var classObject = o.Value; Write($"if (type == typeof({classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)}))"); @@ -82,13 +86,28 @@ public override void Write(ClassSyntaxReceiver classSyntaxReceiver) foreach (var field in classObject.FieldSymbols) { Write($"if (name == \"{field.Name}\") return "); - WritePropertyDescriptor(field.Name, field.Type, field.IsReadOnly, field.GetAttributes(), ';'); + WritePropertyDescriptor(field.Name, field.Type, field.IsReadOnly, field.GetAttributes(), field.IsRequired, ';'); } foreach (var property in classObject.PropertySymbols) { Write($"if (name == \"{property.Name}\") return ", false); - WritePropertyDescriptor(property.Name, property.Type, property.SetMethod == null, property.GetAttributes(), ';'); + WritePropertyDescriptor(property.Name, property.Type, property.SetMethod == null, property.GetAttributes(), property.IsRequired, ';'); + } + Write("if (caseInsensitivePropertyMatching)"); + Write("{"); Indent(); + foreach (var field in classObject.FieldSymbols) + { + Write($"if (name.Equals(\"{field.Name}\", System.StringComparison.OrdinalIgnoreCase)) return "); + WritePropertyDescriptor(field.Name, field.Type, field.IsReadOnly, field.GetAttributes(), field.IsRequired, ';'); } + foreach (var property in classObject.PropertySymbols) + { + Write($"if (name.Equals(\"{property.Name}\", System.StringComparison.OrdinalIgnoreCase)) return "); + WritePropertyDescriptor(property.Name, property.Type, property.SetMethod == null, property.GetAttributes(), property.IsRequired, ';'); + } + + UnIndent(); Write("}"); + UnIndent(); Write("}"); } @@ -97,16 +116,60 @@ public override void Write(ClassSyntaxReceiver classSyntaxReceiver) UnIndent(); Write("}"); #endregion + Write("public string GetEnumName(Type type, string name)"); + Write("{"); Indent(); + var prefix = string.Empty; + foreach (var map in syntaxReceiver.EnumMappings) + { + Write($"{prefix}if (type == typeof({map.Key.GetFullName()}))"); + Write("{"); Indent(); + + foreach (var mapping in map.Value) + { + Write($"if (name == \"{mapping.EnumMemberValue.Replace("\"", "\\\"")}\") return \"{mapping.ActualName}\";"); + } + + UnIndent(); Write("}"); + prefix = "else "; + } + Write("return name;"); + UnIndent(); Write("}"); + + prefix = string.Empty; + Write("public string GetEnumValue(object value)"); + Write("{"); Indent(); + Write("var type = value.GetType();"); + foreach (var map in syntaxReceiver.EnumMappings) + { + Write($"{prefix}if (type == typeof({map.Key.GetFullName()}))"); + Write("{"); Indent(); + + foreach (var mapping in map.Value) + { + Write($"if (({mapping.Type.GetFullName()})value == {mapping.Type.GetFullName()}.{mapping.ActualName}) return \"{mapping.EnumMemberValue.Replace("\"", "\\\"")}\";"); + } + + UnIndent(); Write("}"); + prefix = "else "; + } + Write("return value.ToString();"); + UnIndent(); Write("}"); + UnIndent(); Write("}"); } - private void WritePropertyDescriptor(string name, ITypeSymbol type, bool isReadonly, ImmutableArray attributes, char finalChar) + private void WritePropertyDescriptor(string name, ITypeSymbol type, bool isReadonly, ImmutableArray attributes, bool isRequired, char finalChar) { + var allowNulls = type.NullableAnnotation.HasFlag(NullableAnnotation.Annotated) && context.Compilation.Options.NullableContextOptions.AnnotationsEnabled(); + AttributeData? yamlConverterAttribute = null; Write($"new StaticPropertyDescriptor(_typeResolver, accessor, \"{name}\", {(!isReadonly).ToString().ToLower()}, typeof({type.GetFullName().Replace("?", string.Empty)}), new Attribute[] {{"); foreach (var attribute in attributes) { switch (attribute.AttributeClass?.ToDisplayString()) { + case "YamlDotNet.Serialization.YamlConverterAttribute()": + yamlConverterAttribute = attribute; + break; case "YamlDotNet.Serialization.YamlIgnore": Write("new YamlDotNet.Serialization.YamlIgnoreAttribute(),"); break; @@ -145,8 +208,18 @@ private void WritePropertyDescriptor(string name, ITypeSymbol type, bool isReado break; } } - Write($"}}){finalChar}"); - + Write($"}}, {allowNulls.ToString().ToLower()}, {isRequired.ToString().ToLower()}, ", false); + if (yamlConverterAttribute == null) + { + Write("null", false); + } + else + { + var argument = yamlConverterAttribute.ConstructorArguments[0]; + var converterType = (Type)argument.Value!; + Write($"typeof({converterType.FullName})", false); + } + Write($"){finalChar}"); //TODO: replace null with the class for the typeconverter. } } } diff --git a/YamlDotNet.Analyzers.StaticGenerator/StaticTypeResolverFile.cs b/YamlDotNet.Analyzers.StaticGenerator/StaticTypeResolverFile.cs index 3ead85c3f..7ceee4fe2 100644 --- a/YamlDotNet.Analyzers.StaticGenerator/StaticTypeResolverFile.cs +++ b/YamlDotNet.Analyzers.StaticGenerator/StaticTypeResolverFile.cs @@ -30,7 +30,7 @@ public StaticTypeResolverFile(Action write, Action indent, Action { } - public override void Write(ClassSyntaxReceiver classSyntaxReceiver) + public override void Write(SerializableSyntaxReceiver syntaxReceiver) { Write($"class StaticTypeResolver : YamlDotNet.Serialization.TypeResolvers.StaticTypeResolver"); Write("{"); Indent(); diff --git a/YamlDotNet.Analyzers.StaticGenerator/TypeFactoryGenerator.cs b/YamlDotNet.Analyzers.StaticGenerator/TypeFactoryGenerator.cs index 97671e7c3..52beab621 100644 --- a/YamlDotNet.Analyzers.StaticGenerator/TypeFactoryGenerator.cs +++ b/YamlDotNet.Analyzers.StaticGenerator/TypeFactoryGenerator.cs @@ -32,7 +32,7 @@ public class TypeFactoryGenerator : ISourceGenerator public void Execute(GeneratorExecutionContext context) { - if (!(context.SyntaxContextReceiver is ClassSyntaxReceiver receiver)) + if (!(context.SyntaxContextReceiver is SerializableSyntaxReceiver receiver)) { return; } @@ -54,11 +54,11 @@ public void Execute(GeneratorExecutionContext context) public void Initialize(GeneratorInitializationContext context) { - var classSyntaxReceiver = new ClassSyntaxReceiver(); - context.RegisterForSyntaxNotifications(() => classSyntaxReceiver); + var syntaxReceiver = new SerializableSyntaxReceiver(); + context.RegisterForSyntaxNotifications(() => syntaxReceiver); } - private string GenerateSource(ClassSyntaxReceiver classSyntaxReceiver) + private string GenerateSource(SerializableSyntaxReceiver syntaxReceiver) { var result = new StringBuilder(); var indentation = 0; @@ -89,17 +89,17 @@ private string GenerateSource(ClassSyntaxReceiver classSyntaxReceiver) write("using System;", true); write("using System.Collections.Generic;", true); - var namespaceName = classSyntaxReceiver.YamlStaticContextType?.ContainingNamespace.ContainingNamespace; + var namespaceName = syntaxReceiver.YamlStaticContextType?.ContainingNamespace.ContainingNamespace; - write($"namespace {classSyntaxReceiver.YamlStaticContextType?.GetNamespace() ?? "YamlDotNet.Static"}", true); + write($"namespace {syntaxReceiver.YamlStaticContextType?.GetNamespace() ?? "YamlDotNet.Static"}", true); write("{", true); indent(); - new StaticContextFile(write, indent, unindent, _context).Write(classSyntaxReceiver); - new StaticObjectFactoryFile(write, indent, unindent, _context).Write(classSyntaxReceiver); - new StaticTypeResolverFile(write, indent, unindent, _context).Write(classSyntaxReceiver); - new StaticPropertyDescriptorFile(write, indent, unindent, _context).Write(classSyntaxReceiver); - new StaticTypeInspectorFile(write, indent, unindent, _context).Write(classSyntaxReceiver); - new ObjectAccessorFileGenerator(write, indent, unindent, _context).Write(classSyntaxReceiver); + new StaticContextFile(write, indent, unindent, _context).Write(syntaxReceiver); + new StaticObjectFactoryFile(write, indent, unindent, _context).Write(syntaxReceiver); + new StaticTypeResolverFile(write, indent, unindent, _context).Write(syntaxReceiver); + new StaticPropertyDescriptorFile(write, indent, unindent, _context).Write(syntaxReceiver); + new StaticTypeInspectorFile(write, indent, unindent, _context).Write(syntaxReceiver); + new ObjectAccessorFileGenerator(write, indent, unindent, _context).Write(syntaxReceiver); unindent(); write("}", true); } diff --git a/YamlDotNet.Benchmark/BigFileBenchmark.cs b/YamlDotNet.Benchmark/BigFileBenchmark.cs new file mode 100644 index 000000000..efad79e7c --- /dev/null +++ b/YamlDotNet.Benchmark/BigFileBenchmark.cs @@ -0,0 +1,54 @@ +// This file is part of YamlDotNet - A .NET library for YAML. +// Copyright (c) Antoine Aubry and contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System.IO.Compression; +using System.Text; +using BenchmarkDotNet.Attributes; +using FastSerialization; +using YamlDotNet.RepresentationModel; +using YamlDotNet.Serialization; + +namespace YamlDotNet.Benchmark; + +[MemoryDiagnoser] +public class BigFileBenchmark +{ + private string yamlString = ""; + + [GlobalSetup] + public void Setup() + { + var stringBuilder = new StringBuilder(); + //100mb + while (stringBuilder.Length < 1000 * 1000 * 100) + { + stringBuilder.AppendLine("- test"); + } + yamlString = stringBuilder.ToString(); + } + + [Benchmark] + public void LoadLarge() + { + var deserializer = new DeserializerBuilder().Build(); + var result = deserializer.Deserialize>(yamlString); + } +} diff --git a/YamlDotNet.Benchmark/Program.cs b/YamlDotNet.Benchmark/Program.cs index 9fc2ebd9a..e8df09cdd 100644 --- a/YamlDotNet.Benchmark/Program.cs +++ b/YamlDotNet.Benchmark/Program.cs @@ -1,4 +1,8 @@ -using BenchmarkDotNet.Running; +using System.Diagnostics; using YamlDotNet.Benchmark; +using YamlDotNet.Serialization; -BenchmarkSwitcher.FromAssembly(typeof(YamlStreamBenchmark).Assembly).Run(args); +var serializer = new SerializerBuilder().JsonCompatible().Build(); +var v = new { nan = float.NaN, inf = float.NegativeInfinity, posinf = float.PositiveInfinity, max = float.MaxValue, min = float.MinValue, good = .1234f, good1 = 1, good2 = -.1234, good3= -1 }; +var yaml = serializer.Serialize(v); +Console.WriteLine(yaml); diff --git a/YamlDotNet.Core7AoTCompileTest.Model/ExternalModel.cs b/YamlDotNet.Core7AoTCompileTest.Model/ExternalModel.cs index a882325d5..241c96181 100644 --- a/YamlDotNet.Core7AoTCompileTest.Model/ExternalModel.cs +++ b/YamlDotNet.Core7AoTCompileTest.Model/ExternalModel.cs @@ -3,4 +3,5 @@ public class ExternalModel { public string? Text { get; set; } + public string NotNull { get; set; } = string.Empty; } diff --git a/YamlDotNet.Core7AoTCompileTest.Model/YamlDotNet.Core7AoTCompileTest.Model.csproj b/YamlDotNet.Core7AoTCompileTest.Model/YamlDotNet.Core7AoTCompileTest.Model.csproj index cfadb03dd..89b0c6003 100644 --- a/YamlDotNet.Core7AoTCompileTest.Model/YamlDotNet.Core7AoTCompileTest.Model.csproj +++ b/YamlDotNet.Core7AoTCompileTest.Model/YamlDotNet.Core7AoTCompileTest.Model.csproj @@ -1,9 +1,10 @@ - net7.0 + net8.0 enable enable + true diff --git a/YamlDotNet.Core7AoTCompileTest/Program.cs b/YamlDotNet.Core7AoTCompileTest/Program.cs index 246cbb4e8..cab145991 100644 --- a/YamlDotNet.Core7AoTCompileTest/Program.cs +++ b/YamlDotNet.Core7AoTCompileTest/Program.cs @@ -26,6 +26,8 @@ using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Security.Cryptography.X509Certificates; +using System.Threading.Tasks; using YamlDotNet.Core; using YamlDotNet.Core7AoTCompileTest.Model; using YamlDotNet.Serialization; @@ -76,6 +78,12 @@ NotInherited: world External: Text: hello +SomeCollectionStrings: +- test +- value +SomeEnumerableStrings: +- test +- value SomeObject: a SomeDictionary: a: 1 @@ -148,7 +156,17 @@ Console.WriteLine("Inherited == null: <{0}>", x.Inherited == null); Console.WriteLine("Inherited.Inherited: <{0}>", x.Inherited?.Inherited); Console.WriteLine("Inherited.NotInherited: <{0}>", x.Inherited?.NotInherited); - +Console.WriteLine("SomeEnumerableStrings:"); +foreach (var s in x.SomeEnumerableStrings) +{ + Console.WriteLine(" {0}", s); +} +Console.ReadLine(); +Console.WriteLine("SomeCollectionStrings:"); +foreach (var s in x.SomeCollectionStrings) +{ + Console.WriteLine(" {0}", s); +} Console.WriteLine("=============="); Console.WriteLine("Serialized:"); @@ -157,6 +175,7 @@ var output = serializer.Serialize(x); Console.WriteLine(output); +Console.WriteLine("============== Done with the primary object"); yaml = @"- myArray: - 1 @@ -171,6 +190,36 @@ Console.WriteLine("Items[0]: <{0}>", string.Join(',', o[0].myArray)); Console.WriteLine("Items[1]: <{0}>", string.Join(',', o[1].myArray)); +deserializer = new StaticDeserializerBuilder(aotContext).WithEnforceNullability().Build(); +yaml = "Nullable: null"; +var nullable = deserializer.Deserialize(yaml); +Console.WriteLine("Nullable Value (should be empty): <{0}>", nullable.Nullable); +yaml = "NotNullable: test"; +nullable = deserializer.Deserialize(yaml); +Console.WriteLine("NotNullable Value (should be test): <{0}>", nullable.NotNullable); +try +{ + yaml = "NotNullable: null"; + nullable = deserializer.Deserialize(yaml); + throw new Exception("NotNullable should not be allowed to be set to null."); +} +catch (YamlException exception) +{ + if (exception.InnerException is NullReferenceException) + { + Console.WriteLine("Exception thrown while setting non nullable value to null, as it should."); + } + else + { + throw new Exception("NotNullable should not be allowed to be set to null."); + } +} + +Console.WriteLine("The next line should say goodbye"); +Console.WriteLine(serializer.Serialize(EnumMemberedEnum.Hello)); +Console.WriteLine("The next line should say hello"); +Console.WriteLine(deserializer.Deserialize("goodbye")); + [YamlSerializable] public class MyArray { @@ -183,6 +232,13 @@ public class Inner public string? Text { get; set; } } +[YamlSerializable] +public class NullableTestClass +{ + public string? Nullable { get; set; } + public string NotNullable { get; set; } +} + [YamlSerializable] public class PrimitiveTypes { @@ -215,6 +271,8 @@ public class PrimitiveTypes public List? MyList { get; set; } public Inherited Inherited { get; set; } public ExternalModel External { get; set; } + public IEnumerable SomeEnumerableStrings { get; set; } + public ICollection SomeCollectionStrings { get; set; } public object SomeObject { get; set; } public object SomeDictionary { get; set; } } @@ -262,6 +320,15 @@ public enum MyTestEnum Z = 1, } +[YamlSerializable] +public enum EnumMemberedEnum +{ + No = 0, + + [System.Runtime.Serialization.EnumMember(Value = "goodbye")] + Hello = 1 +} + #pragma warning restore CS8604 // Possible null reference argument. #pragma warning restore CS8618 // Possible null reference argument. #pragma warning restore CS8602 // Possible null reference argument. diff --git a/YamlDotNet.Core7AoTCompileTest/YamlDotNet.Core7AoTCompileTest.csproj b/YamlDotNet.Core7AoTCompileTest/YamlDotNet.Core7AoTCompileTest.csproj index 494a5300c..0fe07dabf 100644 --- a/YamlDotNet.Core7AoTCompileTest/YamlDotNet.Core7AoTCompileTest.csproj +++ b/YamlDotNet.Core7AoTCompileTest/YamlDotNet.Core7AoTCompileTest.csproj @@ -2,10 +2,11 @@ Exe - net7.0 + net8.0 true true enable + true @@ -20,9 +21,7 @@ - + diff --git a/YamlDotNet.Fsharp.Test/DeserializerTests.fs b/YamlDotNet.Fsharp.Test/DeserializerTests.fs index b3ed65dbc..9e0bb2825 100644 --- a/YamlDotNet.Fsharp.Test/DeserializerTests.fs +++ b/YamlDotNet.Fsharp.Test/DeserializerTests.fs @@ -1,168 +1,181 @@ -module DeserializerTests - -open System -open Xunit -open YamlDotNet.Serialization -open YamlDotNet.Serialization.NamingConventions -open FsUnit.Xunit -open System.ComponentModel - -[] -type Spec = { - EngineType: string - DriveType: string -} - -[] -type Car = { - Name: string - Year: int - Spec: Spec option - Nickname: string option -} - -[] -type Person = { - Name: string - MomentOfBirth: DateTime - Cars: Car array -} - -[] -let Deserialize_YamlWithScalarOptions() = - let yaml = """ -name: Jack -momentOfBirth: 1983-04-21T20:21:03.0041599Z -cars: -- name: Mercedes - year: 2018 - nickname: Jessy -- name: Honda - year: 2021 -""" - let sut = DeserializerBuilder() - .WithNamingConvention(CamelCaseNamingConvention.Instance) - .Build() - - let person = sut.Deserialize(yaml) - person.Name |> should equal "Jack" - person.Cars |> should haveLength 2 - person.Cars[0].Name |> should equal "Mercedes" - person.Cars[0].Nickname |> should equal (Some "Jessy") - person.Cars[1].Name |> should equal "Honda" - person.Cars[1].Nickname |> should equal None - - -[] -let Deserialize_YamlWithObjectOptions() = - let yaml = """ -name: Jack -momentOfBirth: 1983-04-21T20:21:03.0041599Z -cars: -- name: Mercedes - year: 2018 - spec: - engineType: V6 - driveType: AWD -- name: Honda - year: 2021 -""" - let sut = DeserializerBuilder() - .WithNamingConvention(CamelCaseNamingConvention.Instance) - .Build() - - let person = sut.Deserialize(yaml) - person.Name |> should equal "Jack" - person.Cars |> should haveLength 2 - - person.Cars[0].Name |> should equal "Mercedes" - person.Cars[0].Spec |> should not' (be null) - person.Cars[0].Spec |> Option.isSome |> should equal true - person.Cars[0].Spec.Value.EngineType |> should equal "V6" - person.Cars[0].Spec.Value.DriveType |> should equal "AWD" - - person.Cars[1].Name |> should equal "Honda" - person.Cars[1].Spec |> should be null - person.Cars[1].Spec |> should equal None - person.Cars[1].Nickname |> should equal None - - -[] -type TestSeq = { - name: string - numbers: int seq -} - -[] -let Deserialize_YamlSeq() = - let jackTheDriver = { - name = "Jack" - numbers = seq { 12; 2; 2 } - } - - let yaml = """name: Jack -numbers: -- 12 -- 2 -- 2 -""" - let sut = DeserializerBuilder() - .WithNamingConvention(CamelCaseNamingConvention.Instance) - .Build() - - let person = sut.Deserialize(yaml) - person.name |> should equal jackTheDriver.name - person.numbers |> should equalSeq jackTheDriver.numbers - -[] -type TestList = { - name: string - numbers: int list -} - -[] -let Deserialize_YamlList() = - let jackTheDriver = { - name = "Jack" - numbers = [ 12; 2; 2 ] - } - - let yaml = """name: Jack -numbers: -- 12 -- 2 -- 2 -""" - let sut = DeserializerBuilder() - .WithNamingConvention(CamelCaseNamingConvention.Instance) - .Build() - - let person = sut.Deserialize(yaml) - person |> should equal jackTheDriver - - -[] -type TestArray = { - name: string - numbers: int array -} - -[] -let Deserialize_YamlArray() = - let jackTheDriver = { - name = "Jack" - numbers = [| 12; 2; 2 |] - } - - let yaml = """name: Jack -numbers: -- 12 -- 2 -- 2 -""" - let sut = DeserializerBuilder() - .WithNamingConvention(CamelCaseNamingConvention.Instance) - .Build() - - let person = sut.Deserialize(yaml) - person |> should equal jackTheDriver +module DeserializerTests + +open System +open System.Linq +open Xunit +open YamlDotNet.Serialization +open YamlDotNet.Serialization.NamingConventions +open System.ComponentModel + +[] +type Spec = { + EngineType: string + DriveType: string +} + +[] +type Car = { + Name: string + Year: int + Spec: Spec option + Nickname: string option +} + +[] +type Person = { + Name: string + MomentOfBirth: DateTime + Cars: Car array +} + +[] +let Deserialize_YamlWithScalarOptions() = + let yaml = """ +name: Jack +momentOfBirth: 1983-04-21T20:21:03.0041599Z +cars: +- name: Mercedes + year: 2018 + nickname: Jessy +- name: Honda + year: 2021 +""" + let sut = DeserializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .Build() + + let person = sut.Deserialize(yaml) + Assert.Equal("Jack", person.Name) + Assert.Equal(2, person.Cars.Length) + Assert.Equal("Mercedes", person.Cars[0].Name) + Assert.Equal(Some "Jessy", person.Cars[0].Nickname)// |> should equal (Some "Jessy") + Assert.Equal("Honda", person.Cars[1].Name) + Assert.Equal(None, person.Cars[1].Nickname) + +[] +let Deserialize_YamlWithObjectOptions() = + let yaml = """ +name: Jack +momentOfBirth: 1983-04-21T20:21:03.0041599Z +cars: +- name: Mercedes + year: 2018 + spec: + engineType: V6 + driveType: AWD +- name: Honda + year: 2021 +""" + let sut = DeserializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .Build() + + let person = sut.Deserialize(yaml) + Assert.Equal("Jack", person.Name) + Assert.Equal(2, person.Cars.Length) + + Assert.Equal("Mercedes", person.Cars[0].Name) + Assert.NotNull(person.Cars[0].Spec) + Assert.True(person.Cars[0].Spec |> Option.isSome) + Assert.Equal("V6", person.Cars[0].Spec.Value.EngineType) + Assert.Equal("AWD", person.Cars[0].Spec.Value.DriveType) + + Assert.Equal("Honda", person.Cars[1].Name) + Assert.Null(person.Cars[1].Spec) + Assert.Equal(None, person.Cars[1].Spec) + Assert.Equal(None, person.Cars[1].Nickname) + + +[] +type TestSeq = { + name: string + numbers: int seq +} + +[] +let Deserialize_YamlSeq() = + let jackTheDriver = { + name = "Jack" + numbers = seq { 12; 2; 2 } + } + + let yaml = """name: Jack +numbers: +- 12 +- 2 +- 2 +""" + let sut = DeserializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .Build() + + let person = sut.Deserialize(yaml) + Assert.Equal(jackTheDriver.name, person.name) + let numbers = person.numbers.ToArray() + Assert.Equal(3, numbers.Length) + Assert.Equal(12, numbers[0]) + Assert.Equal(2, numbers[1]) + Assert.Equal(2, numbers[2]) + +[] +type TestList = { + name: string + numbers: int list +} + +[] +let Deserialize_YamlList() = + let jackTheDriver = { + name = "Jack" + numbers = [ 12; 2; 2 ] + } + + let yaml = """name: Jack +numbers: +- 12 +- 2 +- 2 +""" + let sut = DeserializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .Build() + + let person = sut.Deserialize(yaml) + Assert.Equal(jackTheDriver.name, person.name) + let numbers = person.numbers.ToArray() + Assert.Equal(3, numbers.Length) + Assert.Equal(12, numbers[0]) + Assert.Equal(2, numbers[1]) + Assert.Equal(2, numbers[2]) + + +[] +type TestArray = { + name: string + numbers: int array +} + +[] +let Deserialize_YamlArray() = + let jackTheDriver = { + name = "Jack" + numbers = [| 12; 2; 2 |] + } + + let yaml = """name: Jack +numbers: +- 12 +- 2 +- 2 +""" + let sut = DeserializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .Build() + + let person = sut.Deserialize(yaml) + Assert.Equal(jackTheDriver.name, person.name) + let numbers = person.numbers.ToArray() + Assert.Equal(3, numbers.Length) + Assert.Equal(12, numbers[0]) + Assert.Equal(2, numbers[1]) + Assert.Equal(2, numbers[2]) diff --git a/YamlDotNet.Fsharp.Test/SerializerTests.fs b/YamlDotNet.Fsharp.Test/SerializerTests.fs index e7ca88d11..a7bd209b2 100644 --- a/YamlDotNet.Fsharp.Test/SerializerTests.fs +++ b/YamlDotNet.Fsharp.Test/SerializerTests.fs @@ -4,8 +4,8 @@ open System open Xunit open YamlDotNet.Serialization open YamlDotNet.Serialization.NamingConventions -open FsUnit.Xunit open YamlDotNet.Core +open YamlDotNet.Fsharp.Test [] type Spec = { @@ -65,8 +65,7 @@ cars: .Build() let person = sut.Serialize(jackTheDriver) - person |> should equal yaml - + Assert.Equal(yaml.Clean(), person.Clean()) [] let Serialize_YamlWithScalarOptions_OmitNull() = @@ -102,8 +101,7 @@ cars: .Build() let person = sut.Serialize(jackTheDriver) - person |> should equal yaml - + Assert.Equal(yaml.Clean(), person.Clean()) [] let Serialize_YamlWithObjectOptions_OmitNull() = @@ -144,7 +142,7 @@ cars: .Build() let person = sut.Serialize(jackTheDriver) - person |> should equal yaml + Assert.Equal(yaml.Clean(), person.Clean()) [] type TestSeq = { @@ -171,8 +169,7 @@ numbers: .Build() let person = sut.Serialize(jackTheDriver) - person |> should equal yaml - + Assert.Equal(yaml.Clean(), person.Clean()) [] type TestList = { @@ -199,7 +196,7 @@ numbers: .Build() let person = sut.Serialize(jackTheDriver) - person |> should equal yaml + Assert.Equal(yaml.Clean(), person.Clean()) [] type TestArray = { @@ -226,4 +223,4 @@ numbers: .Build() let person = sut.Serialize(jackTheDriver) - person |> should equal yaml + Assert.Equal(yaml.Clean(), person.Clean()) diff --git a/YamlDotNet.Fsharp.Test/StringExtensions.fs b/YamlDotNet.Fsharp.Test/StringExtensions.fs new file mode 100644 index 000000000..5bf956d2d --- /dev/null +++ b/YamlDotNet.Fsharp.Test/StringExtensions.fs @@ -0,0 +1,15 @@ +namespace YamlDotNet.Fsharp.Test + +open System.Runtime.CompilerServices + +[] +type StringExtensions() = + [] + static member NormalizeNewLines(x: string) = + x.Replace("\r\n", "\n").Replace("\n", System.Environment.NewLine) + [] + static member TrimNewLines(x: string) = + x.TrimEnd('\r').TrimEnd('\n') + [] + static member Clean(x: string) = + x.NormalizeNewLines().TrimNewLines() diff --git a/YamlDotNet.Fsharp.Test/YamlDotNet.Fsharp.Test.fsproj b/YamlDotNet.Fsharp.Test/YamlDotNet.Fsharp.Test.fsproj index 300cec1f9..734929861 100644 --- a/YamlDotNet.Fsharp.Test/YamlDotNet.Fsharp.Test.fsproj +++ b/YamlDotNet.Fsharp.Test/YamlDotNet.Fsharp.Test.fsproj @@ -1,25 +1,35 @@ - net8.0;net7.0;net6.0;net47 + net8.0;net6.0;net47 false ..\YamlDotNet.snk true 8.0 true + true + - - - - - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/YamlDotNet.Samples.Fsharp/YamlDotNet.Samples.Fsharp.fsproj b/YamlDotNet.Samples.Fsharp/YamlDotNet.Samples.Fsharp.fsproj index 659afbbf6..e1f20065d 100644 --- a/YamlDotNet.Samples.Fsharp/YamlDotNet.Samples.Fsharp.fsproj +++ b/YamlDotNet.Samples.Fsharp/YamlDotNet.Samples.Fsharp.fsproj @@ -2,7 +2,7 @@ Exe - net7.0 + net8.0 false diff --git a/YamlDotNet.Samples/ValidatingDuringDeserialization.cs b/YamlDotNet.Samples/ValidatingDuringDeserialization.cs index 95f590b31..d97ee9df4 100644 --- a/YamlDotNet.Samples/ValidatingDuringDeserialization.cs +++ b/YamlDotNet.Samples/ValidatingDuringDeserialization.cs @@ -43,9 +43,9 @@ public ValidatingNodeDeserializer(INodeDeserializer nodeDeserializer) this.nodeDeserializer = nodeDeserializer; } - public bool Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object value) + public bool Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object value, ObjectDeserializer rootDeserializer) { - if (nodeDeserializer.Deserialize(parser, expectedType, nestedObjectDeserializer, out value)) + if (nodeDeserializer.Deserialize(parser, expectedType, nestedObjectDeserializer, out value, rootDeserializer)) { var context = new ValidationContext(value, null, null); Validator.ValidateObject(value, context, true); diff --git a/YamlDotNet.Test/Analyzers/StaticGenerator/ObjectTests.cs b/YamlDotNet.Test/Analyzers/StaticGenerator/ObjectTests.cs index aeee517bf..0a5e04256 100644 --- a/YamlDotNet.Test/Analyzers/StaticGenerator/ObjectTests.cs +++ b/YamlDotNet.Test/Analyzers/StaticGenerator/ObjectTests.cs @@ -19,9 +19,12 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +using System; +using System.Collections; using System.Collections.Generic; using Xunit; using YamlDotNet.Core; +using YamlDotNet.Core.Events; using YamlDotNet.Serialization; using YamlDotNet.Serialization.Callbacks; using YamlDotNet.Serialization.NamingConventions; @@ -106,6 +109,36 @@ public void RegularObjectWorks() Assert.Equal(yaml.NormalizeNewLines().TrimNewLines(), actualYaml.NormalizeNewLines().TrimNewLines()); } + [Fact] + public void EnumerablesAreTreatedAsLists() => ExecuteListOverrideTest(); + + [Fact] + public void CollectionsAreTreatedAsLists() => ExecuteListOverrideTest(); + + [Fact] + public void IListsAreTreatedAsLists() => ExecuteListOverrideTest(); + + [Fact] + public void ReadOnlyCollectionsAreTreatedAsLists() => ExecuteListOverrideTest(); + + [Fact] + public void ReadOnlyListsAreTreatedAsLists() => ExecuteListOverrideTest(); + + [Fact] + public void IListAreTreatedAsLists() + { + var yaml = @"Test: +- value1 +- value2 +"; + var deserializer = new StaticDeserializerBuilder(new StaticContext()).Build(); + var actual = deserializer.Deserialize(yaml); + Assert.NotNull(actual); + Assert.IsType>(actual.Test); + Assert.Equal("value1", ((List)actual.Test)[0]); + Assert.Equal("value2", ((List)actual.Test)[1]); + } + [Fact] public void CallbacksAreExecuted() { @@ -141,6 +174,185 @@ public void NamingConventionAppliedToEnumWhenDeserializing() Assert.Equal(expected, actual); } + [Fact] + public void ReadOnlyDictionariesAreTreatedAsDictionaries() + { + var yaml = @"Test: + a: b + c: d +"; + var deserializer = new StaticDeserializerBuilder(new StaticContext()).Build(); + var actual = deserializer.Deserialize(yaml); + Assert.NotNull(actual); + Assert.IsType>(actual.Test); + var dictionary = (Dictionary)actual.Test; + Assert.Equal(2, dictionary.Count); + Assert.Equal("b", dictionary["a"]); + Assert.Equal("d", dictionary["c"]); + } + +#if NET6_0_OR_GREATER + [Fact] + public void EnumDeserializationUsesEnumMemberAttribute() + { + var deserializer = new StaticDeserializerBuilder(new StaticContext()).Build(); + var yaml = "goodbye"; + var actual = deserializer.Deserialize(yaml); + Assert.Equal(EnumMemberedEnum.Hello, actual); + } + + [Fact] + public void EnumSerializationUsesEnumMemberAttribute() + { + var serializer = new StaticSerializerBuilder(new StaticContext()).Build(); + var actual = serializer.Serialize(EnumMemberedEnum.Hello); + Assert.Equal("goodbye", actual.TrimNewLines()); + } + + [YamlSerializable] + public enum EnumMemberedEnum + { + No = 0, + + [System.Runtime.Serialization.EnumMember(Value = "goodbye")] + Hello = 1 + } +#endif + [Fact] + public void ComplexTypeConverter_UsesSerializerToSerializeComplexTypes() + { + var serializer = new StaticSerializerBuilder(new StaticContext()).WithTypeConverter(new ComplexTypeConverter()).Build(); + var o = new ComplexType + { + InnerType1 = new InnerType + { + Prop1 = "prop1", + Prop2 = "prop2" + }, + InnerType2 = new InnerType + { + Prop1 = "2.1", + Prop2 = "2.2" + } + }; + var actual = serializer.Serialize(o); + var expected = @"inner.prop1: prop1 +inner.prop2: prop2 +prop2: + Prop1: 2.1 + Prop2: 2.2".NormalizeNewLines(); + Assert.Equal(expected, actual.NormalizeNewLines().TrimNewLines()); + } + + [Fact] + public void ComplexTypeConverter_UsesDeserializerToDeserializeComplexTypes() + { + var deserializer = new StaticDeserializerBuilder(new StaticContext()).WithTypeConverter(new ComplexTypeConverter()).Build(); + var yaml = @"inner.prop1: prop1 +inner.prop2: prop2 +prop2: + Prop1: 2.1 + Prop2: 2.2"; + var actual = deserializer.Deserialize(yaml); + Assert.Equal("prop1", actual.InnerType1.Prop1); + Assert.Equal("prop2", actual.InnerType1.Prop2); + Assert.Equal("2.1", actual.InnerType2.Prop1); + Assert.Equal("2.2", actual.InnerType2.Prop2); + } + + [YamlSerializable] + public class ComplexType + { + public InnerType InnerType1 { get; set; } + public InnerType InnerType2 { get; set; } + } + + [YamlSerializable] + public class InnerType + { + public string Prop1 { get; set; } + public string Prop2 { get; set; } + } + + private class ComplexTypeConverter : IYamlTypeConverter + { + public bool Accepts(Type type) + { + if (type == typeof(ComplexType)) + { + return true; + } + return false; + } + + public object ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) + { + parser.Consume(); + + var result = new ComplexType(); + result.InnerType1 = new InnerType(); + + Consume(parser, result, rootDeserializer); + Consume(parser, result, rootDeserializer); + Consume(parser, result, rootDeserializer); + + parser.Consume(); + + return result; + } + + public void WriteYaml(IEmitter emitter, object value, Type type, ObjectSerializer serializer) + { + var c = (ComplexType)value; + emitter.Emit(new MappingStart()); + emitter.Emit(new Scalar("inner.prop1")); + emitter.Emit(new Scalar(c.InnerType1.Prop1)); + emitter.Emit(new Scalar("inner.prop2")); + emitter.Emit(new Scalar(c.InnerType1.Prop2)); + emitter.Emit(new Scalar("prop2")); + serializer(c.InnerType2); + emitter.Emit(new MappingEnd()); + } + + private void Consume(IParser parser, ComplexType type, ObjectDeserializer deserializer) + { + var name = parser.Consume(); + if (name.Value == "inner.prop1") + { + var value = parser.Consume(); + type.InnerType1.Prop1 = value.Value; + } + else if (name.Value == "inner.prop2") + { + var value = parser.Consume(); + type.InnerType1.Prop2 = value.Value; + } + else if (name.Value == "prop2") + { + var value = deserializer(typeof(InnerType)); + type.InnerType2 = (InnerType)value; + } + else + { + throw new Exception("Invalid property name"); + } + } + } + + private void ExecuteListOverrideTest() where TClass : InterfaceLists + { + var yaml = @"Test: +- value1 +- value2 +"; + var deserializer = new StaticDeserializerBuilder(new StaticContext()).Build(); + var actual = deserializer.Deserialize(yaml); + Assert.NotNull(actual); + Assert.IsType>(actual.TestValue); + Assert.Equal("value1", ((List)actual.TestValue)[0]); + Assert.Equal("value2", ((List)actual.TestValue)[1]); + } + [YamlSerializable] public class TestState { @@ -207,4 +419,55 @@ public class RegularObjectInner public string Prop1 { get; set; } public int Prop2 { get; set; } } + + [YamlSerializable] + public class EnumerableClass : InterfaceLists> + { + public IEnumerable Test { get; set; } + public object TestValue => Test; + } + + [YamlSerializable] + public class CollectionClass : InterfaceLists> + { + public ICollection Test { get; set; } + public object TestValue => Test; + } + + [YamlSerializable] + public class ListClass : InterfaceLists> + { + public IList Test { get; set; } + public object TestValue => Test; + } + + [YamlSerializable] + public class ReadOnlyCollectionClass : InterfaceLists> + { + public IReadOnlyCollection Test { get; set; } + public object TestValue => Test; + } + + [YamlSerializable] + public class ReadOnlyListClass : InterfaceLists> + { + public IReadOnlyList Test { get; set; } + public object TestValue => Test; + } + + [YamlSerializable] + public class ReadOnlyDictionaryClass + { + public IReadOnlyDictionary Test { get; set; } + } + + public interface InterfaceLists : InterfaceLists where TType : IEnumerable + { + TType Test { get; set; } + } + + public interface InterfaceLists + { + object TestValue { get; } + } } diff --git a/YamlDotNet.Test/Serialization/ComplexYamlTypeConverterTests.cs b/YamlDotNet.Test/Serialization/ComplexYamlTypeConverterTests.cs new file mode 100644 index 000000000..ea72a2137 --- /dev/null +++ b/YamlDotNet.Test/Serialization/ComplexYamlTypeConverterTests.cs @@ -0,0 +1,151 @@ +// This file is part of YamlDotNet - A .NET library for YAML. +// Copyright (c) Antoine Aubry and contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System; +using Xunit; +using YamlDotNet.Core; +using YamlDotNet.Core.Events; +using YamlDotNet.Serialization; + +namespace YamlDotNet.Test.Serialization +{ + public class ComplexYamlTypeConverterTests + { + [Fact] + public void ComplexTypeConverter_UsesSerializerToSerializeComplexTypes() + { + var serializer = new SerializerBuilder().WithTypeConverter(new ComplexTypeConverter()).Build(); + var o = new ComplexType + { + InnerType1 = new InnerType + { + Prop1 = "prop1", + Prop2 = "prop2" + }, + InnerType2 = new InnerType + { + Prop1 = "2.1", + Prop2 = "2.2" + } + }; + var actual = serializer.Serialize(o); + var expected = @"inner.prop1: prop1 +inner.prop2: prop2 +prop2: + Prop1: 2.1 + Prop2: 2.2".NormalizeNewLines(); + Assert.Equal(expected, actual.NormalizeNewLines().TrimNewLines()); + } + + [Fact] + public void ComplexTypeConverter_UsesDeserializerToDeserializeComplexTypes() + { + var deserializer = new DeserializerBuilder().WithTypeConverter(new ComplexTypeConverter()).Build(); + var yaml = @"inner.prop1: prop1 +inner.prop2: prop2 +prop2: + Prop1: 2.1 + Prop2: 2.2"; + var actual = deserializer.Deserialize(yaml); + Assert.Equal("prop1", actual.InnerType1.Prop1); + Assert.Equal("prop2", actual.InnerType1.Prop2); + Assert.Equal("2.1", actual.InnerType2.Prop1); + Assert.Equal("2.2", actual.InnerType2.Prop2); + } + + private class ComplexType + { + public InnerType InnerType1 { get; set; } + public InnerType InnerType2 { get; set; } + } + + private class InnerType + { + public string Prop1 { get; set; } + public string Prop2 { get; set; } + } + + private class ComplexTypeConverter : IYamlTypeConverter + { + public bool Accepts(Type type) + { + if (type == typeof(ComplexType)) + { + return true; + } + return false; + } + + public object ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) + { + parser.Consume(); + + var result = new ComplexType(); + result.InnerType1 = new InnerType(); + + Consume(parser, result, rootDeserializer); + Consume(parser, result, rootDeserializer); + Consume(parser, result, rootDeserializer); + + parser.Consume(); + + return result; + } + + public void WriteYaml(IEmitter emitter, object value, Type type, ObjectSerializer serializer) + { + var c = (ComplexType)value; + emitter.Emit(new MappingStart()); + emitter.Emit(new Scalar("inner.prop1")); + emitter.Emit(new Scalar(c.InnerType1.Prop1)); + emitter.Emit(new Scalar("inner.prop2")); + emitter.Emit(new Scalar(c.InnerType1.Prop2)); + emitter.Emit(new Scalar("prop2")); + serializer(c.InnerType2); + emitter.Emit(new MappingEnd()); + } + + private void Consume(IParser parser, ComplexType type, ObjectDeserializer deserializer) + { + var name = parser.Consume(); + if (name.Value == "inner.prop1") + { + var value = parser.Consume(); + type.InnerType1.Prop1 = value.Value; + } + else if (name.Value == "inner.prop2") + { + var value = parser.Consume(); + type.InnerType1.Prop2 = value.Value; + } + else if (name.Value == "prop2") + { + var value = deserializer(typeof(InnerType)); + type.InnerType2 = (InnerType)value; + } + else + { + throw new Exception("Invalid property name"); + } + } + } + } +} diff --git a/YamlDotNet.Test/Serialization/DateOnlyConverterTests.cs b/YamlDotNet.Test/Serialization/DateOnlyConverterTests.cs index dbfce3b01..036bfebea 100644 --- a/YamlDotNet.Test/Serialization/DateOnlyConverterTests.cs +++ b/YamlDotNet.Test/Serialization/DateOnlyConverterTests.cs @@ -73,7 +73,7 @@ public void Given_Yaml_WithInvalidDateTimeFormat_WithDefaultParameter_ReadYaml_S var converter = new DateOnlyConverter(); - Action action = () => { converter.ReadYaml(parser, typeof(DateOnly)); }; + Action action = () => { converter.ReadYaml(parser, typeof(DateOnly), null); }; action.ShouldThrow(); } @@ -96,7 +96,7 @@ public void Given_Yaml_WithValidDateTimeFormat_WithDefaultParameter_ReadYaml_Sho var converter = new DateOnlyConverter(); - var result = converter.ReadYaml(parser, typeof(DateOnly)); + var result = converter.ReadYaml(parser, typeof(DateOnly), null); result.Should().BeOfType(); ((DateOnly)result).Year.Should().Be(year); @@ -123,7 +123,7 @@ public void Given_Yaml_WithValidDateTimeFormat_ReadYaml_ShouldReturn_Result(int var converter = new DateOnlyConverter(formats: new[] { format1, format2 }); - var result = converter.ReadYaml(parser, typeof(DateOnly)); + var result = converter.ReadYaml(parser, typeof(DateOnly), null); result.Should().BeOfType(); ((DateOnly)result).Year.Should().Be(year); @@ -151,7 +151,7 @@ public void Given_Yaml_WithSpecificCultureAndValidDateTimeFormat_ReadYaml_Should var culture = new CultureInfo("ko-KR"); // Sample specific culture var converter = new DateOnlyConverter(provider: culture, formats: new[] { format1, format2 }); - var result = converter.ReadYaml(parser, typeof(DateOnly)); + var result = converter.ReadYaml(parser, typeof(DateOnly), null); result.Should().BeOfType(); ((DateOnly)result).Year.Should().Be(year); @@ -183,7 +183,7 @@ public void Given_Yaml_WithDateTimeFormat_ReadYaml_ShouldReturn_Result(string fo var parser = A.Fake(); A.CallTo(() => parser.Current).ReturnsLazily(() => new Scalar(value)); - var result = converter.ReadYaml(parser, typeof(DateOnly)); + var result = converter.ReadYaml(parser, typeof(DateOnly), null); result.Should().Be(expected); } @@ -235,7 +235,7 @@ public void Given_Yaml_WithLocaleAndDateTimeFormat_ReadYaml_ShouldReturn_Result( var parser = A.Fake(); A.CallTo(() => parser.Current).ReturnsLazily(() => new Scalar(value)); - var result = converter.ReadYaml(parser, typeof(DateOnly)); + var result = converter.ReadYaml(parser, typeof(DateOnly), null); result.Should().Be(expected); } diff --git a/YamlDotNet.Test/Serialization/DateTime8601ConverterTests.cs b/YamlDotNet.Test/Serialization/DateTime8601ConverterTests.cs new file mode 100644 index 000000000..743c55a6a --- /dev/null +++ b/YamlDotNet.Test/Serialization/DateTime8601ConverterTests.cs @@ -0,0 +1,69 @@ +// This file is part of YamlDotNet - A .NET library for YAML. +// Copyright (c) Antoine Aubry and contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using FakeItEasy; +using Xunit; +using YamlDotNet.Core; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.Converters; + +namespace YamlDotNet.Test.Serialization +{ + public class DateTime8601ConverterTests + { + [Fact] + public void Uses8601TimeFormat_UTC() + { + var serializer = new SerializerBuilder() + .WithTypeConverter(new DateTime8601Converter(ScalarStyle.Plain)) + .Build(); + var actual = serializer.Serialize(new DateTime(2024, 07, 11, 18, 05, 07, DateTimeKind.Utc)).TrimNewLines(); + Assert.Equal("2024-07-11T18:05:07.0000000Z", actual); + } + + [Fact] + public void Uses8601TimeFormat_Unspecified() + { + var serializer = new SerializerBuilder() + .WithTypeConverter(new DateTime8601Converter(ScalarStyle.Plain)) + .Build(); + var actual = serializer.Serialize(new DateTime(2024, 07, 11, 18, 05, 07)).TrimNewLines(); + Assert.Equal("2024-07-11T18:05:07.0000000", actual); + } + + [Fact] + public void Uses8601TimeFormat_Local() + { + var serializer = new SerializerBuilder() + .WithTypeConverter(new DateTime8601Converter(ScalarStyle.DoubleQuoted)) + .Build(); + var dt = new DateTime(2024, 07, 11, 18, 05, 07, DateTimeKind.Local); + var actual = serializer.Serialize(dt).TrimNewLines(); + + Assert.Equal($"\"{dt.ToString("O")}\"", actual); + } + } +} diff --git a/YamlDotNet.Test/Serialization/DateTimeConverterTests.cs b/YamlDotNet.Test/Serialization/DateTimeConverterTests.cs index 877094dd1..bb741250e 100644 --- a/YamlDotNet.Test/Serialization/DateTimeConverterTests.cs +++ b/YamlDotNet.Test/Serialization/DateTimeConverterTests.cs @@ -72,7 +72,7 @@ public void Given_Yaml_WithInvalidDateTimeFormat_WithDefaultParameters_ReadYaml_ var converter = new DateTimeConverter(); - Action action = () => { converter.ReadYaml(parser, typeof(DateTime)); }; + Action action = () => { converter.ReadYaml(parser, typeof(DateTime), null); }; action.ShouldThrow(); } @@ -98,7 +98,7 @@ public void Given_Yaml_WithValidDateTimeFormat_WithDefaultParameters_ReadYaml_Sh var converter = new DateTimeConverter(); - var result = converter.ReadYaml(parser, typeof(DateTime)); + var result = converter.ReadYaml(parser, typeof(DateTime), null); result.Should().BeOfType(); ((DateTime)result).Kind.Should().Be(DateTimeKind.Utc); @@ -131,7 +131,7 @@ public void Given_Yaml_WithValidDateTimeFormat_WithDefaultParameterAndUnspecifie var converter = new DateTimeConverter(DateTimeKind.Unspecified); - var result = converter.ReadYaml(parser, typeof(DateTime)); + var result = converter.ReadYaml(parser, typeof(DateTime), null); result.Should().BeOfType(); ((DateTime)result).Kind.Should().Be(DateTimeKind.Utc); @@ -164,7 +164,7 @@ public void Given_Yaml_WithValidDateTimeFormat_WithDefaultParameterAndLocal_Read var converter = new DateTimeConverter(DateTimeKind.Local); - var result = converter.ReadYaml(parser, typeof(DateTime)); + var result = converter.ReadYaml(parser, typeof(DateTime), null); result.Should().BeOfType(); ((DateTime)result).Kind.Should().Be(DateTimeKind.Local); @@ -196,7 +196,7 @@ public void Given_Yaml_WithValidDateTimeFormat_ReadYaml_ShouldReturn_Result(int var converter = new DateTimeConverter(formats: new[] { format1, format2 }); - var result = converter.ReadYaml(parser, typeof(DateTime)); + var result = converter.ReadYaml(parser, typeof(DateTime), null); result.Should().BeOfType(); ((DateTime)result).Kind.Should().Be(DateTimeKind.Utc); @@ -229,7 +229,7 @@ public void Given_Yaml_WithSpecificCultureAndValidDateTimeFormat_ReadYaml_Should var culture = new CultureInfo("ko-KR"); // Sample specific culture var converter = new DateTimeConverter(provider: culture, formats: new[] { format1, format2 }); - var result = converter.ReadYaml(parser, typeof(DateTime)); + var result = converter.ReadYaml(parser, typeof(DateTime), null); result.Should().BeOfType(); ((DateTime)result).Kind.Should().Be(DateTimeKind.Utc); @@ -261,7 +261,7 @@ public void Given_Yaml_WithValidDateTimeFormatAndUnspecified_ReadYaml_ShouldRetu var converter = new DateTimeConverter(DateTimeKind.Unspecified, formats: new[] { format1, format2 }); - var result = converter.ReadYaml(parser, typeof(DateTime)); + var result = converter.ReadYaml(parser, typeof(DateTime), null); result.Should().BeOfType(); ((DateTime)result).Kind.Should().Be(DateTimeKind.Utc); @@ -293,7 +293,7 @@ public void Given_Yaml_WithValidDateTimeFormatAndLocal_ReadYaml_ShouldReturn_Res var converter = new DateTimeConverter(DateTimeKind.Local, formats: new[] { format1, format2 }); - var result = converter.ReadYaml(parser, typeof(DateTime)); + var result = converter.ReadYaml(parser, typeof(DateTime), null); result.Should().BeOfType(); ((DateTime)result).Kind.Should().Be(DateTimeKind.Local); @@ -335,7 +335,7 @@ public void Given_Yaml_WithTimeFormat_ReadYaml_ShouldReturn_Result(string format var parser = A.Fake(); A.CallTo(() => parser.Current).ReturnsLazily(() => new Scalar(value)); - var result = converter.ReadYaml(parser, typeof(DateTime)); + var result = converter.ReadYaml(parser, typeof(DateTime), null); result.Should().Be(expected); } @@ -398,7 +398,7 @@ public void Given_Yaml_WithLocaleAndTimeFormat_ReadYaml_ShouldReturn_Result(stri var parser = A.Fake(); A.CallTo(() => parser.Current).ReturnsLazily(() => new Scalar(value)); - var result = converter.ReadYaml(parser, typeof(DateTime)); + var result = converter.ReadYaml(parser, typeof(DateTime), null); result.Should().Be(expected); } @@ -432,7 +432,7 @@ public void Given_Yaml_WithTimeFormatAndLocal1_ReadYaml_ShouldReturn_Result(stri var parser = A.Fake(); A.CallTo(() => parser.Current).ReturnsLazily(() => new Scalar(value)); - var result = converter.ReadYaml(parser, typeof(DateTime)); + var result = converter.ReadYaml(parser, typeof(DateTime), null); result.Should().Be(expected); } @@ -466,7 +466,7 @@ public void Given_Yaml_WithTimeFormatAndLocal2_ReadYaml_ShouldReturn_Result(stri var parser = A.Fake(); A.CallTo(() => parser.Current).ReturnsLazily(() => new Scalar(value)); - var result = converter.ReadYaml(parser, typeof(DateTime)); + var result = converter.ReadYaml(parser, typeof(DateTime), null); result.Should().Be(expected); } @@ -564,16 +564,6 @@ public void Given_Values_WithFormats_WriteYaml_ShouldReturn_Result_WithFirstForm serialised.Should().ContainEquivalentOf($"datetime: {formatted}"); } - - [Fact] - public void JsonCompatible_EncaseDateTimesInDoubleQuotes() - { - var serializer = new SerializerBuilder().JsonCompatible().Build(); - var testObject = new TestObject { DateTime = new DateTime(2023, 01, 14, 0, 1, 2, DateTimeKind.Utc) }; - var actual = serializer.Serialize(testObject); - - actual.TrimNewLines().Should().ContainEquivalentOf("{\"DateTime\": \"01/14/2023 00:01:02\"}"); - } } /// diff --git a/YamlDotNet.Test/Serialization/DateTimeOffsetConverterTests.cs b/YamlDotNet.Test/Serialization/DateTimeOffsetConverterTests.cs index 8bfbb2222..cafe0817d 100644 --- a/YamlDotNet.Test/Serialization/DateTimeOffsetConverterTests.cs +++ b/YamlDotNet.Test/Serialization/DateTimeOffsetConverterTests.cs @@ -64,7 +64,7 @@ public void InvalidFormatThrowsException() var converter = new DateTimeOffsetConverter(CultureInfo.InvariantCulture); - Action action = () => { converter.ReadYaml(parser, typeof(DateTimeOffset)); }; + Action action = () => { converter.ReadYaml(parser, typeof(DateTimeOffset), null); }; action.ShouldThrow(); } @@ -77,7 +77,7 @@ public void ValidYamlReturnsDateTimeOffsetDefaultFormat() var parser = A.Fake(); A.CallTo(() => parser.Current).ReturnsLazily(() => new Scalar(yaml)); var converter = new DateTimeOffsetConverter(CultureInfo.InvariantCulture); - var actual = converter.ReadYaml(parser, typeof(DateTimeOffset)); + var actual = converter.ReadYaml(parser, typeof(DateTimeOffset), null); Assert.Equal(_expected, actual); } @@ -96,8 +96,8 @@ public void ValidYamlReturnsDateTimeOffsetAdditionalFormats() "O", "MM/dd/yyyy HH:mm:ss zzz"); - converter.ReadYaml(parser, typeof(DateTimeOffset)); - var actual = converter.ReadYaml(parser, typeof(DateTimeOffset)); + converter.ReadYaml(parser, typeof(DateTimeOffset), null); + var actual = converter.ReadYaml(parser, typeof(DateTimeOffset), null); Assert.Equal(_expected, actual); } diff --git a/YamlDotNet.Test/Serialization/DeserializerTest.cs b/YamlDotNet.Test/Serialization/DeserializerTest.cs index 2c6bb3033..cf8809e10 100644 --- a/YamlDotNet.Test/Serialization/DeserializerTest.cs +++ b/YamlDotNet.Test/Serialization/DeserializerTest.cs @@ -110,36 +110,6 @@ public void Deserialize_YamlWithTwoInterfaceTypesAndMappings_ReturnsModel() person.Cars[1].Spec.DriveType.Should().Be("FWD"); } - [Fact] - public void Deserialize_YamlWithCircularReferenceInArray_ReturnsModel() - { - var yaml = @" -sessions: -- &s001_FunWithLetters - name: Fun With Letters - greeter: &a001_Jill - name: Jill - sessions: - - *s001_FunWithLetters -attendees: -- *a001_Jill -"; - - var sut = new DeserializerBuilder() - .WithNamingConvention(CamelCaseNamingConvention.Instance) - .Build(); - - var conference = sut.Deserialize(yaml); - var jill = conference.Attendees.Single(a => a.Name == "Jill"); - var session = conference.Sessions.Single(s => s.Name == "Fun With Letters"); - - jill.Should().NotBeNull(); - jill.Sessions.ShouldBeEquivalentTo(new[] { session }); - - session.Should().NotBeNull(); - session.Greeter.Should().Be(jill); - } - [Fact] public void SetterOnlySetsWithoutException() { @@ -363,6 +333,38 @@ public void DeserializeWithoutDuplicateKeyChecking_YamlWithDuplicateKeys_DoesNot act.ShouldNotThrow("Because duplicate key checking is not enabled"); } + [Fact] + public void EnforceNulalbleTypesWhenNullThrowsException() + { + var deserializer = new DeserializerBuilder().WithEnforceNullability().Build(); + var yaml = @" +Test: null +"; + try + { + var o = deserializer.Deserialize(yaml); + } + catch (YamlException e) + { + if (e.InnerException is NullReferenceException) + { + return; + } + } + + throw new Exception("Non nullable property was set to null."); + } + + [Fact] + public void EnforceNullableTypesWhenNotNullDoesNotThrowException() + { + var deserializer = new DeserializerBuilder().WithEnforceNullability().Build(); + var yaml = @" +Test: test 123 +"; + var o = deserializer.Deserialize(yaml); + } + [Fact] public void SerializeStateMethodsGetCalledOnce() { @@ -374,6 +376,92 @@ public void SerializeStateMethodsGetCalledOnce() Assert.Equal(1, test.OnDeserializingCallCount); } + [Fact] + public void WithCaseInsensitivePropertyMatching_IgnoreCase() + { + var yaml = @"PrOpErTy: Value +fIeLd: Value +"; + var deserializer = new DeserializerBuilder().WithCaseInsensitivePropertyMatching().Build(); + var test = deserializer.Deserialize(yaml); + Assert.Equal("Value", test.Property); + Assert.Equal("Value", test.Field); + } + +#if NET8_0_OR_GREATER + [Fact] + public void WithRequiredMemberSet_ThrowsWhenFieldNotSet() + { + var deserializer = new DeserializerBuilder().WithEnforceRequiredMembers().Build(); + var yaml = "Property: test"; + Assert.Throws(() => + { + deserializer.Deserialize(yaml); + }); + } + + [Fact] + public void WithRequiredMemberSet_ThrowsWhenPropertyNotSet() + { + var deserializer = new DeserializerBuilder().WithEnforceRequiredMembers().Build(); + var yaml = "Field: test"; + Assert.Throws(() => + { + deserializer.Deserialize(yaml); + }); + } + + [Fact] + public void WithRequiredMemberSet_DoesNotThrow() + { + var deserializer = new DeserializerBuilder().WithEnforceRequiredMembers().Build(); + var yaml = @"Field: test-field +Property: test-property"; + var actual = deserializer.Deserialize(yaml); + Assert.Equal("test-field", actual.Field); + Assert.Equal("test-property", actual.Property); + } + + public class RequiredMemberClass + { + public required string Field = string.Empty; + public required string Property { get; set; } = string.Empty; + } +#endif + +#if NET6_0_OR_GREATER + [Fact] + public void EnumDeserializationUsesEnumMemberAttribute() + { + var deserializer = new DeserializerBuilder().Build(); + var yaml = "goodbye"; + var actual = deserializer.Deserialize(yaml); + Assert.Equal(EnumMemberedEnum.Hello, actual); + } + + public enum EnumMemberedEnum + { + No = 0, + + [System.Runtime.Serialization.EnumMember(Value = "goodbye")] + Hello = 1 + + } +#endif + +#nullable enable + public class NonNullableClass + { + public string Test { get; set; } = "Some default value"; + } +#nullable disable + + public class CaseInsensitiveTest + { + public string Property { get; set; } + public string Field; + } + public class TestState { public int OnDeserializedCallCount { get; set; } diff --git a/YamlDotNet.Test/Serialization/RepresentationModelSerializationTests.cs b/YamlDotNet.Test/Serialization/RepresentationModelSerializationTests.cs index 7bd567b56..938e73775 100644 --- a/YamlDotNet.Test/Serialization/RepresentationModelSerializationTests.cs +++ b/YamlDotNet.Test/Serialization/RepresentationModelSerializationTests.cs @@ -105,7 +105,7 @@ public bool Accepts(Type type) return type == typeof(byte[]); } - public object ReadYaml(IParser parser, Type type) + public object ReadYaml(IParser parser, Type type, ObjectDeserializer deserializer) { var scalar = (YamlDotNet.Core.Events.Scalar)parser.Current; var bytes = Convert.FromBase64String(scalar.Value); @@ -113,7 +113,7 @@ public object ReadYaml(IParser parser, Type type) return bytes; } - public void WriteYaml(IEmitter emitter, object value, Type type) + public void WriteYaml(IEmitter emitter, object value, Type type, ObjectSerializer serializer) { var bytes = (byte[])value; emitter.Emit(new YamlDotNet.Core.Events.Scalar( diff --git a/YamlDotNet.Test/Serialization/SerializationTestHelper.cs b/YamlDotNet.Test/Serialization/SerializationTestHelper.cs index 2ff42fea4..4eafe01ae 100644 --- a/YamlDotNet.Test/Serialization/SerializationTestHelper.cs +++ b/YamlDotNet.Test/Serialization/SerializationTestHelper.cs @@ -304,14 +304,14 @@ public bool Accepts(Type type) return type == typeof(MissingDefaultCtor); } - public object ReadYaml(IParser parser, Type type) + public object ReadYaml(IParser parser, Type type, ObjectDeserializer deserializer) { var value = ((Scalar)parser.Current).Value; parser.MoveNext(); return new MissingDefaultCtor(value); } - public void WriteYaml(IEmitter emitter, object value, Type type) + public void WriteYaml(IEmitter emitter, object value, Type type, ObjectSerializer serializer) { emitter.Emit(new Scalar(((MissingDefaultCtor)value).Value)); } diff --git a/YamlDotNet.Test/Serialization/SerializationTests.cs b/YamlDotNet.Test/Serialization/SerializationTests.cs index 84af7a64a..0d619904a 100644 --- a/YamlDotNet.Test/Serialization/SerializationTests.cs +++ b/YamlDotNet.Test/Serialization/SerializationTests.cs @@ -1,2598 +1,2614 @@ -// This file is part of YamlDotNet - A .NET library for YAML. -// Copyright (c) Antoine Aubry and contributors -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of -// this software and associated documentation files (the "Software"), to deal in -// the Software without restriction, including without limitation the rights to -// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -// of the Software, and to permit persons to whom the Software is furnished to do -// so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -using System; -using System.Collections; -using System.Collections.Generic; -using System.ComponentModel; -using System.Drawing; -using System.Dynamic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Text.RegularExpressions; -using FakeItEasy; -using FluentAssertions; -using FluentAssertions.Common; -using Xunit; -using YamlDotNet.Core; -using YamlDotNet.Core.Events; -using YamlDotNet.Serialization; -using YamlDotNet.Serialization.Callbacks; -using YamlDotNet.Serialization.NamingConventions; -using YamlDotNet.Serialization.ObjectFactories; - -namespace YamlDotNet.Test.Serialization -{ - public class SerializationTests : SerializationTestHelper - { - #region Test Cases - - private static readonly string[] TrueStrings = { "true", "y", "yes", "on" }; - private static readonly string[] FalseStrings = { "false", "n", "no", "off" }; - - public static IEnumerable DeserializeScalarBoolean_TestCases - { - get - { - foreach (var trueString in TrueStrings) - { - yield return new object[] { trueString, true }; - yield return new object[] { trueString.ToUpper(), true }; - } - - foreach (var falseString in FalseStrings) - { - yield return new object[] { falseString, false }; - yield return new object[] { falseString.ToUpper(), false }; - } - } - } - - #endregion - - [Fact] - public void DeserializeEmptyDocument() - { - var emptyText = string.Empty; - - var array = Deserializer.Deserialize(UsingReaderFor(emptyText)); - - array.Should().BeNull(); - } - - [Fact] - public void DeserializeScalar() - { - var stream = Yaml.ReaderFrom("02-scalar-in-imp-doc.yaml"); - - var result = Deserializer.Deserialize(stream); - - result.Should().Be("a scalar"); - } - - [Theory] - [MemberData(nameof(DeserializeScalarBoolean_TestCases))] - public void DeserializeScalarBoolean(string value, bool expected) - { - var result = Deserializer.Deserialize(UsingReaderFor(value)); - - result.Should().Be(expected); - } - - [Fact] - public void DeserializeScalarBooleanThrowsWhenInvalid() - { - Action action = () => Deserializer.Deserialize(UsingReaderFor("not-a-boolean")); - - action.ShouldThrow().WithInnerException(); - } - - [Fact] - public void DeserializeScalarZero() - { - var result = Deserializer.Deserialize(UsingReaderFor("0")); - - result.Should().Be(0); - } - - [Fact] - public void DeserializeScalarDecimal() - { - var result = Deserializer.Deserialize(UsingReaderFor("+1_234_567")); - - result.Should().Be(1234567); - } - - [Fact] - public void DeserializeScalarBinaryNumber() - { - var result = Deserializer.Deserialize(UsingReaderFor("-0b1_0010_1001_0010")); - - result.Should().Be(-4754); - } - - [Fact] - public void DeserializeScalarOctalNumber() - { - var result = Deserializer.Deserialize(UsingReaderFor("+071_352")); - - result.Should().Be(29418); - } - - [Fact] - public void DeserializeNullableScalarOctalNumber() - { - var result = Deserializer.Deserialize(UsingReaderFor("+071_352")); - - result.Should().Be(29418); - } - - [Fact] - public void DeserializeScalarHexNumber() - { - var result = Deserializer.Deserialize(UsingReaderFor("-0x_0F_B9")); - - result.Should().Be(-0xFB9); - } - - [Fact] - public void DeserializeScalarLongBase60Number() - { - var result = Deserializer.Deserialize(UsingReaderFor("99_:_58:47:3:6_2:10")); - - result.Should().Be(77744246530L); - } - - [Theory] - [InlineData(EnumExample.One)] - [InlineData(EnumExample.One | EnumExample.Two)] - public void RoundtripEnums(EnumExample value) - { - var result = DoRoundtripFromObjectTo(value); - - result.Should().Be(value); - } - - [Theory] - [InlineData(EnumExample.One)] - [InlineData(EnumExample.One | EnumExample.Two)] - [InlineData(null)] - public void RoundtripNullableEnums(EnumExample? value) - { - var result = DoRoundtripFromObjectTo(value); - - result.Should().Be(value); - } - - [Fact] - public void RoundtripNullableStructWithValue() - { - var value = new StructExample { Value = 2 }; - - var result = DoRoundtripFromObjectTo(value); - - result.Should().Be(value); - } - - [Fact] - public void RoundtripNullableStructWithoutValue() - { - var result = DoRoundtripFromObjectTo(null); - - result.Should().Be(null); - } - - [Fact] - public void SerializeCircularReference() - { - var obj = new CircularReference(); - obj.Child1 = new CircularReference - { - Child1 = obj, - Child2 = obj - }; - - Action action = () => SerializerBuilder.EnsureRoundtrip().Build().Serialize(new StringWriter(), obj, typeof(CircularReference)); - - action.ShouldNotThrow(); - } - - [Fact] - public void DeserializeIncompleteDirective() - { - Action action = () => Deserializer.Deserialize(UsingReaderFor("%Y")); - - action.ShouldThrow() - .WithMessage("While scanning a directive, found unexpected end of stream."); - } - - [Fact] - public void DeserializeSkippedReservedDirective() - { - Action action = () => Deserializer.Deserialize(UsingReaderFor("%Y ")); - - action.ShouldNotThrow(); - } - - [Fact] - public void DeserializeCustomTags() - { - var stream = Yaml.ReaderFrom("tags.yaml"); - - DeserializerBuilder.WithTagMapping("tag:yaml.org,2002:point", typeof(Point)); - var result = Deserializer.Deserialize(stream); - - result.Should().BeOfType().And - .Subject.As() - .ShouldBeEquivalentTo(new { X = 10, Y = 20 }, o => o.ExcludingMissingMembers()); - } - - [Fact] - public void DeserializeWithGapsBetweenKeys() - { - var yamlReader = new StringReader(@"Text: > - Some Text. - -Value: foo"); - var result = Deserializer.Deserialize(yamlReader); - - result.Should().NotBeNull(); - } - - [Fact] - public void SerializeCustomTags() - { - var expectedResult = Yaml.ReaderFrom("tags.yaml").ReadToEnd().NormalizeNewLines(); - SerializerBuilder - .ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitDefaults) - .WithTagMapping(new TagName("tag:yaml.org,2002:point"), typeof(Point)); - - var point = new Point(10, 20); - var result = Serializer.Serialize(point); - - result.Should().Be(expectedResult); - } - - [Fact] - public void SerializeWithCRLFNewLine() - { - var expectedResult = Yaml - .ReaderFrom("list.yaml") - .ReadToEnd() - .NormalizeNewLines() - .Replace(Environment.NewLine, "\r\n"); - - var list = new string[] { "one", "two", "three" }; - var result = SerializerBuilder - .WithNewLine("\r\n") - .Build() - .Serialize(list); - - result.Should().Be(expectedResult); - } - - [Fact] - public void SerializeWithLFNewLine() - { - var expectedResult = Yaml - .ReaderFrom("list.yaml") - .ReadToEnd() - .NormalizeNewLines() - .Replace(Environment.NewLine, "\n"); - - var list = new string[] { "one", "two", "three" }; - var result = SerializerBuilder - .WithNewLine("\n") - .Build() - .Serialize(list); - - result.Should().Be(expectedResult); - } - - [Fact] - public void SerializeWithCRNewLine() - { - var expectedResult = Yaml - .ReaderFrom("list.yaml") - .ReadToEnd() - .NormalizeNewLines() - .Replace(Environment.NewLine, "\r"); - - var list = new string[] { "one", "two", "three" }; - var result = SerializerBuilder - .WithNewLine("\r") - .Build() - .Serialize(list); - - result.Should().Be(expectedResult); - } - - [Fact] - public void DeserializeExplicitType() - { - var text = Yaml.ReaderFrom("explicit-type.template").TemplatedOn(); - - var result = new DeserializerBuilder() - .WithTagMapping("!Simple", typeof(Simple)) - .Build() - .Deserialize(UsingReaderFor(text)); - - result.aaa.Should().Be("bbb"); - } - - [Fact] - public void DeserializeConvertible() - { - var text = Yaml.ReaderFrom("convertible.template").TemplatedOn(); - - var result = new DeserializerBuilder() - .WithTagMapping("!Convertible", typeof(Convertible)) - .Build() - .Deserialize(UsingReaderFor(text)); - - result.aaa.Should().Be("[hello, world]"); - } - - [Fact] - public void DeserializationFailsForUndefinedForwardReferences() - { - var text = Lines( - "Nothing: *forward", - "MyString: ForwardReference"); - - Action action = () => Deserializer.Deserialize(UsingReaderFor(text)); - - action.ShouldThrow(); - } - - [Fact] - public void RoundtripObject() - { - var obj = new Example(); - - var result = DoRoundtripFromObjectTo( - obj, - new SerializerBuilder() - .WithTagMapping("!Example", typeof(Example)) - .EnsureRoundtrip() - .Build(), - new DeserializerBuilder() - .WithTagMapping("!Example", typeof(Example)) - .Build() - ); - - result.ShouldBeEquivalentTo(obj); - } - - [Fact] - public void RoundtripObjectWithDefaults() - { - var obj = new Example(); - - var result = DoRoundtripFromObjectTo( - obj, - new SerializerBuilder() - .WithTagMapping("!Example", typeof(Example)) - .EnsureRoundtrip() - .Build(), - new DeserializerBuilder() - .WithTagMapping("!Example", typeof(Example)) - .Build() - ); - - result.ShouldBeEquivalentTo(obj); - } - - [Fact] - public void RoundtripAnonymousType() - { - var data = new { Key = 3 }; - - var result = DoRoundtripFromObjectTo>(data); - - result.Should().Equal(new Dictionary { - { "Key", "3" } - }); - } - - [Fact] - public void RoundtripWithYamlTypeConverter() - { - var obj = new MissingDefaultCtor("Yo"); - - SerializerBuilder - .EnsureRoundtrip() - .WithTypeConverter(new MissingDefaultCtorConverter()); - - DeserializerBuilder - .WithTypeConverter(new MissingDefaultCtorConverter()); - - var result = DoRoundtripFromObjectTo(obj, Serializer, Deserializer); - - result.Value.Should().Be("Yo"); - } - - [Fact] - public void RoundtripAlias() - { - var writer = new StringWriter(); - var input = new NameConvention { AliasTest = "Fourth" }; - - SerializerBuilder - .ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitDefaults); - - Serializer.Serialize(writer, input, input.GetType()); - var text = writer.ToString(); - - // Todo: use RegEx once FluentAssertions 2.2 is released - text.TrimEnd('\r', '\n').Should().Be("fourthTest: Fourth"); - - var output = Deserializer.Deserialize(UsingReaderFor(text)); - - output.AliasTest.Should().Be(input.AliasTest); - } - - [Fact] - public void RoundtripAliasOverride() - { - var writer = new StringWriter(); - var input = new NameConvention { AliasTest = "Fourth" }; - - var attribute = new YamlMemberAttribute - { - Alias = "fourthOverride" - }; - - var serializer = new SerializerBuilder() - .ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitDefaults) - .WithAttributeOverride(nc => nc.AliasTest, attribute) - .Build(); - - serializer.Serialize(writer, input, input.GetType()); - var text = writer.ToString(); - - // Todo: use RegEx once FluentAssertions 2.2 is released - text.TrimEnd('\r', '\n').Should().Be("fourthOverride: Fourth"); - - DeserializerBuilder.WithAttributeOverride(n => n.AliasTest, attribute); - var output = Deserializer.Deserialize(UsingReaderFor(text)); - - output.AliasTest.Should().Be(input.AliasTest); - } - - [Fact] - // Todo: is the assert on the string necessary? - public void RoundtripDerivedClass() - { - var obj = new InheritanceExample - { - SomeScalar = "Hello", - RegularBase = new Derived { BaseProperty = "foo", DerivedProperty = "bar" } - }; - - var result = DoRoundtripFromObjectTo( - obj, - new SerializerBuilder() - .WithTagMapping("!InheritanceExample", typeof(InheritanceExample)) - .WithTagMapping("!Derived", typeof(Derived)) - .EnsureRoundtrip() - .Build(), - new DeserializerBuilder() - .WithTagMapping("!InheritanceExample", typeof(InheritanceExample)) - .WithTagMapping("!Derived", typeof(Derived)) - .Build() - ); - - result.SomeScalar.Should().Be("Hello"); - result.RegularBase.Should().BeOfType().And - .Subject.As().ShouldBeEquivalentTo(new { ChildProp = "bar" }, o => o.ExcludingMissingMembers()); - } - - [Fact] - public void RoundtripDerivedClassWithSerializeAs() - { - var obj = new InheritanceExample - { - SomeScalar = "Hello", - BaseWithSerializeAs = new Derived { BaseProperty = "foo", DerivedProperty = "bar" } - }; - - var result = DoRoundtripFromObjectTo( - obj, - new SerializerBuilder() - .WithTagMapping("!InheritanceExample", typeof(InheritanceExample)) - .EnsureRoundtrip() - .Build(), - new DeserializerBuilder() - .WithTagMapping("!InheritanceExample", typeof(InheritanceExample)) - .Build() - ); - - result.BaseWithSerializeAs.Should().BeOfType().And - .Subject.As().ShouldBeEquivalentTo(new { ParentProp = "foo" }, o => o.ExcludingMissingMembers()); - } - - [Fact] - public void RoundtripInterfaceProperties() - { - AssumingDeserializerWith(new LambdaObjectFactory(t => - { - if (t == typeof(InterfaceExample)) { return new InterfaceExample(); } - else if (t == typeof(IDerived)) { return new Derived(); } - return null; - })); - - var obj = new InterfaceExample - { - Derived = new Derived { BaseProperty = "foo", DerivedProperty = "bar" } - }; - - var result = DoRoundtripFromObjectTo(obj); - - result.Derived.Should().BeOfType().And - .Subject.As().ShouldBeEquivalentTo(new { BaseProperty = "foo", DerivedProperty = "bar" }, o => o.ExcludingMissingMembers()); - } - - [Fact] - public void DeserializeGuid() - { - var stream = Yaml.ReaderFrom("guid.yaml"); - var result = Deserializer.Deserialize(stream); - - result.Should().Be(new Guid("9462790d5c44468985425e2dd38ebd98")); - } - - [Fact] - public void DeserializationOfOrderedProperties() - { - var stream = Yaml.ReaderFrom("ordered-properties.yaml"); - - var orderExample = Deserializer.Deserialize(stream); - - orderExample.Order1.Should().Be("Order1 value"); - orderExample.Order2.Should().Be("Order2 value"); - } - - [Fact] - public void DeserializeEnumerable() - { - var obj = new[] { new Simple { aaa = "bbb" } }; - - var result = DoRoundtripFromObjectTo>(obj); - - result.Should().ContainSingle(item => "bbb".Equals(item.aaa)); - } - - [Fact] - public void DeserializeArray() - { - var stream = Yaml.ReaderFrom("list.yaml"); - - var result = Deserializer.Deserialize(stream); - - result.Should().Equal(new[] { "one", "two", "three" }); - } - - [Fact] - public void DeserializeList() - { - var stream = Yaml.ReaderFrom("list.yaml"); - - var result = Deserializer.Deserialize(stream); - - result.Should().BeAssignableTo().And - .Subject.As().Should().Equal(new[] { "one", "two", "three" }); - } - - [Fact] - public void DeserializeExplicitList() - { - var stream = Yaml.ReaderFrom("list-explicit.yaml"); - - var result = new DeserializerBuilder() - .WithTagMapping("!List", typeof(List)) - .Build() - .Deserialize(stream); - - result.Should().BeAssignableTo>().And - .Subject.As>().Should().Equal(3, 4, 5); - } - - [Fact] - public void RoundtripList() - { - var obj = new List { 2, 4, 6 }; - - var result = DoRoundtripOn>(obj, SerializerBuilder.EnsureRoundtrip().Build()); - - result.Should().Equal(obj); - } - - [Fact] - public void RoundtripArrayWithTypeConversion() - { - var obj = new object[] { 1, 2, "3" }; - - var result = DoRoundtripFromObjectTo(obj); - - result.Should().Equal(1, 2, 3); - } - - [Fact] - public void RoundtripArrayOfIdenticalObjects() - { - var z = new Simple { aaa = "bbb" }; - var obj = new[] { z, z, z }; - - var result = DoRoundtripOn(obj); - - result.Should().HaveCount(3).And.OnlyContain(x => z.aaa.Equals(x.aaa)); - result[0].Should().BeSameAs(result[1]).And.BeSameAs(result[2]); - } - - [Fact] - public void DeserializeDictionary() - { - var stream = Yaml.ReaderFrom("dictionary.yaml"); - - var result = Deserializer.Deserialize(stream); - - result.Should().BeAssignableTo>().And.Subject - .As>().Should().Equal(new Dictionary { - { "key1", "value1" }, - { "key2", "value2" } - }); - } - - [Fact] - public void DeserializeExplicitDictionary() - { - var stream = Yaml.ReaderFrom("dictionary-explicit.yaml"); - - var result = new DeserializerBuilder() - .WithTagMapping("!Dictionary", typeof(Dictionary)) - .Build() - .Deserialize(stream); - - result.Should().BeAssignableTo>().And.Subject - .As>().Should().Equal(new Dictionary { - { "key1", 1 }, - { "key2", 2 } - }); - } - - [Fact] - public void RoundtripDictionary() - { - var obj = new Dictionary { - { "key1", "value1" }, - { "key2", "value2" }, - { "key3", "value3" } - }; - - var result = DoRoundtripFromObjectTo>(obj); - - result.Should().Equal(obj); - } - - [Fact] - public void DeserializeListOfDictionaries() - { - var stream = Yaml.ReaderFrom("list-of-dictionaries.yaml"); - - var result = Deserializer.Deserialize>>(stream); - - result.ShouldBeEquivalentTo(new[] { - new Dictionary { - { "connection", "conn1" }, - { "path", "path1" } - }, - new Dictionary { - { "connection", "conn2" }, - { "path", "path2" } - }}, opt => opt.WithStrictOrderingFor(root => root)); - } - - [Fact] - public void DeserializeTwoDocuments() - { - var reader = ParserFor(Lines( - "---", - "aaa: 111", - "---", - "aaa: 222", - "...")); - - reader.Consume(); - var one = Deserializer.Deserialize(reader); - var two = Deserializer.Deserialize(reader); - - one.ShouldBeEquivalentTo(new { aaa = "111" }); - two.ShouldBeEquivalentTo(new { aaa = "222" }); - } - - [Fact] - public void DeserializeThreeDocuments() - { - var reader = ParserFor(Lines( - "---", - "aaa: 111", - "---", - "aaa: 222", - "---", - "aaa: 333", - "...")); - - reader.Consume(); - var one = Deserializer.Deserialize(reader); - var two = Deserializer.Deserialize(reader); - var three = Deserializer.Deserialize(reader); - - reader.Accept(out var _).Should().BeTrue("reader should have reached StreamEnd"); - one.ShouldBeEquivalentTo(new { aaa = "111" }); - two.ShouldBeEquivalentTo(new { aaa = "222" }); - three.ShouldBeEquivalentTo(new { aaa = "333" }); - } - - [Fact] - public void SerializeGuid() - { - var guid = new Guid("{9462790D-5C44-4689-8542-5E2DD38EBD98}"); - - var writer = new StringWriter(); - - Serializer.Serialize(writer, guid); - var serialized = writer.ToString(); - Regex.IsMatch(serialized, "^" + guid.ToString("D")).Should().BeTrue("serialized content should contain the guid, but instead contained: " + serialized); - } - - [Fact] - public void SerializeNullObject() - { -#nullable enable - object? obj = null; - - var writer = new StringWriter(); - - Serializer.Serialize(writer, obj); - var serialized = writer.ToString(); - serialized.Should().Be("--- " + writer.NewLine); -#nullable restore - } - - [Fact] - public void SerializationOfNullInListsAreAlwaysEmittedWithoutUsingEmitDefaults() - { - var writer = new StringWriter(); - var obj = new[] { "foo", null, "bar" }; - - Serializer.Serialize(writer, obj); - var serialized = writer.ToString(); - - Regex.Matches(serialized, "-").Count.Should().Be(3, "there should have been 3 elements"); - } - - [Fact] - public void SerializationOfNullInListsAreAlwaysEmittedWhenUsingEmitDefaults() - { - var writer = new StringWriter(); - var obj = new[] { "foo", null, "bar" }; - - SerializerBuilder.Build().Serialize(writer, obj); - var serialized = writer.ToString(); - - Regex.Matches(serialized, "-").Count.Should().Be(3, "there should have been 3 elements"); - } - - [Fact] - public void SerializationIncludesKeyWhenEmittingDefaults() - { - var writer = new StringWriter(); - var obj = new Example { MyString = null }; - - SerializerBuilder.Build().Serialize(writer, obj, typeof(Example)); - - writer.ToString().Should().Contain("MyString"); - } - - [Fact] - [Trait("Motive", "Bug fix")] - public void SerializationIncludesKeyFromAnonymousTypeWhenEmittingDefaults() - { - var writer = new StringWriter(); - var obj = new { MyString = (string)null }; - - SerializerBuilder.Build().Serialize(writer, obj, obj.GetType()); - - writer.ToString().Should().Contain("MyString"); - } - - [Fact] - public void SerializationDoesNotIncludeKeyWhenDisregardingDefaults() - { - var writer = new StringWriter(); - var obj = new Example { MyString = null }; - - SerializerBuilder - .ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitDefaults); - - Serializer.Serialize(writer, obj, typeof(Example)); - - writer.ToString().Should().NotContain("MyString"); - } - - [Fact] - public void SerializationOfDefaultsWorkInJson() - { - var writer = new StringWriter(); - var obj = new Example { MyString = null }; - - SerializerBuilder.JsonCompatible().Build().Serialize(writer, obj, typeof(Example)); - - writer.ToString().Should().Contain("MyString"); - } - - [Fact] - public void SerializationOfLongKeysWorksInJson() - { - var writer = new StringWriter(); - var obj = new Dictionary - { - { new string('x', 3000), "extremely long key" } - }; - - SerializerBuilder.JsonCompatible().Build().Serialize(writer, obj, typeof(Dictionary)); - - writer.ToString().Should().NotContain("?"); - } - - [Fact] - public void SerializationOfAnchorWorksInJson() - { - var deserializer = new DeserializerBuilder().Build(); - var yamlObject = deserializer.Deserialize(Yaml.ReaderForText(@" -x: &anchor1 - z: - v: 1 -y: - k: *anchor1")); - - var serializer = new SerializerBuilder() - .JsonCompatible() - .Build(); - - serializer.Serialize(yamlObject).Trim().Should() - .BeEquivalentTo(@"{""x"": {""z"": {""v"": ""1""}}, ""y"": {""k"": {""z"": {""v"": ""1""}}}}"); - } - - [Fact] - // Todo: this is actually roundtrip - public void DeserializationOfDefaultsWorkInJson() - { - var writer = new StringWriter(); - var obj = new Example { MyString = null }; - - SerializerBuilder.EnsureRoundtrip().JsonCompatible().Build().Serialize(writer, obj, typeof(Example)); - var result = Deserializer.Deserialize(UsingReaderFor(writer)); - - result.MyString.Should().BeNull(); - } - - [Fact] - public void NullsRoundTrip() - { - var writer = new StringWriter(); - var obj = new Example { MyString = null }; - - SerializerBuilder.EnsureRoundtrip().Build().Serialize(writer, obj, typeof(Example)); - var result = Deserializer.Deserialize(UsingReaderFor(writer)); - - result.MyString.Should().BeNull(); - } - - [Theory] - [InlineData(typeof(SByteEnum))] - [InlineData(typeof(ByteEnum))] - [InlineData(typeof(Int16Enum))] - [InlineData(typeof(UInt16Enum))] - [InlineData(typeof(Int32Enum))] - [InlineData(typeof(UInt32Enum))] - [InlineData(typeof(Int64Enum))] - [InlineData(typeof(UInt64Enum))] - public void DeserializationOfEnumWorksInJson(Type enumType) - { - var defaultEnumValue = 0; - var nonDefaultEnumValue = Enum.GetValues(enumType).GetValue(1); - - var jsonSerializer = SerializerBuilder.EnsureRoundtrip().JsonCompatible().Build(); - var jsonSerializedEnum = jsonSerializer.Serialize(nonDefaultEnumValue); - - nonDefaultEnumValue.Should().NotBe(defaultEnumValue); - jsonSerializedEnum.Should().Contain($"\"{nonDefaultEnumValue}\""); - } - - [Fact] - public void SerializationOfOrderedProperties() - { - var obj = new OrderExample(); - var writer = new StringWriter(); - - Serializer.Serialize(writer, obj); - var serialized = writer.ToString(); - - serialized.Should() - .Be("Order1: Order1 value\r\nOrder2: Order2 value\r\n".NormalizeNewLines(), "the properties should be in the right order"); - } - - [Fact] - public void SerializationRespectsYamlIgnoreAttribute() - { - - var writer = new StringWriter(); - var obj = new IgnoreExample(); - - Serializer.Serialize(writer, obj); - var serialized = writer.ToString(); - - serialized.Should().NotContain("IgnoreMe"); - } - - [Fact] - public void SerializationRespectsYamlIgnoreAttributeOfDerivedClasses() - { - - var writer = new StringWriter(); - var obj = new IgnoreExampleDerived(); - - Serializer.Serialize(writer, obj); - var serialized = writer.ToString(); - - serialized.Should().NotContain("IgnoreMe"); - } - - [Fact] - public void SerializationRespectsYamlIgnoreOverride() - { - - var writer = new StringWriter(); - var obj = new Simple(); - - var ignore = new YamlIgnoreAttribute(); - var serializer = new SerializerBuilder() - .WithAttributeOverride(s => s.aaa, ignore) - .Build(); - - serializer.Serialize(writer, obj); - var serialized = writer.ToString(); - - serialized.Should().NotContain("aaa"); - } - - [Fact] - public void SerializationRespectsScalarStyle() - { - var writer = new StringWriter(); - var obj = new ScalarStyleExample(); - - Serializer.Serialize(writer, obj); - var serialized = writer.ToString(); - - serialized.Should() - .Be("LiteralString: |-\r\n Test\r\nDoubleQuotedString: \"Test\"\r\n".NormalizeNewLines(), "the properties should be specifically styled"); - } - - [Fact] - public void SerializationRespectsScalarStyleOverride() - { - var writer = new StringWriter(); - var obj = new ScalarStyleExample(); - - var serializer = new SerializerBuilder() - .WithAttributeOverride(e => e.LiteralString, new YamlMemberAttribute { ScalarStyle = ScalarStyle.DoubleQuoted }) - .WithAttributeOverride(e => e.DoubleQuotedString, new YamlMemberAttribute { ScalarStyle = ScalarStyle.Literal }) - .Build(); - - serializer.Serialize(writer, obj); - var serialized = writer.ToString(); - - serialized.Should() - .Be("LiteralString: \"Test\"\r\nDoubleQuotedString: |-\r\n Test\r\n".NormalizeNewLines(), "the properties should be specifically styled"); - } - - [Fact] - public void SerializationRespectsDefaultScalarStyle() - { - var writer = new StringWriter(); - var obj = new MixedFormatScalarStyleExample(new string[] { "01", "0.1", "myString" }); - - var serializer = new SerializerBuilder().WithDefaultScalarStyle(ScalarStyle.SingleQuoted).Build(); - - serializer.Serialize(writer, obj); - - var yaml = writer.ToString(); - - var expected = Yaml.Text(@" - Data: - - '01' - - '0.1' - - 'myString' - "); - - Assert.Equal(expected.NormalizeNewLines(), yaml.NormalizeNewLines().TrimNewLines()); - } - - [Fact] - public void SerializationDerivedAttributeOverride() - { - var writer = new StringWriter(); - var obj = new Derived { DerivedProperty = "Derived", BaseProperty = "Base" }; - - var ignore = new YamlIgnoreAttribute(); - var serializer = new SerializerBuilder() - .WithAttributeOverride(d => d.DerivedProperty, ignore) - .Build(); - - serializer.Serialize(writer, obj); - var serialized = writer.ToString(); - - serialized.Should() - .Be("BaseProperty: Base\r\n".NormalizeNewLines(), "the derived property should be specifically ignored"); - } - - [Fact] - public void SerializationBaseAttributeOverride() - { - var writer = new StringWriter(); - var obj = new Derived { DerivedProperty = "Derived", BaseProperty = "Base" }; - - var ignore = new YamlIgnoreAttribute(); - var serializer = new SerializerBuilder() - .WithAttributeOverride(b => b.BaseProperty, ignore) - .Build(); - - serializer.Serialize(writer, obj); - var serialized = writer.ToString(); - - serialized.Should() - .Be("DerivedProperty: Derived\r\n".NormalizeNewLines(), "the base property should be specifically ignored"); - } - - [Fact] - public void SerializationSkipsPropertyWhenUsingDefaultValueAttribute() - { - var writer = new StringWriter(); - var obj = new DefaultsExample { Value = DefaultsExample.DefaultValue }; - - SerializerBuilder - .ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitDefaults); - - Serializer.Serialize(writer, obj); - var serialized = writer.ToString(); - - serialized.Should().NotContain("Value"); - } - - [Fact] - public void SerializationEmitsPropertyWhenUsingEmitDefaultsAndDefaultValueAttribute() - { - var writer = new StringWriter(); - var obj = new DefaultsExample { Value = DefaultsExample.DefaultValue }; - - SerializerBuilder.Build().Serialize(writer, obj); - var serialized = writer.ToString(); - - serialized.Should().Contain("Value"); - } - - [Fact] - public void SerializationEmitsPropertyWhenValueDifferFromDefaultValueAttribute() - { - var writer = new StringWriter(); - var obj = new DefaultsExample { Value = "non-default" }; - - Serializer.Serialize(writer, obj); - var serialized = writer.ToString(); - - serialized.Should().Contain("Value"); - } - - [Fact] - public void SerializingAGenericDictionaryShouldNotThrowTargetException() - { - var obj = new CustomGenericDictionary { - { "hello", "world" } - }; - - Action action = () => Serializer.Serialize(new StringWriter(), obj); - - action.ShouldNotThrow(); - } - - [Fact] - public void SerializationUtilizeNamingConventions() - { - var convention = A.Fake(); - A.CallTo(() => convention.Apply(A._)).ReturnsLazily((string x) => x); - var obj = new NameConvention { FirstTest = "1", SecondTest = "2" }; - - var serializer = new SerializerBuilder() - .WithNamingConvention(convention) - .Build(); - - serializer.Serialize(new StringWriter(), obj); - - A.CallTo(() => convention.Apply("FirstTest")).MustHaveHappened(); - A.CallTo(() => convention.Apply("SecondTest")).MustHaveHappened(); - } - - [Fact] - public void DeserializationUtilizeNamingConventions() - { - var convention = A.Fake(); - A.CallTo(() => convention.Apply(A._)).ReturnsLazily((string x) => x); - var text = Lines( - "FirstTest: 1", - "SecondTest: 2"); - - var deserializer = new DeserializerBuilder() - .WithNamingConvention(convention) - .Build(); - - deserializer.Deserialize(UsingReaderFor(text)); - - A.CallTo(() => convention.Apply("FirstTest")).MustHaveHappened(); - A.CallTo(() => convention.Apply("SecondTest")).MustHaveHappened(); - } - - [Fact] - public void TypeConverterIsUsedOnListItems() - { - var text = Lines( - "- !{type}", - " Left: hello", - " Right: world") - .TemplatedOn(); - - var list = new DeserializerBuilder() - .WithTagMapping("!Convertible", typeof(Convertible)) - .Build() - .Deserialize>(UsingReaderFor(text)); - - list - .Should().NotBeNull() - .And.ContainSingle(c => c.Equals("[hello, world]")); - } - - [Fact] - public void BackreferencesAreMergedWithMappings() - { - var stream = Yaml.ReaderFrom("backreference.yaml"); - - var parser = new MergingParser(new Parser(stream)); - var result = Deserializer.Deserialize>>(parser); - - var alias = result["alias"]; - alias.Should() - .Contain("key1", "value1", "key1 should be inherited from the backreferenced mapping") - .And.Contain("key2", "Overriding key2", "key2 should be overriden by the actual mapping") - .And.Contain("key3", "value3", "key3 is defined in the actual mapping"); - } - - [Fact] - public void MergingDoesNotProduceDuplicateAnchors() - { - var parser = new MergingParser(Yaml.ParserForText(@" - anchor: &default - key1: &myValue value1 - key2: value2 - alias: - <<: *default - key2: Overriding key2 - key3: value3 - useMyValue: - key: *myValue - ")); - var result = Deserializer.Deserialize>>(parser); - - var alias = result["alias"]; - alias.Should() - .Contain("key1", "value1", "key1 should be inherited from the backreferenced mapping") - .And.Contain("key2", "Overriding key2", "key2 should be overriden by the actual mapping") - .And.Contain("key3", "value3", "key3 is defined in the actual mapping"); - - result["useMyValue"].Should() - .Contain("key", "value1", "key should be copied"); - } - - [Fact] - public void ExampleFromSpecificationIsHandledCorrectly() - { - var parser = new MergingParser(Yaml.ParserForText(@" - obj: - - &CENTER { x: 1, y: 2 } - - &LEFT { x: 0, y: 2 } - - &BIG { r: 10 } - - &SMALL { r: 1 } - - # All the following maps are equal: - results: - - # Explicit keys - x: 1 - y: 2 - r: 10 - label: center/big - - - # Merge one map - << : *CENTER - r: 10 - label: center/big - - - # Merge multiple maps - << : [ *CENTER, *BIG ] - label: center/big - - - # Override - << : [ *BIG, *LEFT, *SMALL ] - x: 1 - label: center/big - ")); - - var result = Deserializer.Deserialize>>>(parser); - - var index = 0; - foreach (var mapping in result["results"]) - { - mapping.Should() - .Contain("x", "1", "'x' should be '1' in result #{0}", index) - .And.Contain("y", "2", "'y' should be '2' in result #{0}", index) - .And.Contain("r", "10", "'r' should be '10' in result #{0}", index) - .And.Contain("label", "center/big", "'label' should be 'center/big' in result #{0}", index); - - ++index; - } - } - - [Fact] - public void MergeNestedReferenceCorrectly() - { - var parser = new MergingParser(Yaml.ParserForText(@" - base1: &level1 - key: X - level: 1 - base2: &level2 - <<: *level1 - key: Y - level: 2 - derived1: - <<: *level1 - key: D1 - derived2: - <<: *level2 - key: D2 - derived3: - <<: [ *level1, *level2 ] - key: D3 - ")); - - var result = Deserializer.Deserialize>>(parser); - - result["derived1"].Should() - .Contain("key", "D1", "key should be overriden by the actual mapping") - .And.Contain("level", "1", "level should be inherited from the backreferenced mapping"); - - result["derived2"].Should() - .Contain("key", "D2", "key should be overriden by the actual mapping") - .And.Contain("level", "2", "level should be inherited from the backreferenced mapping"); - - result["derived3"].Should() - .Contain("key", "D3", "key should be overriden by the actual mapping") - .And.Contain("level", "1", "level should be inherited from the backreferenced mapping"); - } - - [Fact] - public void IgnoreExtraPropertiesIfWanted() - { - var text = Lines("aaa: hello", "bbb: world"); - DeserializerBuilder.IgnoreUnmatchedProperties(); - var actual = Deserializer.Deserialize(UsingReaderFor(text)); - actual.aaa.Should().Be("hello"); - } - - [Fact] - public void DontIgnoreExtraPropertiesIfWanted() - { - var text = Lines("aaa: hello", "bbb: world"); - var actual = Record.Exception(() => Deserializer.Deserialize(UsingReaderFor(text))); - Assert.IsType(actual); - ((YamlException)actual).Start.Column.Should().Be(1); - ((YamlException)actual).Start.Line.Should().Be(2); - ((YamlException)actual).Start.Index.Should().Be(12); - ((YamlException)actual).End.Column.Should().Be(4); - ((YamlException)actual).End.Line.Should().Be(2); - ((YamlException)actual).End.Index.Should().Be(15); - ((YamlException)actual).Message.Should().Be("Property 'bbb' not found on type 'YamlDotNet.Test.Serialization.Simple'."); - } - - [Fact] - public void IgnoreExtraPropertiesIfWantedBefore() - { - var text = Lines("bbb: [200,100]", "aaa: hello"); - DeserializerBuilder.IgnoreUnmatchedProperties(); - var actual = Deserializer.Deserialize(UsingReaderFor(text)); - actual.aaa.Should().Be("hello"); - } - - [Fact] - public void IgnoreExtraPropertiesIfWantedNamingScheme() - { - var text = Lines( - "scratch: 'scratcher'", - "deleteScratch: false", - "notScratch: 9443", - "notScratch: 192.168.1.30", - "mappedScratch:", - "- '/work/'" - ); - - DeserializerBuilder - .WithNamingConvention(CamelCaseNamingConvention.Instance) - .IgnoreUnmatchedProperties(); - - var actual = Deserializer.Deserialize(UsingReaderFor(text)); - actual.Scratch.Should().Be("scratcher"); - actual.DeleteScratch.Should().Be(false); - actual.MappedScratch.Should().ContainInOrder(new[] { "/work/" }); - } - - [Fact] - public void InvalidTypeConversionsProduceProperExceptions() - { - var text = Lines("- 1", "- two", "- 3"); - - var sut = new Deserializer(); - var exception = Assert.Throws(() => sut.Deserialize>(UsingReaderFor(text))); - - Assert.Equal(2, exception.Start.Line); - Assert.Equal(3, exception.Start.Column); - } - - [Theory] - [InlineData("blah")] - [InlineData("hello=world")] - [InlineData("+190:20:30")] - [InlineData("x:y")] - public void ValueAllowedAfterDocumentStartToken(string text) - { - var value = Lines("--- " + text); - - var sut = new Deserializer(); - var actual = sut.Deserialize(UsingReaderFor(value)); - - Assert.Equal(text, actual); - } - - [Fact] - public void MappingDisallowedAfterDocumentStartToken() - { - var value = Lines("--- x: y"); - - var sut = new Deserializer(); - var exception = Assert.Throws(() => sut.Deserialize(UsingReaderFor(value))); - - Assert.Equal(1, exception.Start.Line); - Assert.Equal(6, exception.Start.Column); - } - - [Fact] - public void SerializeDynamicPropertyAndApplyNamingConvention() - { - dynamic obj = new ExpandoObject(); - obj.property_one = new ExpandoObject(); - ((IDictionary)obj.property_one).Add("new_key_here", "new_value"); - - var mockNamingConvention = A.Fake(); - A.CallTo(() => mockNamingConvention.Apply(A.Ignored)).Returns("xxx"); - - var serializer = new SerializerBuilder() - .WithNamingConvention(mockNamingConvention) - .Build(); - - var writer = new StringWriter(); - serializer.Serialize(writer, obj); - - writer.ToString().Should().Contain("xxx: new_value"); - } - - [Fact] - public void SerializeGenericDictionaryPropertyAndDoNotApplyNamingConvention() - { - var obj = new Dictionary - { - ["property_one"] = new GenericTestDictionary() - }; - - ((IDictionary)obj["property_one"]).Add("new_key_here", "new_value"); - - var mockNamingConvention = A.Fake(); - A.CallTo(() => mockNamingConvention.Apply(A.Ignored)).Returns("xxx"); - - var serializer = new SerializerBuilder() - .WithNamingConvention(mockNamingConvention) - .Build(); - - var writer = new StringWriter(); - serializer.Serialize(writer, obj); - - writer.ToString().Should().Contain("new_key_here: new_value"); - } - - [Theory, MemberData(nameof(SpecialFloats))] - public void SpecialFloatsAreHandledCorrectly(FloatTestCase testCase) - { - var buffer = new StringWriter(); - Serializer.Serialize(buffer, testCase.Value); - - var firstLine = buffer.ToString().Split('\r', '\n')[0]; - Assert.Equal(testCase.ExpectedTextRepresentation, firstLine); - - var deserializer = new Deserializer(); - var deserializedValue = deserializer.Deserialize(new StringReader(buffer.ToString()), testCase.Value.GetType()); - - Assert.Equal(testCase.Value, deserializedValue); - } - - [Theory] - [InlineData(TestEnum.True)] - [InlineData(TestEnum.False)] - [InlineData(TestEnum.ABC)] - [InlineData(TestEnum.Null)] - public void RoundTripSpecialEnum(object testValue) - { - var test = new TestEnumTestCase { TestEnum = (TestEnum)testValue }; - var serializer = new SerializerBuilder().WithQuotingNecessaryStrings().Build(); - var deserializer = new DeserializerBuilder().Build(); - var serialized = serializer.Serialize(test); - var actual = deserializer.Deserialize(serialized); - Assert.Equal(testValue, actual.TestEnum); - } - - [Fact] - public void EmptyStringsAreQuoted() - { - var serializer = new SerializerBuilder().WithQuotingNecessaryStrings().Build(); - var o = new { test = string.Empty }; - var result = serializer.Serialize(o); - var expected = $"test: \"\"{Environment.NewLine}"; - Assert.Equal(expected, result); - } - - public enum TestEnum - { - True, - False, - ABC, - Null - } - - public class TestEnumTestCase - { - public TestEnum TestEnum { get; set; } - } - - public class FloatTestCase - { - private readonly string description; - public object Value { get; private set; } - public string ExpectedTextRepresentation { get; private set; } - - public FloatTestCase(string description, object value, string expectedTextRepresentation) - { - this.description = description; - Value = value; - ExpectedTextRepresentation = expectedTextRepresentation; - } - - public override string ToString() - { - return description; - } - } - - public static IEnumerable SpecialFloats - { - get - { - return - new[] - { - new FloatTestCase("double.NaN", double.NaN, ".nan"), - new FloatTestCase("double.PositiveInfinity", double.PositiveInfinity, ".inf"), - new FloatTestCase("double.NegativeInfinity", double.NegativeInfinity, "-.inf"), - new FloatTestCase("double.Epsilon", double.Epsilon, double.Epsilon.ToString("G", CultureInfo.InvariantCulture)), - new FloatTestCase("double.26.67", 26.67D, "26.67"), - - new FloatTestCase("float.NaN", float.NaN, ".nan"), - new FloatTestCase("float.PositiveInfinity", float.PositiveInfinity, ".inf"), - new FloatTestCase("float.NegativeInfinity", float.NegativeInfinity, "-.inf"), - new FloatTestCase("float.Epsilon", float.Epsilon, float.Epsilon.ToString("G", CultureInfo.InvariantCulture)), - new FloatTestCase("float.26.67", 26.67F, "26.67"), - -#if NET - new FloatTestCase("double.MinValue", double.MinValue, double.MinValue.ToString("G", CultureInfo.InvariantCulture)), - new FloatTestCase("double.MaxValue", double.MaxValue, double.MaxValue.ToString("G", CultureInfo.InvariantCulture)), - new FloatTestCase("float.MinValue", float.MinValue, float.MinValue.ToString("G", CultureInfo.InvariantCulture)), - new FloatTestCase("float.MaxValue", float.MaxValue, float.MaxValue.ToString("G", CultureInfo.InvariantCulture)), -#endif - } - .Select(tc => new object[] { tc }); - } - } - - [Fact] - public void NegativeIntegersCanBeDeserialized() - { - var deserializer = new Deserializer(); - - var value = deserializer.Deserialize(Yaml.ReaderForText(@" - '-123' - ")); - Assert.Equal(-123, value); - } - - [Fact] - public void GenericDictionaryThatDoesNotImplementIDictionaryCanBeDeserialized() - { - var sut = new Deserializer(); - var deserialized = sut.Deserialize>(Yaml.ReaderForText(@" - a: 1 - b: 2 - ")); - - Assert.Equal("1", deserialized["a"]); - Assert.Equal("2", deserialized["b"]); - } - - [Fact] - public void GenericListThatDoesNotImplementIListCanBeDeserialized() - { - var sut = new Deserializer(); - var deserialized = sut.Deserialize>(Yaml.ReaderForText(@" - - a - - b - ")); - - Assert.Contains("a", deserialized); - Assert.Contains("b", deserialized); - } - - [Fact] - public void GuidsShouldBeQuotedWhenSerializedAsJson() - { - var sut = new SerializerBuilder() - .JsonCompatible() - .Build(); - - var yamlAsJson = new StringWriter(); - sut.Serialize(yamlAsJson, new - { - id = Guid.Empty - }); - - Assert.Contains("\"00000000-0000-0000-0000-000000000000\"", yamlAsJson.ToString()); - } - - public class Foo - { - public bool IsRequired { get; set; } - } - - [Fact] - public void AttributeOverridesAndNamingConventionDoNotConflict() - { - var namingConvention = CamelCaseNamingConvention.Instance; - - var yamlMember = new YamlMemberAttribute - { - Alias = "Required" - }; - - var serializer = new SerializerBuilder() - .WithNamingConvention(namingConvention) - .WithAttributeOverride(f => f.IsRequired, yamlMember) - .Build(); - - var yaml = serializer.Serialize(new Foo { IsRequired = true }); - Assert.Contains("required: true", yaml); - - var deserializer = new DeserializerBuilder() - .WithNamingConvention(namingConvention) - .WithAttributeOverride(f => f.IsRequired, yamlMember) - .Build(); - - var deserializedFoo = deserializer.Deserialize(yaml); - Assert.True(deserializedFoo.IsRequired); - } - - [Fact] - public void YamlConvertiblesAreAbleToEmitAndParseComments() - { - var serializer = new Serializer(); - var yaml = serializer.Serialize(new CommentWrapper { Comment = "A comment", Value = "The value" }); - - var deserializer = new Deserializer(); - var parser = new Parser(new Scanner(new StringReader(yaml), skipComments: false)); - var parsed = deserializer.Deserialize>(parser); - - Assert.Equal("A comment", parsed.Comment); - Assert.Equal("The value", parsed.Value); - } - - public class CommentWrapper : IYamlConvertible - { - public string Comment { get; set; } - public T Value { get; set; } - - public void Read(IParser parser, Type expectedType, ObjectDeserializer nestedObjectDeserializer) - { - if (parser.TryConsume(out var comment)) - { - Comment = comment.Value; - } - - Value = (T)nestedObjectDeserializer(typeof(T)); - } - - public void Write(IEmitter emitter, ObjectSerializer nestedObjectSerializer) - { - if (!string.IsNullOrEmpty(Comment)) - { - emitter.Emit(new Comment(Comment, false)); - } - - nestedObjectSerializer(Value, typeof(T)); - } - } - - [Theory] - [InlineData(uint.MinValue)] - [InlineData(uint.MaxValue)] - [InlineData(0x8000000000000000UL)] - public void DeserializationOfUInt64Succeeds(ulong value) - { - var yaml = new Serializer().Serialize(value); - Assert.Contains(value.ToString(), yaml); - - var parsed = new Deserializer().Deserialize(yaml); - Assert.Equal(value, parsed); - } - - [Theory] - [InlineData(int.MinValue)] - [InlineData(int.MaxValue)] - [InlineData(0L)] - public void DeserializationOfInt64Succeeds(long value) - { - var yaml = new Serializer().Serialize(value); - Assert.Contains(value.ToString(), yaml); - - var parsed = new Deserializer().Deserialize(yaml); - Assert.Equal(value, parsed); - } - - public class AnchorsOverwritingTestCase - { - public List a { get; set; } - public List b { get; set; } - public List c { get; set; } - public List d { get; set; } - } - - [Fact] - public void DeserializationOfStreamWithDuplicateAnchorsSucceeds() - { - var yaml = Yaml.ParserForResource("anchors-overwriting.yaml"); - var serializer = new DeserializerBuilder() - .IgnoreUnmatchedProperties() - .Build(); - var deserialized = serializer.Deserialize(yaml); - Assert.NotNull(deserialized); - } - - private sealed class AnchorPrecedence - { - internal sealed class AnchorPrecedenceNested - { - public string b1 { get; set; } - public Dictionary b2 { get; set; } - } - - public string a { get; set; } - public AnchorPrecedenceNested b { get; set; } - public string c { get; set; } - } - - [Fact] - public void DeserializationWithDuplicateAnchorsSucceeds() - { - var sut = new Deserializer(); - var deserialized = sut.Deserialize(@" -a: &anchor1 test0 -b: - b1: &anchor1 test1 - b2: - b21: &anchor1 test2 -c: *anchor1"); - - Assert.Equal("test0", deserialized.a); - Assert.Equal("test1", deserialized.b.b1); - Assert.Contains("b21", deserialized.b.b2.Keys); - Assert.Equal("test2", deserialized.b.b2["b21"]); - Assert.Equal("test2", deserialized.c); - } - - [Fact] - public void SerializeExceptionWithStackTrace() - { - var ex = GetExceptionWithStackTrace(); - var serializer = new SerializerBuilder() - .WithTypeConverter(new MethodInfoConverter()) - .Build(); - var yaml = serializer.Serialize(ex); - Assert.Contains("GetExceptionWithStackTrace", yaml); - } - - private class MethodInfoConverter : IYamlTypeConverter - { - public bool Accepts(Type type) - { - return typeof(MethodInfo).IsAssignableFrom(type); - } - - public object ReadYaml(IParser parser, Type type) - { - throw new NotImplementedException(); - } - - public void WriteYaml(IEmitter emitter, object value, Type type) - { - var method = (MethodInfo)value; - emitter.Emit(new Scalar(string.Format("{0}.{1}", method.DeclaringType.FullName, method.Name))); - } - } - - static Exception GetExceptionWithStackTrace() - { - try - { - throw new ArgumentNullException("foo"); - } - catch (Exception ex) - { - return ex; - } - } - - [Fact] - public void RegisteringATypeConverterPreventsTheTypeFromBeingVisited() - { - var serializer = new SerializerBuilder() - .WithTypeConverter(new NonSerializableTypeConverter()) - .Build(); - - var yaml = serializer.Serialize(new NonSerializableContainer - { - Value = new NonSerializable { Text = "hello" }, - }); - - var deserializer = new DeserializerBuilder() - .WithTypeConverter(new NonSerializableTypeConverter()) - .Build(); - - var result = deserializer.Deserialize(yaml); - - Assert.Equal("hello", result.Value.Text); - } - - [Fact] - public void NamingConventionIsNotAppliedBySerializerWhenApplyNamingConventionsIsFalse() - { - var sut = new SerializerBuilder() - .WithNamingConvention(CamelCaseNamingConvention.Instance) - .Build(); - - var yaml = sut.Serialize(new NamingConventionDisabled { NoConvention = "value" }); - - Assert.Contains("NoConvention", yaml); - } - - [Fact] - public void NamingConventionIsNotAppliedByDeserializerWhenApplyNamingConventionsIsFalse() - { - var sut = new DeserializerBuilder() - .WithNamingConvention(CamelCaseNamingConvention.Instance) - .Build(); - - var yaml = "NoConvention: value"; - - var parsed = sut.Deserialize(yaml); - - Assert.Equal("value", parsed.NoConvention); - } - - [Fact] - public void TypesAreSerializable() - { - var sut = new SerializerBuilder() - .Build(); - - var yaml = sut.Serialize(typeof(string)); - - Assert.Contains(typeof(string).AssemblyQualifiedName, yaml); - } - - [Fact] - public void TypesAreDeserializable() - { - var sut = new DeserializerBuilder() - .Build(); - - var type = sut.Deserialize(typeof(string).AssemblyQualifiedName); - - Assert.Equal(typeof(string), type); - } - - [Fact] - public void TypesAreConvertedWhenNeededFromScalars() - { - var sut = new DeserializerBuilder() - .WithTagMapping("!dbl", typeof(DoublyConverted)) - .Build(); - - var result = sut.Deserialize("!dbl hello"); - - Assert.Equal(5, result); - } - - [Fact] - public void TypesAreConvertedWhenNeededInsideLists() - { - var sut = new DeserializerBuilder() - .WithTagMapping("!dbl", typeof(DoublyConverted)) - .Build(); - - var result = sut.Deserialize>("- !dbl hello"); - - Assert.Equal(5, result[0]); - } - - [Fact] - public void TypesAreConvertedWhenNeededInsideDictionary() - { - var sut = new DeserializerBuilder() - .WithTagMapping("!dbl", typeof(DoublyConverted)) - .Build(); - - var result = sut.Deserialize>("!dbl hello: !dbl you"); - - Assert.True(result.ContainsKey(5)); - Assert.Equal(3, result[5]); - } - - [Fact] - public void InfiniteRecursionIsDetected() - { - var sut = new SerializerBuilder() - .DisableAliases() - .Build(); - - var recursionRoot = new - { - Nested = new[] - { - new Dictionary() - } - }; - - recursionRoot.Nested[0].Add("loop", recursionRoot); - - var exception = Assert.Throws(() => sut.Serialize(recursionRoot)); - } - - [Fact] - public void TuplesAreSerializable() - { - var sut = new SerializerBuilder() - .Build(); - - var yaml = sut.Serialize(new[] - { - Tuple.Create(1, "one"), - Tuple.Create(2, "two"), - }); - - var expected = Yaml.Text(@" - - Item1: 1 - Item2: one - - Item1: 2 - Item2: two - "); - - Assert.Equal(expected.NormalizeNewLines(), yaml.NormalizeNewLines().TrimNewLines()); - } - - [Fact] - public void ValueTuplesAreSerializableWithoutMetadata() - { - var sut = new SerializerBuilder() - .Build(); - - var yaml = sut.Serialize(new[] - { - (num: 1, txt: "one"), - (num: 2, txt: "two"), - }); - - var expected = Yaml.Text(@" - - Item1: 1 - Item2: one - - Item1: 2 - Item2: two - "); - - Assert.Equal(expected.NormalizeNewLines(), yaml.NormalizeNewLines().TrimNewLines()); - } - - [Fact] - public void AnchorNameWithTrailingColonReferencedInKeyCanBeDeserialized() - { - var sut = new Deserializer(); - var deserialized = sut.Deserialize>(Yaml.ReaderForText(@" - a: &::::scaryanchor:::: anchor "" value "" - *::::scaryanchor::::: 2 - myvalue: *::::scaryanchor:::: - ")); - - Assert.Equal(@"anchor "" value """, deserialized["a"]); - Assert.Equal("2", deserialized[@"anchor "" value """]); - Assert.Equal(@"anchor "" value """, deserialized["myvalue"]); - } - - [Fact] - public void AliasBeforeAnchorCannotBeDeserialized() - { - var sut = new Deserializer(); - Action action = () => sut.Deserialize>(@" -a: *anchor1 -b: &anchor1 test0 -c: *anchor1"); - - action.ShouldThrow(); - } - - [Fact] - public void AnchorWithAllowedCharactersCanBeDeserialized() - { - var sut = new Deserializer(); - var deserialized = sut.Deserialize>(Yaml.ReaderForText(@" - a: &@nchor<>""@-_123$>>>😁🎉🐻🍔end some value - myvalue: my *@nchor<>""@-_123$>>>😁🎉🐻🍔end test - interpolated value: *@nchor<>""@-_123$>>>😁🎉🐻🍔end - ")); - - Assert.Equal("some value", deserialized["a"]); - Assert.Equal(@"my *@nchor<>""@-_123$>>>😁🎉🐻🍔end test", deserialized["myvalue"]); - Assert.Equal("some value", deserialized["interpolated value"]); - } - - [Fact] - public void SerializationNonPublicPropertiesAreIgnored() - { - var sut = new SerializerBuilder().Build(); - var yaml = sut.Serialize(new NonPublicPropertiesExample()); - Assert.Equal("Public: public", yaml.TrimNewLines()); - } - - [Fact] - public void SerializationNonPublicPropertiesAreIncluded() - { - var sut = new SerializerBuilder().IncludeNonPublicProperties().Build(); - var yaml = sut.Serialize(new NonPublicPropertiesExample()); - - var expected = Yaml.Text(@" - Public: public - Internal: internal - Protected: protected - Private: private - "); - - Assert.Equal(expected.NormalizeNewLines(), yaml.NormalizeNewLines().TrimNewLines()); - } - - [Fact] - public void DeserializationNonPublicPropertiesAreIgnored() - { - var sut = new DeserializerBuilder().IgnoreUnmatchedProperties().Build(); - var deserialized = sut.Deserialize(Yaml.ReaderForText(@" - Public: public2 - Internal: internal2 - Protected: protected2 - Private: private2 - ")); - - Assert.Equal("public2,internal,protected,private", deserialized.ToString()); - } - - [Fact] - public void DeserializationNonPublicPropertiesAreIncluded() - { - var sut = new DeserializerBuilder().IncludeNonPublicProperties().Build(); - var deserialized = sut.Deserialize(Yaml.ReaderForText(@" - Public: public2 - Internal: internal2 - Protected: protected2 - Private: private2 - ")); - - Assert.Equal("public2,internal2,protected2,private2", deserialized.ToString()); - } - - [Fact] - public void SerializationNonPublicFieldsAreIgnored() - { - var sut = new SerializerBuilder().Build(); - var yaml = sut.Serialize(new NonPublicFieldsExample()); - Assert.Equal("Public: public", yaml.TrimNewLines()); - } - - [Fact] - public void DeserializationNonPublicFieldsAreIgnored() - { - var sut = new DeserializerBuilder().IgnoreUnmatchedProperties().Build(); - var deserialized = sut.Deserialize(Yaml.ReaderForText(@" - Public: public2 - Internal: internal2 - Protected: protected2 - Private: private2 - ")); - - Assert.Equal("public2,internal,protected,private", deserialized.ToString()); - } - - [Fact] - public void ShouldNotIndentSequences() - { - var sut = new SerializerBuilder() - .Build(); - - var yaml = sut.Serialize(new - { - first = "first", - items = new[] - { - "item1", - "item2" - }, - nested = new[] - { - new - { - name = "name1", - more = new[] - { - "nested1", - "nested2" - } - } - } - }); - - var expected = Yaml.Text(@" - first: first - items: - - item1 - - item2 - nested: - - name: name1 - more: - - nested1 - - nested2 - "); - - Assert.Equal(expected.NormalizeNewLines(), yaml.NormalizeNewLines().TrimNewLines()); - } - - [Fact] - public void ShouldIndentSequences() - { - var sut = new SerializerBuilder() - .WithIndentedSequences() - .Build(); - - var yaml = sut.Serialize(new - { - first = "first", - items = new[] - { - "item1", - "item2" - }, - nested = new[] - { - new - { - name = "name1", - more = new[] - { - "nested1", - "nested2" - } - } - } - }); - - var expected = Yaml.Text(@" - first: first - items: - - item1 - - item2 - nested: - - name: name1 - more: - - nested1 - - nested2 - "); - - Assert.Equal(expected.NormalizeNewLines(), yaml.NormalizeNewLines().TrimNewLines()); - } - - [Fact] - public void ExampleFromSpecificationIsHandledCorrectlyWithLateDefine() - { - var parser = new MergingParser(Yaml.ParserForText(@" - # All the following maps are equal: - results: - - # Explicit keys - x: 1 - y: 2 - r: 10 - label: center/big - - - # Merge one map - << : *CENTER - r: 10 - label: center/big - - - # Merge multiple maps - << : [ *CENTER, *BIG ] - label: center/big - - - # Override - << : [ *BIG, *LEFT, *SMALL ] - x: 1 - label: center/big - - obj: - - &CENTER { x: 1, y: 2 } - - &LEFT { x: 0, y: 2 } - - &SMALL { r: 1 } - - &BIG { r: 10 } - ")); - - var result = Deserializer.Deserialize>>>(parser); - - int index = 0; - foreach (var mapping in result["results"]) - { - mapping.Should() - .Contain("x", "1", "'x' should be '1' in result #{0}", index) - .And.Contain("y", "2", "'y' should be '2' in result #{0}", index) - .And.Contain("r", "10", "'r' should be '10' in result #{0}", index) - .And.Contain("label", "center/big", "'label' should be 'center/big' in result #{0}", index); - - ++index; - } - } - - public class CycleTestEntity - { - public CycleTestEntity Cycle { get; set; } - } - - [Fact] - public void SerializeCycleWithAlias() - { - var sut = new SerializerBuilder() - .WithTagMapping("!CycleTag", typeof(CycleTestEntity)) - .Build(); - - var entity = new CycleTestEntity(); - entity.Cycle = entity; - var yaml = sut.Serialize(entity); - var expected = Yaml.Text(@"&o0 !CycleTag -Cycle: *o0"); - - Assert.Equal(expected.NormalizeNewLines(), yaml.NormalizeNewLines().TrimNewLines()); - } - - [Fact] - public void DeserializeCycleWithAlias() - { - var sut = new DeserializerBuilder() - .WithTagMapping("!CycleTag", typeof(CycleTestEntity)) - .Build(); - - var yaml = Yaml.Text(@"&o0 !CycleTag -Cycle: *o0"); - var obj = sut.Deserialize(yaml); - - Assert.Same(obj, obj.Cycle); - } - - [Fact] - public void DeserializeCycleWithoutAlias() - { - var sut = new DeserializerBuilder() - .Build(); - - var yaml = Yaml.Text(@"&o0 -Cycle: *o0"); - var obj = sut.Deserialize(yaml); - - Assert.Same(obj, obj.Cycle); - } - - public static IEnumerable Depths => Enumerable.Range(1, 10).Select(i => new[] { (object)i }); - - [Theory] - [MemberData(nameof(Depths))] - public void DeserializeCycleWithAnchorsWithDepth(int? depth) - { - var sut = new DeserializerBuilder() - .WithTagMapping("!CycleTag", typeof(CycleTestEntity)) - .Build(); - - StringBuilder builder = new StringBuilder(@"&o0 !CycleTag"); - builder.AppendLine(); - string indentation; - for (int i = 0; i < depth - 1; ++i) - { - indentation = string.Concat(Enumerable.Repeat(" ", i)); - builder.AppendLine($"{indentation}Cycle: !CycleTag"); - } - indentation = string.Concat(Enumerable.Repeat(" ", depth.Value - 1)); - builder.AppendLine($"{indentation}Cycle: *o0"); - var yaml = Yaml.Text(builder.ToString()); - var obj = sut.Deserialize(yaml); - CycleTestEntity iterator = obj; - for (int i = 0; i < depth; ++i) - { - iterator = iterator.Cycle; - } - Assert.Same(obj, iterator); - } - - [Fact] - public void RoundtripWindowsNewlines() - { - var text = $"Line1{Environment.NewLine}Line2{Environment.NewLine}Line3{Environment.NewLine}{Environment.NewLine}Line4"; - - var sut = new SerializerBuilder().Build(); - var dut = new DeserializerBuilder().Build(); - - using var writer = new StringWriter { NewLine = Environment.NewLine }; - sut.Serialize(writer, new StringContainer { Text = text }); - var serialized = writer.ToString(); - - using var reader = new StringReader(serialized); - var roundtrippedText = dut.Deserialize(reader).Text.NormalizeNewLines(); - Assert.Equal(text, roundtrippedText); - } - - [Theory] - [InlineData("NULL")] - [InlineData("Null")] - [InlineData("null")] - [InlineData("~")] - [InlineData("true")] - [InlineData("false")] - [InlineData("True")] - [InlineData("False")] - [InlineData("TRUE")] - [InlineData("FALSE")] - [InlineData("0o77")] - [InlineData("0x7A")] - [InlineData("+1e10")] - [InlineData("1E10")] - [InlineData("+.inf")] - [InlineData("-.inf")] - [InlineData(".inf")] - [InlineData(".nan")] - [InlineData(".NaN")] - [InlineData(".NAN")] - public void StringsThatMatchKeywordsAreQuoted(string input) - { - var serializer = new SerializerBuilder().WithQuotingNecessaryStrings().Build(); - var o = new { text = input }; - var yaml = serializer.Serialize(o); - Assert.Equal($"text: \"{input}\"{Environment.NewLine}", yaml); - } - - public static IEnumerable Yaml1_1SpecialStringsData = new[] - { - "-.inf", "-.Inf", "-.INF", "-0", "-0100_200", "-0b101", "-0x30", "-190:20:30", "-23", "-3.14", - "._", "._14", ".", ".0", ".1_4", ".14", ".3E-1", ".3e+3", ".inf", ".Inf", - ".INF", ".nan", ".NaN", ".NAN", "+.inf", "+.Inf", "+.INF", "+0.3e+3", "+0", - "+0100_200", "+0b100", "+190:20:30", "+23", "+3.14", "~", "0.0", "0", "00", "001.23", - "0011", "010", "02_0", "07", "0b0", "0b100_101", "0o0", "0o10", "0o7", "0x0", - "0x10", "0x2_0", "0x42", "0xa", "100_000", "190:20:30.15", "190:20:30", "23", "3.", "3.14", "3.3e+3", - "85_230.15", "85.230_15e+03", "false", "False", "FALSE", "n", "N", "no", "No", "NO", - "null", "Null", "NULL", "off", "Off", "OFF", "on", "On", "ON", "true", "True", "TRUE", - "y", "Y", "yes", "Yes", "YES" - }.Select(v => new object[] { v }).ToList(); - - [Theory] - [MemberData(nameof(Yaml1_1SpecialStringsData))] - public void StringsThatMatchYaml1_1KeywordsAreQuoted(string input) - { - var serializer = new SerializerBuilder().WithQuotingNecessaryStrings(true).Build(); - var o = new { text = input }; - var yaml = serializer.Serialize(o); - Assert.Equal($"text: \"{input}\"{Environment.NewLine}", yaml); - } - - [Fact] - public void KeysOnConcreteClassDontGetQuoted_TypeStringGetsQuoted() - { - var serializer = new SerializerBuilder().WithQuotingNecessaryStrings().Build(); - var deserializer = new DeserializerBuilder().WithAttemptingUnquotedStringTypeDeserialization().Build(); - var yaml = @" -True: null -False: hello -Null: true -"; - var obj = deserializer.Deserialize>(yaml); - var result = serializer.Serialize(obj); - obj.True.Should().BeNull(); - obj.False.Should().Be("hello"); - obj.Null.Should().Be("true"); - result.Should().Be($"True: {Environment.NewLine}False: hello{Environment.NewLine}Null: \"true\"{Environment.NewLine}"); - } - - [Fact] - public void KeysOnConcreteClassDontGetQuoted_TypeBoolDoesNotGetQuoted() - { - var serializer = new SerializerBuilder().WithQuotingNecessaryStrings().Build(); - var deserializer = new DeserializerBuilder().WithAttemptingUnquotedStringTypeDeserialization().Build(); - var yaml = @" -True: null -False: hello -Null: true -"; - var obj = deserializer.Deserialize>(yaml); - var result = serializer.Serialize(obj); - obj.True.Should().BeNull(); - obj.False.Should().Be("hello"); - obj.Null.Should().BeTrue(); - result.Should().Be($"True: {Environment.NewLine}False: hello{Environment.NewLine}Null: true{Environment.NewLine}"); - } - - [Fact] - public void SerializeStateMethodsGetCalledOnce() - { - var serializer = new SerializerBuilder().Build(); - var test = new TestState(); - serializer.Serialize(test); - - Assert.Equal(1, test.OnSerializedCallCount); - Assert.Equal(1, test.OnSerializingCallCount); - } - - [Fact] - public void SerializeEnumAsNumber() - { - var serializer = new SerializerBuilder().WithYamlFormatter(new YamlFormatter - { - FormatEnum = (o, namingConvention) => ((int)o).ToString(), - PotentiallyQuoteEnums = (_) => false - }).Build(); - var deserializer = DeserializerBuilder.Build(); - - var value = serializer.Serialize(TestEnumAsNumber.Test1); - Assert.Equal("1", value.TrimNewLines()); - var v = deserializer.Deserialize(value); - Assert.Equal(TestEnumAsNumber.Test1, v); - - value = serializer.Serialize(TestEnumAsNumber.Test1 | TestEnumAsNumber.Test2); - Assert.Equal("3", value.TrimNewLines()); - v = deserializer.Deserialize(value); - Assert.Equal(TestEnumAsNumber.Test1 | TestEnumAsNumber.Test2, v); - } - - [Fact] - public void TabsGetQuotedWhenQuoteNecessaryStringsIsOn() - { - var serializer = new SerializerBuilder() - .WithQuotingNecessaryStrings() - .Build(); - - var s = "\t, something"; - var yaml = serializer.Serialize(s); - var deserializer = new DeserializerBuilder().Build(); - var value = deserializer.Deserialize(yaml); - Assert.Equal(s, value); - } - - [Fact] - public void SpacesGetQuotedWhenQuoteNecessaryStringsIsOn() - { - var serializer = new SerializerBuilder() - .WithQuotingNecessaryStrings() - .Build(); - - var s = " , something"; - var yaml = serializer.Serialize(s); - var deserializer = new DeserializerBuilder().Build(); - var value = deserializer.Deserialize(yaml); - Assert.Equal(s, value); - } - - [Flags] - private enum TestEnumAsNumber - { - Test1 = 1, - Test2 = 2 - } - - [Fact] - public void NamingConventionAppliedToEnum() - { - var serializer = new SerializerBuilder().WithEnumNamingConvention(CamelCaseNamingConvention.Instance).Build(); - ScalarStyle style = ScalarStyle.Plain; - var serialized = serializer.Serialize(style); - Assert.Equal("plain", serialized.RemoveNewLines()); - } - - [Fact] - public void NamingConventionAppliedToEnumWhenDeserializing() - { - var serializer = new DeserializerBuilder().WithEnumNamingConvention(UnderscoredNamingConvention.Instance).Build(); - var yaml = "Double_Quoted"; - ScalarStyle expected = ScalarStyle.DoubleQuoted; - var actual = serializer.Deserialize(yaml); - Assert.Equal(expected, actual); - } - - [Fact] - [Trait("motive", "issue #656")] - public void NestedDictionaryTypes_ShouldRoundtrip() - { - var serializer = new SerializerBuilder().EnsureRoundtrip().Build(); - var yaml = serializer.Serialize(new HasNestedDictionary { Lookups = { [1] = new HasNestedDictionary.Payload { I = 1 } } }, typeof(HasNestedDictionary)); - var dct = new DeserializerBuilder().Build().Deserialize(yaml); - Assert.Contains(new KeyValuePair(1, new HasNestedDictionary.Payload { I = 1 }), dct.Lookups); - } - - public class TestState - { - public int OnSerializedCallCount { get; set; } - public int OnSerializingCallCount { get; set; } - - public string Test { get; set; } = string.Empty; - - [OnSerialized] - public void Serialized() => OnSerializedCallCount++; - - [OnSerializing] - public void Serializing() => OnSerializingCallCount++; - } - - public class ReservedWordsTestClass - { - public string True { get; set; } - public string False { get; set; } - public TNullType Null { get; set; } - } - - [TypeConverter(typeof(DoublyConvertedTypeConverter))] - public class DoublyConverted - { - public string Value { get; set; } - } - - public class DoublyConvertedTypeConverter : TypeConverter - { - public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) - { - return destinationType == typeof(int); - } - - public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) - { - return ((DoublyConverted)value).Value.Length; - } - - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - return sourceType == typeof(string); - } - - public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) - { - return new DoublyConverted { Value = (string)value }; - } - } - - public class NamingConventionDisabled - { - [YamlMember(ApplyNamingConventions = false)] - public string NoConvention { get; set; } - } - - public class NonSerializableContainer - { - public NonSerializable Value { get; set; } - } - - public class NonSerializable - { - public string WillThrow { get { throw new Exception(); } } - - public string Text { get; set; } - } - - public class StringContainer - { - public string Text { get; set; } - } - - public class NonSerializableTypeConverter : IYamlTypeConverter - { - public bool Accepts(Type type) - { - return typeof(NonSerializable).IsAssignableFrom(type); - } - - public object ReadYaml(IParser parser, Type type) - { - var scalar = parser.Consume(); - return new NonSerializable { Text = scalar.Value }; - } - - public void WriteYaml(IEmitter emitter, object value, Type type) - { - emitter.Emit(new Scalar(((NonSerializable)value).Text)); - } - } - - public sealed class HasNestedDictionary - { - public Dictionary Lookups { get; set; } = new Dictionary(); - - public struct Payload - { - public int I { get; set; } - } - } - } -} +// This file is part of YamlDotNet - A .NET library for YAML. +// Copyright (c) Antoine Aubry and contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Drawing; +using System.Dynamic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Text.RegularExpressions; +using FakeItEasy; +using FluentAssertions; +using FluentAssertions.Common; +using Xunit; +using YamlDotNet.Core; +using YamlDotNet.Core.Events; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.Callbacks; +using YamlDotNet.Serialization.NamingConventions; +using YamlDotNet.Serialization.ObjectFactories; + +namespace YamlDotNet.Test.Serialization +{ + public class SerializationTests : SerializationTestHelper + { + #region Test Cases + + private static readonly string[] TrueStrings = { "true", "y", "yes", "on" }; + private static readonly string[] FalseStrings = { "false", "n", "no", "off" }; + + public static IEnumerable DeserializeScalarBoolean_TestCases + { + get + { + foreach (var trueString in TrueStrings) + { + yield return new object[] { trueString, true }; + yield return new object[] { trueString.ToUpper(), true }; + } + + foreach (var falseString in FalseStrings) + { + yield return new object[] { falseString, false }; + yield return new object[] { falseString.ToUpper(), false }; + } + } + } + + #endregion + + [Fact] + public void DeserializeEmptyDocument() + { + var emptyText = string.Empty; + + var array = Deserializer.Deserialize(UsingReaderFor(emptyText)); + + array.Should().BeNull(); + } + + [Fact] + public void DeserializeScalar() + { + var stream = Yaml.ReaderFrom("02-scalar-in-imp-doc.yaml"); + + var result = Deserializer.Deserialize(stream); + + result.Should().Be("a scalar"); + } + + [Theory] + [MemberData(nameof(DeserializeScalarBoolean_TestCases))] + public void DeserializeScalarBoolean(string value, bool expected) + { + var result = Deserializer.Deserialize(UsingReaderFor(value)); + + result.Should().Be(expected); + } + + [Fact] + public void DeserializeScalarBooleanThrowsWhenInvalid() + { + Action action = () => Deserializer.Deserialize(UsingReaderFor("not-a-boolean")); + + action.ShouldThrow().WithInnerException(); + } + + [Fact] + public void DeserializeScalarZero() + { + var result = Deserializer.Deserialize(UsingReaderFor("0")); + + result.Should().Be(0); + } + + [Fact] + public void DeserializeScalarDecimal() + { + var result = Deserializer.Deserialize(UsingReaderFor("+1_234_567")); + + result.Should().Be(1234567); + } + + [Fact] + public void DeserializeScalarBinaryNumber() + { + var result = Deserializer.Deserialize(UsingReaderFor("-0b1_0010_1001_0010")); + + result.Should().Be(-4754); + } + + [Fact] + public void DeserializeScalarOctalNumber() + { + var result = Deserializer.Deserialize(UsingReaderFor("+071_352")); + + result.Should().Be(29418); + } + + [Fact] + public void DeserializeNullableScalarOctalNumber() + { + var result = Deserializer.Deserialize(UsingReaderFor("+071_352")); + + result.Should().Be(29418); + } + + [Fact] + public void DeserializeScalarHexNumber() + { + var result = Deserializer.Deserialize(UsingReaderFor("-0x_0F_B9")); + + result.Should().Be(-0xFB9); + } + + [Fact] + public void DeserializeScalarLongBase60Number() + { + var result = Deserializer.Deserialize(UsingReaderFor("99_:_58:47:3:6_2:10")); + + result.Should().Be(77744246530L); + } + + [Theory] + [InlineData(EnumExample.One)] + [InlineData(EnumExample.One | EnumExample.Two)] + public void RoundtripEnums(EnumExample value) + { + var result = DoRoundtripFromObjectTo(value); + + result.Should().Be(value); + } + + [Theory] + [InlineData(EnumExample.One)] + [InlineData(EnumExample.One | EnumExample.Two)] + [InlineData(null)] + public void RoundtripNullableEnums(EnumExample? value) + { + var result = DoRoundtripFromObjectTo(value); + + result.Should().Be(value); + } + + [Fact] + public void RoundtripNullableStructWithValue() + { + var value = new StructExample { Value = 2 }; + + var result = DoRoundtripFromObjectTo(value); + + result.Should().Be(value); + } + + [Fact] + public void RoundtripNullableStructWithoutValue() + { + var result = DoRoundtripFromObjectTo(null); + + result.Should().Be(null); + } + + [Fact] + public void SerializeCircularReference() + { + var obj = new CircularReference(); + obj.Child1 = new CircularReference + { + Child1 = obj, + Child2 = obj + }; + + Action action = () => SerializerBuilder.EnsureRoundtrip().Build().Serialize(new StringWriter(), obj, typeof(CircularReference)); + + action.ShouldNotThrow(); + } + + [Fact] + public void DeserializeIncompleteDirective() + { + Action action = () => Deserializer.Deserialize(UsingReaderFor("%Y")); + + action.ShouldThrow() + .WithMessage("While scanning a directive, found unexpected end of stream."); + } + + [Fact] + public void DeserializeSkippedReservedDirective() + { + Action action = () => Deserializer.Deserialize(UsingReaderFor("%Y ")); + + action.ShouldNotThrow(); + } + + [Fact] + public void DeserializeCustomTags() + { + var stream = Yaml.ReaderFrom("tags.yaml"); + + DeserializerBuilder.WithTagMapping("tag:yaml.org,2002:point", typeof(Point)); + var result = Deserializer.Deserialize(stream); + + result.Should().BeOfType().And + .Subject.As() + .ShouldBeEquivalentTo(new { X = 10, Y = 20 }, o => o.ExcludingMissingMembers()); + } + + [Fact] + public void DeserializeWithGapsBetweenKeys() + { + var yamlReader = new StringReader(@"Text: > + Some Text. + +Value: foo"); + var result = Deserializer.Deserialize(yamlReader); + + result.Should().NotBeNull(); + } + + [Fact] + public void SerializeCustomTags() + { + var expectedResult = Yaml.ReaderFrom("tags.yaml").ReadToEnd().NormalizeNewLines(); + SerializerBuilder + .ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitDefaults) + .WithTagMapping(new TagName("tag:yaml.org,2002:point"), typeof(Point)); + + var point = new Point(10, 20); + var result = Serializer.Serialize(point); + + result.Should().Be(expectedResult); + } + + [Fact] + public void SerializeWithCRLFNewLine() + { + var expectedResult = Yaml + .ReaderFrom("list.yaml") + .ReadToEnd() + .NormalizeNewLines() + .Replace(Environment.NewLine, "\r\n"); + + var list = new string[] { "one", "two", "three" }; + var result = SerializerBuilder + .WithNewLine("\r\n") + .Build() + .Serialize(list); + + result.Should().Be(expectedResult); + } + + [Fact] + public void SerializeWithLFNewLine() + { + var expectedResult = Yaml + .ReaderFrom("list.yaml") + .ReadToEnd() + .NormalizeNewLines() + .Replace(Environment.NewLine, "\n"); + + var list = new string[] { "one", "two", "three" }; + var result = SerializerBuilder + .WithNewLine("\n") + .Build() + .Serialize(list); + + result.Should().Be(expectedResult); + } + + [Fact] + public void SerializeWithCRNewLine() + { + var expectedResult = Yaml + .ReaderFrom("list.yaml") + .ReadToEnd() + .NormalizeNewLines() + .Replace(Environment.NewLine, "\r"); + + var list = new string[] { "one", "two", "three" }; + var result = SerializerBuilder + .WithNewLine("\r") + .Build() + .Serialize(list); + + result.Should().Be(expectedResult); + } + + [Fact] + public void DeserializeExplicitType() + { + var text = Yaml.ReaderFrom("explicit-type.template").TemplatedOn(); + + var result = new DeserializerBuilder() + .WithTagMapping("!Simple", typeof(Simple)) + .Build() + .Deserialize(UsingReaderFor(text)); + + result.aaa.Should().Be("bbb"); + } + + [Fact] + public void DeserializeConvertible() + { + var text = Yaml.ReaderFrom("convertible.template").TemplatedOn(); + + var result = new DeserializerBuilder() + .WithTagMapping("!Convertible", typeof(Convertible)) + .Build() + .Deserialize(UsingReaderFor(text)); + + result.aaa.Should().Be("[hello, world]"); + } + + [Fact] + public void DeserializationFailsForUndefinedForwardReferences() + { + var text = Lines( + "Nothing: *forward", + "MyString: ForwardReference"); + + Action action = () => Deserializer.Deserialize(UsingReaderFor(text)); + + action.ShouldThrow(); + } + + [Fact] + public void RoundtripObject() + { + var obj = new Example(); + + var result = DoRoundtripFromObjectTo( + obj, + new SerializerBuilder() + .WithTagMapping("!Example", typeof(Example)) + .EnsureRoundtrip() + .Build(), + new DeserializerBuilder() + .WithTagMapping("!Example", typeof(Example)) + .Build() + ); + + result.ShouldBeEquivalentTo(obj); + } + + [Fact] + public void RoundtripObjectWithDefaults() + { + var obj = new Example(); + + var result = DoRoundtripFromObjectTo( + obj, + new SerializerBuilder() + .WithTagMapping("!Example", typeof(Example)) + .EnsureRoundtrip() + .Build(), + new DeserializerBuilder() + .WithTagMapping("!Example", typeof(Example)) + .Build() + ); + + result.ShouldBeEquivalentTo(obj); + } + + [Fact] + public void RoundtripAnonymousType() + { + var data = new { Key = 3 }; + + var result = DoRoundtripFromObjectTo>(data); + + result.Should().Equal(new Dictionary { + { "Key", "3" } + }); + } + + [Fact] + public void RoundtripWithYamlTypeConverter() + { + var obj = new MissingDefaultCtor("Yo"); + + SerializerBuilder + .EnsureRoundtrip() + .WithTypeConverter(new MissingDefaultCtorConverter()); + + DeserializerBuilder + .WithTypeConverter(new MissingDefaultCtorConverter()); + + var result = DoRoundtripFromObjectTo(obj, Serializer, Deserializer); + + result.Value.Should().Be("Yo"); + } + + [Fact] + public void RoundtripAlias() + { + var writer = new StringWriter(); + var input = new NameConvention { AliasTest = "Fourth" }; + + SerializerBuilder + .ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitDefaults); + + Serializer.Serialize(writer, input, input.GetType()); + var text = writer.ToString(); + + // Todo: use RegEx once FluentAssertions 2.2 is released + text.TrimEnd('\r', '\n').Should().Be("fourthTest: Fourth"); + + var output = Deserializer.Deserialize(UsingReaderFor(text)); + + output.AliasTest.Should().Be(input.AliasTest); + } + + [Fact] + public void RoundtripAliasOverride() + { + var writer = new StringWriter(); + var input = new NameConvention { AliasTest = "Fourth" }; + + var attribute = new YamlMemberAttribute + { + Alias = "fourthOverride" + }; + + var serializer = new SerializerBuilder() + .ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitDefaults) + .WithAttributeOverride(nc => nc.AliasTest, attribute) + .Build(); + + serializer.Serialize(writer, input, input.GetType()); + var text = writer.ToString(); + + // Todo: use RegEx once FluentAssertions 2.2 is released + text.TrimEnd('\r', '\n').Should().Be("fourthOverride: Fourth"); + + DeserializerBuilder.WithAttributeOverride(n => n.AliasTest, attribute); + var output = Deserializer.Deserialize(UsingReaderFor(text)); + + output.AliasTest.Should().Be(input.AliasTest); + } + + [Fact] + // Todo: is the assert on the string necessary? + public void RoundtripDerivedClass() + { + var obj = new InheritanceExample + { + SomeScalar = "Hello", + RegularBase = new Derived { BaseProperty = "foo", DerivedProperty = "bar" } + }; + + var result = DoRoundtripFromObjectTo( + obj, + new SerializerBuilder() + .WithTagMapping("!InheritanceExample", typeof(InheritanceExample)) + .WithTagMapping("!Derived", typeof(Derived)) + .EnsureRoundtrip() + .Build(), + new DeserializerBuilder() + .WithTagMapping("!InheritanceExample", typeof(InheritanceExample)) + .WithTagMapping("!Derived", typeof(Derived)) + .Build() + ); + + result.SomeScalar.Should().Be("Hello"); + result.RegularBase.Should().BeOfType().And + .Subject.As().ShouldBeEquivalentTo(new { ChildProp = "bar" }, o => o.ExcludingMissingMembers()); + } + + [Fact] + public void RoundtripDerivedClassWithSerializeAs() + { + var obj = new InheritanceExample + { + SomeScalar = "Hello", + BaseWithSerializeAs = new Derived { BaseProperty = "foo", DerivedProperty = "bar" } + }; + + var result = DoRoundtripFromObjectTo( + obj, + new SerializerBuilder() + .WithTagMapping("!InheritanceExample", typeof(InheritanceExample)) + .EnsureRoundtrip() + .Build(), + new DeserializerBuilder() + .WithTagMapping("!InheritanceExample", typeof(InheritanceExample)) + .Build() + ); + + result.BaseWithSerializeAs.Should().BeOfType().And + .Subject.As().ShouldBeEquivalentTo(new { ParentProp = "foo" }, o => o.ExcludingMissingMembers()); + } + + [Fact] + public void RoundtripInterfaceProperties() + { + AssumingDeserializerWith(new LambdaObjectFactory(t => + { + if (t == typeof(InterfaceExample)) { return new InterfaceExample(); } + else if (t == typeof(IDerived)) { return new Derived(); } + return null; + })); + + var obj = new InterfaceExample + { + Derived = new Derived { BaseProperty = "foo", DerivedProperty = "bar" } + }; + + var result = DoRoundtripFromObjectTo(obj); + + result.Derived.Should().BeOfType().And + .Subject.As().ShouldBeEquivalentTo(new { BaseProperty = "foo", DerivedProperty = "bar" }, o => o.ExcludingMissingMembers()); + } + + [Fact] + public void DeserializeGuid() + { + var stream = Yaml.ReaderFrom("guid.yaml"); + var result = Deserializer.Deserialize(stream); + + result.Should().Be(new Guid("9462790d5c44468985425e2dd38ebd98")); + } + + [Fact] + public void DeserializationOfOrderedProperties() + { + var stream = Yaml.ReaderFrom("ordered-properties.yaml"); + + var orderExample = Deserializer.Deserialize(stream); + + orderExample.Order1.Should().Be("Order1 value"); + orderExample.Order2.Should().Be("Order2 value"); + } + + [Fact] + public void DeserializeEnumerable() + { + var obj = new[] { new Simple { aaa = "bbb" } }; + + var result = DoRoundtripFromObjectTo>(obj); + + result.Should().ContainSingle(item => "bbb".Equals(item.aaa)); + } + + [Fact] + public void DeserializeArray() + { + var stream = Yaml.ReaderFrom("list.yaml"); + + var result = Deserializer.Deserialize(stream); + + result.Should().Equal(new[] { "one", "two", "three" }); + } + + [Fact] + public void DeserializeList() + { + var stream = Yaml.ReaderFrom("list.yaml"); + + var result = Deserializer.Deserialize(stream); + + result.Should().BeAssignableTo().And + .Subject.As().Should().Equal(new[] { "one", "two", "three" }); + } + + [Fact] + public void DeserializeExplicitList() + { + var stream = Yaml.ReaderFrom("list-explicit.yaml"); + + var result = new DeserializerBuilder() + .WithTagMapping("!List", typeof(List)) + .Build() + .Deserialize(stream); + + result.Should().BeAssignableTo>().And + .Subject.As>().Should().Equal(3, 4, 5); + } + + [Fact] + public void RoundtripList() + { + var obj = new List { 2, 4, 6 }; + + var result = DoRoundtripOn>(obj, SerializerBuilder.EnsureRoundtrip().Build()); + + result.Should().Equal(obj); + } + + [Fact] + public void RoundtripArrayWithTypeConversion() + { + var obj = new object[] { 1, 2, "3" }; + + var result = DoRoundtripFromObjectTo(obj); + + result.Should().Equal(1, 2, 3); + } + + [Fact] + public void RoundtripArrayOfIdenticalObjects() + { + var z = new Simple { aaa = "bbb" }; + var obj = new[] { z, z, z }; + + var result = DoRoundtripOn(obj); + + result.Should().HaveCount(3).And.OnlyContain(x => z.aaa.Equals(x.aaa)); + result[0].Should().BeSameAs(result[1]).And.BeSameAs(result[2]); + } + + [Fact] + public void DeserializeDictionary() + { + var stream = Yaml.ReaderFrom("dictionary.yaml"); + + var result = Deserializer.Deserialize(stream); + + result.Should().BeAssignableTo>().And.Subject + .As>().Should().Equal(new Dictionary { + { "key1", "value1" }, + { "key2", "value2" } + }); + } + + [Fact] + public void DeserializeExplicitDictionary() + { + var stream = Yaml.ReaderFrom("dictionary-explicit.yaml"); + + var result = new DeserializerBuilder() + .WithTagMapping("!Dictionary", typeof(Dictionary)) + .Build() + .Deserialize(stream); + + result.Should().BeAssignableTo>().And.Subject + .As>().Should().Equal(new Dictionary { + { "key1", 1 }, + { "key2", 2 } + }); + } + + [Fact] + public void RoundtripDictionary() + { + var obj = new Dictionary { + { "key1", "value1" }, + { "key2", "value2" }, + { "key3", "value3" } + }; + + var result = DoRoundtripFromObjectTo>(obj); + + result.Should().Equal(obj); + } + + [Fact] + public void DeserializeListOfDictionaries() + { + var stream = Yaml.ReaderFrom("list-of-dictionaries.yaml"); + + var result = Deserializer.Deserialize>>(stream); + + result.ShouldBeEquivalentTo(new[] { + new Dictionary { + { "connection", "conn1" }, + { "path", "path1" } + }, + new Dictionary { + { "connection", "conn2" }, + { "path", "path2" } + }}, opt => opt.WithStrictOrderingFor(root => root)); + } + + [Fact] + public void DeserializeTwoDocuments() + { + var reader = ParserFor(Lines( + "---", + "aaa: 111", + "---", + "aaa: 222", + "...")); + + reader.Consume(); + var one = Deserializer.Deserialize(reader); + var two = Deserializer.Deserialize(reader); + + one.ShouldBeEquivalentTo(new { aaa = "111" }); + two.ShouldBeEquivalentTo(new { aaa = "222" }); + } + + [Fact] + public void DeserializeThreeDocuments() + { + var reader = ParserFor(Lines( + "---", + "aaa: 111", + "---", + "aaa: 222", + "---", + "aaa: 333", + "...")); + + reader.Consume(); + var one = Deserializer.Deserialize(reader); + var two = Deserializer.Deserialize(reader); + var three = Deserializer.Deserialize(reader); + + reader.Accept(out var _).Should().BeTrue("reader should have reached StreamEnd"); + one.ShouldBeEquivalentTo(new { aaa = "111" }); + two.ShouldBeEquivalentTo(new { aaa = "222" }); + three.ShouldBeEquivalentTo(new { aaa = "333" }); + } + + [Fact] + public void SerializeGuid() + { + var guid = new Guid("{9462790D-5C44-4689-8542-5E2DD38EBD98}"); + + var writer = new StringWriter(); + + Serializer.Serialize(writer, guid); + var serialized = writer.ToString(); + Regex.IsMatch(serialized, "^" + guid.ToString("D")).Should().BeTrue("serialized content should contain the guid, but instead contained: " + serialized); + } + + [Fact] + public void SerializeNullObject() + { +#nullable enable + object? obj = null; + + var writer = new StringWriter(); + + Serializer.Serialize(writer, obj); + var serialized = writer.ToString(); + serialized.Should().Be("--- " + writer.NewLine); +#nullable restore + } + + [Fact] + public void SerializationOfNullInListsAreAlwaysEmittedWithoutUsingEmitDefaults() + { + var writer = new StringWriter(); + var obj = new[] { "foo", null, "bar" }; + + Serializer.Serialize(writer, obj); + var serialized = writer.ToString(); + + Regex.Matches(serialized, "-").Count.Should().Be(3, "there should have been 3 elements"); + } + + [Fact] + public void SerializationOfNullInListsAreAlwaysEmittedWhenUsingEmitDefaults() + { + var writer = new StringWriter(); + var obj = new[] { "foo", null, "bar" }; + + SerializerBuilder.Build().Serialize(writer, obj); + var serialized = writer.ToString(); + + Regex.Matches(serialized, "-").Count.Should().Be(3, "there should have been 3 elements"); + } + + [Fact] + public void SerializationIncludesKeyWhenEmittingDefaults() + { + var writer = new StringWriter(); + var obj = new Example { MyString = null }; + + SerializerBuilder.Build().Serialize(writer, obj, typeof(Example)); + + writer.ToString().Should().Contain("MyString"); + } + + [Fact] + [Trait("Motive", "Bug fix")] + public void SerializationIncludesKeyFromAnonymousTypeWhenEmittingDefaults() + { + var writer = new StringWriter(); + var obj = new { MyString = (string)null }; + + SerializerBuilder.Build().Serialize(writer, obj, obj.GetType()); + + writer.ToString().Should().Contain("MyString"); + } + + [Fact] + public void SerializationDoesNotIncludeKeyWhenDisregardingDefaults() + { + var writer = new StringWriter(); + var obj = new Example { MyString = null }; + + SerializerBuilder + .ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitDefaults); + + Serializer.Serialize(writer, obj, typeof(Example)); + + writer.ToString().Should().NotContain("MyString"); + } + + [Fact] + public void SerializationOfDefaultsWorkInJson() + { + var writer = new StringWriter(); + var obj = new Example { MyString = null }; + + SerializerBuilder.JsonCompatible().Build().Serialize(writer, obj, typeof(Example)); + + writer.ToString().Should().Contain("MyString"); + } + + [Fact] + public void SerializationOfLongKeysWorksInJson() + { + var writer = new StringWriter(); + var obj = new Dictionary + { + { new string('x', 3000), "extremely long key" } + }; + + SerializerBuilder.JsonCompatible().Build().Serialize(writer, obj, typeof(Dictionary)); + + writer.ToString().Should().NotContain("?"); + } + + [Fact] + public void SerializationOfAnchorWorksInJson() + { + var deserializer = new DeserializerBuilder().Build(); + var yamlObject = deserializer.Deserialize(Yaml.ReaderForText(@" +x: &anchor1 + z: + v: 1 +y: + k: *anchor1")); + + var serializer = new SerializerBuilder() + .JsonCompatible() + .Build(); + + serializer.Serialize(yamlObject).Trim().Should() + .BeEquivalentTo(@"{""x"": {""z"": {""v"": ""1""}}, ""y"": {""k"": {""z"": {""v"": ""1""}}}}"); + } + + [Fact] + // Todo: this is actually roundtrip + public void DeserializationOfDefaultsWorkInJson() + { + var writer = new StringWriter(); + var obj = new Example { MyString = null }; + + SerializerBuilder.EnsureRoundtrip().JsonCompatible().Build().Serialize(writer, obj, typeof(Example)); + var result = Deserializer.Deserialize(UsingReaderFor(writer)); + + result.MyString.Should().BeNull(); + } + + [Fact] + public void NullsRoundTrip() + { + var writer = new StringWriter(); + var obj = new Example { MyString = null }; + + SerializerBuilder.EnsureRoundtrip().Build().Serialize(writer, obj, typeof(Example)); + var result = Deserializer.Deserialize(UsingReaderFor(writer)); + + result.MyString.Should().BeNull(); + } + + [Theory] + [InlineData(typeof(SByteEnum))] + [InlineData(typeof(ByteEnum))] + [InlineData(typeof(Int16Enum))] + [InlineData(typeof(UInt16Enum))] + [InlineData(typeof(Int32Enum))] + [InlineData(typeof(UInt32Enum))] + [InlineData(typeof(Int64Enum))] + [InlineData(typeof(UInt64Enum))] + public void DeserializationOfEnumWorksInJson(Type enumType) + { + var defaultEnumValue = 0; + var nonDefaultEnumValue = Enum.GetValues(enumType).GetValue(1); + + var jsonSerializer = SerializerBuilder.EnsureRoundtrip().JsonCompatible().Build(); + var jsonSerializedEnum = jsonSerializer.Serialize(nonDefaultEnumValue); + + nonDefaultEnumValue.Should().NotBe(defaultEnumValue); + jsonSerializedEnum.Should().Contain($"\"{nonDefaultEnumValue}\""); + } + + [Fact] + public void SerializationOfOrderedProperties() + { + var obj = new OrderExample(); + var writer = new StringWriter(); + + Serializer.Serialize(writer, obj); + var serialized = writer.ToString(); + + serialized.Should() + .Be("Order1: Order1 value\r\nOrder2: Order2 value\r\n".NormalizeNewLines(), "the properties should be in the right order"); + } + + [Fact] + public void SerializationRespectsYamlIgnoreAttribute() + { + + var writer = new StringWriter(); + var obj = new IgnoreExample(); + + Serializer.Serialize(writer, obj); + var serialized = writer.ToString(); + + serialized.Should().NotContain("IgnoreMe"); + } + + [Fact] + public void SerializationRespectsYamlIgnoreAttributeOfDerivedClasses() + { + + var writer = new StringWriter(); + var obj = new IgnoreExampleDerived(); + + Serializer.Serialize(writer, obj); + var serialized = writer.ToString(); + + serialized.Should().NotContain("IgnoreMe"); + } + + [Fact] + public void SerializationRespectsYamlIgnoreOverride() + { + + var writer = new StringWriter(); + var obj = new Simple(); + + var ignore = new YamlIgnoreAttribute(); + var serializer = new SerializerBuilder() + .WithAttributeOverride(s => s.aaa, ignore) + .Build(); + + serializer.Serialize(writer, obj); + var serialized = writer.ToString(); + + serialized.Should().NotContain("aaa"); + } + + [Fact] + public void SerializationRespectsScalarStyle() + { + var writer = new StringWriter(); + var obj = new ScalarStyleExample(); + + Serializer.Serialize(writer, obj); + var serialized = writer.ToString(); + + serialized.Should() + .Be("LiteralString: |-\r\n Test\r\nDoubleQuotedString: \"Test\"\r\n".NormalizeNewLines(), "the properties should be specifically styled"); + } + + [Fact] + public void SerializationRespectsScalarStyleOverride() + { + var writer = new StringWriter(); + var obj = new ScalarStyleExample(); + + var serializer = new SerializerBuilder() + .WithAttributeOverride(e => e.LiteralString, new YamlMemberAttribute { ScalarStyle = ScalarStyle.DoubleQuoted }) + .WithAttributeOverride(e => e.DoubleQuotedString, new YamlMemberAttribute { ScalarStyle = ScalarStyle.Literal }) + .Build(); + + serializer.Serialize(writer, obj); + var serialized = writer.ToString(); + + serialized.Should() + .Be("LiteralString: \"Test\"\r\nDoubleQuotedString: |-\r\n Test\r\n".NormalizeNewLines(), "the properties should be specifically styled"); + } + + [Fact] + public void SerializationRespectsDefaultScalarStyle() + { + var writer = new StringWriter(); + var obj = new MixedFormatScalarStyleExample(new string[] { "01", "0.1", "myString" }); + + var serializer = new SerializerBuilder().WithDefaultScalarStyle(ScalarStyle.SingleQuoted).Build(); + + serializer.Serialize(writer, obj); + + var yaml = writer.ToString(); + + var expected = Yaml.Text(@" + Data: + - '01' + - '0.1' + - 'myString' + "); + + Assert.Equal(expected.NormalizeNewLines(), yaml.NormalizeNewLines().TrimNewLines()); + } + + [Fact] + public void SerializationDerivedAttributeOverride() + { + var writer = new StringWriter(); + var obj = new Derived { DerivedProperty = "Derived", BaseProperty = "Base" }; + + var ignore = new YamlIgnoreAttribute(); + var serializer = new SerializerBuilder() + .WithAttributeOverride(d => d.DerivedProperty, ignore) + .Build(); + + serializer.Serialize(writer, obj); + var serialized = writer.ToString(); + + serialized.Should() + .Be("BaseProperty: Base\r\n".NormalizeNewLines(), "the derived property should be specifically ignored"); + } + + [Fact] + public void SerializationBaseAttributeOverride() + { + var writer = new StringWriter(); + var obj = new Derived { DerivedProperty = "Derived", BaseProperty = "Base" }; + + var ignore = new YamlIgnoreAttribute(); + var serializer = new SerializerBuilder() + .WithAttributeOverride(b => b.BaseProperty, ignore) + .Build(); + + serializer.Serialize(writer, obj); + var serialized = writer.ToString(); + + serialized.Should() + .Be("DerivedProperty: Derived\r\n".NormalizeNewLines(), "the base property should be specifically ignored"); + } + + [Fact] + public void SerializationSkipsPropertyWhenUsingDefaultValueAttribute() + { + var writer = new StringWriter(); + var obj = new DefaultsExample { Value = DefaultsExample.DefaultValue }; + + SerializerBuilder + .ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitDefaults); + + Serializer.Serialize(writer, obj); + var serialized = writer.ToString(); + + serialized.Should().NotContain("Value"); + } + + [Fact] + public void SerializationEmitsPropertyWhenUsingEmitDefaultsAndDefaultValueAttribute() + { + var writer = new StringWriter(); + var obj = new DefaultsExample { Value = DefaultsExample.DefaultValue }; + + SerializerBuilder.Build().Serialize(writer, obj); + var serialized = writer.ToString(); + + serialized.Should().Contain("Value"); + } + + [Fact] + public void SerializationEmitsPropertyWhenValueDifferFromDefaultValueAttribute() + { + var writer = new StringWriter(); + var obj = new DefaultsExample { Value = "non-default" }; + + Serializer.Serialize(writer, obj); + var serialized = writer.ToString(); + + serialized.Should().Contain("Value"); + } + + [Fact] + public void SerializingAGenericDictionaryShouldNotThrowTargetException() + { + var obj = new CustomGenericDictionary { + { "hello", "world" } + }; + + Action action = () => Serializer.Serialize(new StringWriter(), obj); + + action.ShouldNotThrow(); + } + + [Fact] + public void SerializationUtilizeNamingConventions() + { + var convention = A.Fake(); + A.CallTo(() => convention.Apply(A._)).ReturnsLazily((string x) => x); + var obj = new NameConvention { FirstTest = "1", SecondTest = "2" }; + + var serializer = new SerializerBuilder() + .WithNamingConvention(convention) + .Build(); + + serializer.Serialize(new StringWriter(), obj); + + A.CallTo(() => convention.Apply("FirstTest")).MustHaveHappened(); + A.CallTo(() => convention.Apply("SecondTest")).MustHaveHappened(); + } + + [Fact] + public void DeserializationUtilizeNamingConventions() + { + var convention = A.Fake(); + A.CallTo(() => convention.Apply(A._)).ReturnsLazily((string x) => x); + var text = Lines( + "FirstTest: 1", + "SecondTest: 2"); + + var deserializer = new DeserializerBuilder() + .WithNamingConvention(convention) + .Build(); + + deserializer.Deserialize(UsingReaderFor(text)); + + A.CallTo(() => convention.Apply("FirstTest")).MustHaveHappened(); + A.CallTo(() => convention.Apply("SecondTest")).MustHaveHappened(); + } + + [Fact] + public void TypeConverterIsUsedOnListItems() + { + var text = Lines( + "- !{type}", + " Left: hello", + " Right: world") + .TemplatedOn(); + + var list = new DeserializerBuilder() + .WithTagMapping("!Convertible", typeof(Convertible)) + .Build() + .Deserialize>(UsingReaderFor(text)); + + list + .Should().NotBeNull() + .And.ContainSingle(c => c.Equals("[hello, world]")); + } + + [Fact] + public void BackreferencesAreMergedWithMappings() + { + var stream = Yaml.ReaderFrom("backreference.yaml"); + + var parser = new MergingParser(new Parser(stream)); + var result = Deserializer.Deserialize>>(parser); + + var alias = result["alias"]; + alias.Should() + .Contain("key1", "value1", "key1 should be inherited from the backreferenced mapping") + .And.Contain("key2", "Overriding key2", "key2 should be overriden by the actual mapping") + .And.Contain("key3", "value3", "key3 is defined in the actual mapping"); + } + + [Fact] + public void MergingDoesNotProduceDuplicateAnchors() + { + var parser = new MergingParser(Yaml.ParserForText(@" + anchor: &default + key1: &myValue value1 + key2: value2 + alias: + <<: *default + key2: Overriding key2 + key3: value3 + useMyValue: + key: *myValue + ")); + var result = Deserializer.Deserialize>>(parser); + + var alias = result["alias"]; + alias.Should() + .Contain("key1", "value1", "key1 should be inherited from the backreferenced mapping") + .And.Contain("key2", "Overriding key2", "key2 should be overriden by the actual mapping") + .And.Contain("key3", "value3", "key3 is defined in the actual mapping"); + + result["useMyValue"].Should() + .Contain("key", "value1", "key should be copied"); + } + + [Fact] + public void ExampleFromSpecificationIsHandledCorrectly() + { + var parser = new MergingParser(Yaml.ParserForText(@" + obj: + - &CENTER { x: 1, y: 2 } + - &LEFT { x: 0, y: 2 } + - &BIG { r: 10 } + - &SMALL { r: 1 } + + # All the following maps are equal: + results: + - # Explicit keys + x: 1 + y: 2 + r: 10 + label: center/big + + - # Merge one map + << : *CENTER + r: 10 + label: center/big + + - # Merge multiple maps + << : [ *CENTER, *BIG ] + label: center/big + + - # Override + << : [ *BIG, *LEFT, *SMALL ] + x: 1 + label: center/big + ")); + + var result = Deserializer.Deserialize>>>(parser); + + var index = 0; + foreach (var mapping in result["results"]) + { + mapping.Should() + .Contain("x", "1", "'x' should be '1' in result #{0}", index) + .And.Contain("y", "2", "'y' should be '2' in result #{0}", index) + .And.Contain("r", "10", "'r' should be '10' in result #{0}", index) + .And.Contain("label", "center/big", "'label' should be 'center/big' in result #{0}", index); + + ++index; + } + } + + [Fact] + public void MergeNestedReferenceCorrectly() + { + var parser = new MergingParser(Yaml.ParserForText(@" + base1: &level1 + key: X + level: 1 + base2: &level2 + <<: *level1 + key: Y + level: 2 + derived1: + <<: *level1 + key: D1 + derived2: + <<: *level2 + key: D2 + derived3: + <<: [ *level1, *level2 ] + key: D3 + ")); + + var result = Deserializer.Deserialize>>(parser); + + result["derived1"].Should() + .Contain("key", "D1", "key should be overriden by the actual mapping") + .And.Contain("level", "1", "level should be inherited from the backreferenced mapping"); + + result["derived2"].Should() + .Contain("key", "D2", "key should be overriden by the actual mapping") + .And.Contain("level", "2", "level should be inherited from the backreferenced mapping"); + + result["derived3"].Should() + .Contain("key", "D3", "key should be overriden by the actual mapping") + .And.Contain("level", "1", "level should be inherited from the backreferenced mapping"); + } + + [Fact] + public void IgnoreExtraPropertiesIfWanted() + { + var text = Lines("aaa: hello", "bbb: world"); + DeserializerBuilder.IgnoreUnmatchedProperties(); + var actual = Deserializer.Deserialize(UsingReaderFor(text)); + actual.aaa.Should().Be("hello"); + } + + [Fact] + public void DontIgnoreExtraPropertiesIfWanted() + { + var text = Lines("aaa: hello", "bbb: world"); + var actual = Record.Exception(() => Deserializer.Deserialize(UsingReaderFor(text))); + Assert.IsType(actual); + ((YamlException)actual).Start.Column.Should().Be(1); + ((YamlException)actual).Start.Line.Should().Be(2); + ((YamlException)actual).Start.Index.Should().Be(12); + ((YamlException)actual).End.Column.Should().Be(4); + ((YamlException)actual).End.Line.Should().Be(2); + ((YamlException)actual).End.Index.Should().Be(15); + ((YamlException)actual).Message.Should().Be("Property 'bbb' not found on type 'YamlDotNet.Test.Serialization.Simple'."); + } + + [Fact] + public void IgnoreExtraPropertiesIfWantedBefore() + { + var text = Lines("bbb: [200,100]", "aaa: hello"); + DeserializerBuilder.IgnoreUnmatchedProperties(); + var actual = Deserializer.Deserialize(UsingReaderFor(text)); + actual.aaa.Should().Be("hello"); + } + + [Fact] + public void IgnoreExtraPropertiesIfWantedNamingScheme() + { + var text = Lines( + "scratch: 'scratcher'", + "deleteScratch: false", + "notScratch: 9443", + "notScratch: 192.168.1.30", + "mappedScratch:", + "- '/work/'" + ); + + DeserializerBuilder + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .IgnoreUnmatchedProperties(); + + var actual = Deserializer.Deserialize(UsingReaderFor(text)); + actual.Scratch.Should().Be("scratcher"); + actual.DeleteScratch.Should().Be(false); + actual.MappedScratch.Should().ContainInOrder(new[] { "/work/" }); + } + + [Fact] + public void InvalidTypeConversionsProduceProperExceptions() + { + var text = Lines("- 1", "- two", "- 3"); + + var sut = new Deserializer(); + var exception = Assert.Throws(() => sut.Deserialize>(UsingReaderFor(text))); + + Assert.Equal(2, exception.Start.Line); + Assert.Equal(3, exception.Start.Column); + } + + [Theory] + [InlineData("blah")] + [InlineData("hello=world")] + [InlineData("+190:20:30")] + [InlineData("x:y")] + public void ValueAllowedAfterDocumentStartToken(string text) + { + var value = Lines("--- " + text); + + var sut = new Deserializer(); + var actual = sut.Deserialize(UsingReaderFor(value)); + + Assert.Equal(text, actual); + } + + [Fact] + public void MappingDisallowedAfterDocumentStartToken() + { + var value = Lines("--- x: y"); + + var sut = new Deserializer(); + var exception = Assert.Throws(() => sut.Deserialize(UsingReaderFor(value))); + + Assert.Equal(1, exception.Start.Line); + Assert.Equal(6, exception.Start.Column); + } + + [Fact] + public void SerializeDynamicPropertyAndApplyNamingConvention() + { + dynamic obj = new ExpandoObject(); + obj.property_one = new ExpandoObject(); + ((IDictionary)obj.property_one).Add("new_key_here", "new_value"); + + var mockNamingConvention = A.Fake(); + A.CallTo(() => mockNamingConvention.Apply(A.Ignored)).Returns("xxx"); + + var serializer = new SerializerBuilder() + .WithNamingConvention(mockNamingConvention) + .Build(); + + var writer = new StringWriter(); + serializer.Serialize(writer, obj); + + writer.ToString().Should().Contain("xxx: new_value"); + } + + [Fact] + public void SerializeGenericDictionaryPropertyAndDoNotApplyNamingConvention() + { + var obj = new Dictionary + { + ["property_one"] = new GenericTestDictionary() + }; + + ((IDictionary)obj["property_one"]).Add("new_key_here", "new_value"); + + var mockNamingConvention = A.Fake(); + A.CallTo(() => mockNamingConvention.Apply(A.Ignored)).Returns("xxx"); + + var serializer = new SerializerBuilder() + .WithNamingConvention(mockNamingConvention) + .Build(); + + var writer = new StringWriter(); + serializer.Serialize(writer, obj); + + writer.ToString().Should().Contain("new_key_here: new_value"); + } + + [Theory, MemberData(nameof(SpecialFloats))] + public void SpecialFloatsAreHandledCorrectly(FloatTestCase testCase) + { + var buffer = new StringWriter(); + Serializer.Serialize(buffer, testCase.Value); + + var firstLine = buffer.ToString().Split('\r', '\n')[0]; + Assert.Equal(testCase.ExpectedTextRepresentation, firstLine); + + var deserializer = new Deserializer(); + var deserializedValue = deserializer.Deserialize(new StringReader(buffer.ToString()), testCase.Value.GetType()); + + Assert.Equal(testCase.Value, deserializedValue); + } + + [Theory] + [InlineData(TestEnum.True)] + [InlineData(TestEnum.False)] + [InlineData(TestEnum.ABC)] + [InlineData(TestEnum.Null)] + public void RoundTripSpecialEnum(object testValue) + { + var test = new TestEnumTestCase { TestEnum = (TestEnum)testValue }; + var serializer = new SerializerBuilder().WithQuotingNecessaryStrings().Build(); + var deserializer = new DeserializerBuilder().Build(); + var serialized = serializer.Serialize(test); + var actual = deserializer.Deserialize(serialized); + Assert.Equal(testValue, actual.TestEnum); + } + + [Fact] + public void EmptyStringsAreQuoted() + { + var serializer = new SerializerBuilder().WithQuotingNecessaryStrings().Build(); + var o = new { test = string.Empty }; + var result = serializer.Serialize(o); + var expected = $"test: \"\"{Environment.NewLine}"; + Assert.Equal(expected, result); + } + +#if NET6_0_OR_GREATER + [Fact] + public void EnumSerializationUsesEnumMemberAttribute() + { + var serializer = new SerializerBuilder().Build(); + var actual = serializer.Serialize(EnumMemberedEnum.Hello); + Assert.Equal("goodbye", actual.TrimNewLines()); + } + + public enum EnumMemberedEnum + { + [System.Runtime.Serialization.EnumMember(Value = "goodbye")] + Hello = 1 + } +#endif + + public enum TestEnum + { + True, + False, + ABC, + Null + } + + public class TestEnumTestCase + { + public TestEnum TestEnum { get; set; } + } + + public class FloatTestCase + { + private readonly string description; + public object Value { get; private set; } + public string ExpectedTextRepresentation { get; private set; } + + public FloatTestCase(string description, object value, string expectedTextRepresentation) + { + this.description = description; + Value = value; + ExpectedTextRepresentation = expectedTextRepresentation; + } + + public override string ToString() + { + return description; + } + } + + public static IEnumerable SpecialFloats + { + get + { + return + new[] + { + new FloatTestCase("double.NaN", double.NaN, ".nan"), + new FloatTestCase("double.PositiveInfinity", double.PositiveInfinity, ".inf"), + new FloatTestCase("double.NegativeInfinity", double.NegativeInfinity, "-.inf"), + new FloatTestCase("double.Epsilon", double.Epsilon, double.Epsilon.ToString("G", CultureInfo.InvariantCulture)), + new FloatTestCase("double.26.67", 26.67D, "26.67"), + + new FloatTestCase("float.NaN", float.NaN, ".nan"), + new FloatTestCase("float.PositiveInfinity", float.PositiveInfinity, ".inf"), + new FloatTestCase("float.NegativeInfinity", float.NegativeInfinity, "-.inf"), + new FloatTestCase("float.Epsilon", float.Epsilon, float.Epsilon.ToString("G", CultureInfo.InvariantCulture)), + new FloatTestCase("float.26.67", 26.67F, "26.67"), + +#if NET + new FloatTestCase("double.MinValue", double.MinValue, double.MinValue.ToString("G", CultureInfo.InvariantCulture)), + new FloatTestCase("double.MaxValue", double.MaxValue, double.MaxValue.ToString("G", CultureInfo.InvariantCulture)), + new FloatTestCase("float.MinValue", float.MinValue, float.MinValue.ToString("G", CultureInfo.InvariantCulture)), + new FloatTestCase("float.MaxValue", float.MaxValue, float.MaxValue.ToString("G", CultureInfo.InvariantCulture)), +#endif + } + .Select(tc => new object[] { tc }); + } + } + + [Fact] + public void NegativeIntegersCanBeDeserialized() + { + var deserializer = new Deserializer(); + + var value = deserializer.Deserialize(Yaml.ReaderForText(@" + '-123' + ")); + Assert.Equal(-123, value); + } + + [Fact] + public void GenericDictionaryThatDoesNotImplementIDictionaryCanBeDeserialized() + { + var sut = new Deserializer(); + var deserialized = sut.Deserialize>(Yaml.ReaderForText(@" + a: 1 + b: 2 + ")); + + Assert.Equal("1", deserialized["a"]); + Assert.Equal("2", deserialized["b"]); + } + + [Fact] + public void GenericListThatDoesNotImplementIListCanBeDeserialized() + { + var sut = new Deserializer(); + var deserialized = sut.Deserialize>(Yaml.ReaderForText(@" + - a + - b + ")); + + Assert.Contains("a", deserialized); + Assert.Contains("b", deserialized); + } + + [Fact] + public void GuidsShouldBeQuotedWhenSerializedAsJson() + { + var sut = new SerializerBuilder() + .JsonCompatible() + .Build(); + + var yamlAsJson = new StringWriter(); + sut.Serialize(yamlAsJson, new + { + id = Guid.Empty + }); + + Assert.Contains("\"00000000-0000-0000-0000-000000000000\"", yamlAsJson.ToString()); + } + + public class Foo + { + public bool IsRequired { get; set; } + } + + [Fact] + public void AttributeOverridesAndNamingConventionDoNotConflict() + { + var namingConvention = CamelCaseNamingConvention.Instance; + + var yamlMember = new YamlMemberAttribute + { + Alias = "Required" + }; + + var serializer = new SerializerBuilder() + .WithNamingConvention(namingConvention) + .WithAttributeOverride(f => f.IsRequired, yamlMember) + .Build(); + + var yaml = serializer.Serialize(new Foo { IsRequired = true }); + Assert.Contains("required: true", yaml); + + var deserializer = new DeserializerBuilder() + .WithNamingConvention(namingConvention) + .WithAttributeOverride(f => f.IsRequired, yamlMember) + .Build(); + + var deserializedFoo = deserializer.Deserialize(yaml); + Assert.True(deserializedFoo.IsRequired); + } + + [Fact] + public void YamlConvertiblesAreAbleToEmitAndParseComments() + { + var serializer = new Serializer(); + var yaml = serializer.Serialize(new CommentWrapper { Comment = "A comment", Value = "The value" }); + + var deserializer = new Deserializer(); + var parser = new Parser(new Scanner(new StringReader(yaml), skipComments: false)); + var parsed = deserializer.Deserialize>(parser); + + Assert.Equal("A comment", parsed.Comment); + Assert.Equal("The value", parsed.Value); + } + + public class CommentWrapper : IYamlConvertible + { + public string Comment { get; set; } + public T Value { get; set; } + + public void Read(IParser parser, Type expectedType, ObjectDeserializer nestedObjectDeserializer) + { + if (parser.TryConsume(out var comment)) + { + Comment = comment.Value; + } + + Value = (T)nestedObjectDeserializer(typeof(T)); + } + + public void Write(IEmitter emitter, ObjectSerializer nestedObjectSerializer) + { + if (!string.IsNullOrEmpty(Comment)) + { + emitter.Emit(new Comment(Comment, false)); + } + + nestedObjectSerializer(Value, typeof(T)); + } + } + + [Theory] + [InlineData(uint.MinValue)] + [InlineData(uint.MaxValue)] + [InlineData(0x8000000000000000UL)] + public void DeserializationOfUInt64Succeeds(ulong value) + { + var yaml = new Serializer().Serialize(value); + Assert.Contains(value.ToString(), yaml); + + var parsed = new Deserializer().Deserialize(yaml); + Assert.Equal(value, parsed); + } + + [Theory] + [InlineData(int.MinValue)] + [InlineData(int.MaxValue)] + [InlineData(0L)] + public void DeserializationOfInt64Succeeds(long value) + { + var yaml = new Serializer().Serialize(value); + Assert.Contains(value.ToString(), yaml); + + var parsed = new Deserializer().Deserialize(yaml); + Assert.Equal(value, parsed); + } + + public class AnchorsOverwritingTestCase + { + public List a { get; set; } + public List b { get; set; } + public List c { get; set; } + public List d { get; set; } + } + + [Fact] + public void DeserializationOfStreamWithDuplicateAnchorsSucceeds() + { + var yaml = Yaml.ParserForResource("anchors-overwriting.yaml"); + var serializer = new DeserializerBuilder() + .IgnoreUnmatchedProperties() + .Build(); + var deserialized = serializer.Deserialize(yaml); + Assert.NotNull(deserialized); + } + + private sealed class AnchorPrecedence + { + internal sealed class AnchorPrecedenceNested + { + public string b1 { get; set; } + public Dictionary b2 { get; set; } + } + + public string a { get; set; } + public AnchorPrecedenceNested b { get; set; } + public string c { get; set; } + } + + [Fact] + public void DeserializationWithDuplicateAnchorsSucceeds() + { + var sut = new Deserializer(); + var deserialized = sut.Deserialize(@" +a: &anchor1 test0 +b: + b1: &anchor1 test1 + b2: + b21: &anchor1 test2 +c: *anchor1"); + + Assert.Equal("test0", deserialized.a); + Assert.Equal("test1", deserialized.b.b1); + Assert.Contains("b21", deserialized.b.b2.Keys); + Assert.Equal("test2", deserialized.b.b2["b21"]); + Assert.Equal("test2", deserialized.c); + } + + [Fact] + public void SerializeExceptionWithStackTrace() + { + var ex = GetExceptionWithStackTrace(); + var serializer = new SerializerBuilder() + .WithTypeConverter(new MethodInfoConverter()) + .Build(); + var yaml = serializer.Serialize(ex); + Assert.Contains("GetExceptionWithStackTrace", yaml); + } + + private class MethodInfoConverter : IYamlTypeConverter + { + public bool Accepts(Type type) + { + return typeof(MethodInfo).IsAssignableFrom(type); + } + + public object ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) + { + throw new NotImplementedException(); + } + + public void WriteYaml(IEmitter emitter, object value, Type type, ObjectSerializer serializer) + { + var method = (MethodInfo)value; + emitter.Emit(new Scalar(string.Format("{0}.{1}", method.DeclaringType.FullName, method.Name))); + } + } + + static Exception GetExceptionWithStackTrace() + { + try + { + throw new ArgumentNullException("foo"); + } + catch (Exception ex) + { + return ex; + } + } + + [Fact] + public void RegisteringATypeConverterPreventsTheTypeFromBeingVisited() + { + var serializer = new SerializerBuilder() + .WithTypeConverter(new NonSerializableTypeConverter()) + .Build(); + + var yaml = serializer.Serialize(new NonSerializableContainer + { + Value = new NonSerializable { Text = "hello" }, + }); + + var deserializer = new DeserializerBuilder() + .WithTypeConverter(new NonSerializableTypeConverter()) + .Build(); + + var result = deserializer.Deserialize(yaml); + + Assert.Equal("hello", result.Value.Text); + } + + [Fact] + public void NamingConventionIsNotAppliedBySerializerWhenApplyNamingConventionsIsFalse() + { + var sut = new SerializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .Build(); + + var yaml = sut.Serialize(new NamingConventionDisabled { NoConvention = "value" }); + + Assert.Contains("NoConvention", yaml); + } + + [Fact] + public void NamingConventionIsNotAppliedByDeserializerWhenApplyNamingConventionsIsFalse() + { + var sut = new DeserializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .Build(); + + var yaml = "NoConvention: value"; + + var parsed = sut.Deserialize(yaml); + + Assert.Equal("value", parsed.NoConvention); + } + + [Fact] + public void TypesAreSerializable() + { + var sut = new SerializerBuilder() + .Build(); + + var yaml = sut.Serialize(typeof(string)); + + Assert.Contains(typeof(string).AssemblyQualifiedName, yaml); + } + + [Fact] + public void TypesAreDeserializable() + { + var sut = new DeserializerBuilder() + .Build(); + + var type = sut.Deserialize(typeof(string).AssemblyQualifiedName); + + Assert.Equal(typeof(string), type); + } + + [Fact] + public void TypesAreConvertedWhenNeededFromScalars() + { + var sut = new DeserializerBuilder() + .WithTagMapping("!dbl", typeof(DoublyConverted)) + .Build(); + + var result = sut.Deserialize("!dbl hello"); + + Assert.Equal(5, result); + } + + [Fact] + public void TypesAreConvertedWhenNeededInsideLists() + { + var sut = new DeserializerBuilder() + .WithTagMapping("!dbl", typeof(DoublyConverted)) + .Build(); + + var result = sut.Deserialize>("- !dbl hello"); + + Assert.Equal(5, result[0]); + } + + [Fact] + public void TypesAreConvertedWhenNeededInsideDictionary() + { + var sut = new DeserializerBuilder() + .WithTagMapping("!dbl", typeof(DoublyConverted)) + .Build(); + + var result = sut.Deserialize>("!dbl hello: !dbl you"); + + Assert.True(result.ContainsKey(5)); + Assert.Equal(3, result[5]); + } + + [Fact] + public void InfiniteRecursionIsDetected() + { + var sut = new SerializerBuilder() + .DisableAliases() + .Build(); + + var recursionRoot = new + { + Nested = new[] + { + new Dictionary() + } + }; + + recursionRoot.Nested[0].Add("loop", recursionRoot); + + var exception = Assert.Throws(() => sut.Serialize(recursionRoot)); + } + + [Fact] + public void TuplesAreSerializable() + { + var sut = new SerializerBuilder() + .Build(); + + var yaml = sut.Serialize(new[] + { + Tuple.Create(1, "one"), + Tuple.Create(2, "two"), + }); + + var expected = Yaml.Text(@" + - Item1: 1 + Item2: one + - Item1: 2 + Item2: two + "); + + Assert.Equal(expected.NormalizeNewLines(), yaml.NormalizeNewLines().TrimNewLines()); + } + + [Fact] + public void ValueTuplesAreSerializableWithoutMetadata() + { + var sut = new SerializerBuilder() + .Build(); + + var yaml = sut.Serialize(new[] + { + (num: 1, txt: "one"), + (num: 2, txt: "two"), + }); + + var expected = Yaml.Text(@" + - Item1: 1 + Item2: one + - Item1: 2 + Item2: two + "); + + Assert.Equal(expected.NormalizeNewLines(), yaml.NormalizeNewLines().TrimNewLines()); + } + + [Fact] + public void AnchorNameWithTrailingColonReferencedInKeyCanBeDeserialized() + { + var sut = new Deserializer(); + var deserialized = sut.Deserialize>(Yaml.ReaderForText(@" + a: &::::scaryanchor:::: anchor "" value "" + *::::scaryanchor::::: 2 + myvalue: *::::scaryanchor:::: + ")); + + Assert.Equal(@"anchor "" value """, deserialized["a"]); + Assert.Equal("2", deserialized[@"anchor "" value """]); + Assert.Equal(@"anchor "" value """, deserialized["myvalue"]); + } + + [Fact] + public void AliasBeforeAnchorCannotBeDeserialized() + { + var sut = new Deserializer(); + Action action = () => sut.Deserialize>(@" +a: *anchor1 +b: &anchor1 test0 +c: *anchor1"); + + action.ShouldThrow(); + } + + [Fact] + public void AnchorWithAllowedCharactersCanBeDeserialized() + { + var sut = new Deserializer(); + var deserialized = sut.Deserialize>(Yaml.ReaderForText(@" + a: &@nchor<>""@-_123$>>>😁🎉🐻🍔end some value + myvalue: my *@nchor<>""@-_123$>>>😁🎉🐻🍔end test + interpolated value: *@nchor<>""@-_123$>>>😁🎉🐻🍔end + ")); + + Assert.Equal("some value", deserialized["a"]); + Assert.Equal(@"my *@nchor<>""@-_123$>>>😁🎉🐻🍔end test", deserialized["myvalue"]); + Assert.Equal("some value", deserialized["interpolated value"]); + } + + [Fact] + public void SerializationNonPublicPropertiesAreIgnored() + { + var sut = new SerializerBuilder().Build(); + var yaml = sut.Serialize(new NonPublicPropertiesExample()); + Assert.Equal("Public: public", yaml.TrimNewLines()); + } + + [Fact] + public void SerializationNonPublicPropertiesAreIncluded() + { + var sut = new SerializerBuilder().IncludeNonPublicProperties().Build(); + var yaml = sut.Serialize(new NonPublicPropertiesExample()); + + var expected = Yaml.Text(@" + Public: public + Internal: internal + Protected: protected + Private: private + "); + + Assert.Equal(expected.NormalizeNewLines(), yaml.NormalizeNewLines().TrimNewLines()); + } + + [Fact] + public void DeserializationNonPublicPropertiesAreIgnored() + { + var sut = new DeserializerBuilder().IgnoreUnmatchedProperties().Build(); + var deserialized = sut.Deserialize(Yaml.ReaderForText(@" + Public: public2 + Internal: internal2 + Protected: protected2 + Private: private2 + ")); + + Assert.Equal("public2,internal,protected,private", deserialized.ToString()); + } + + [Fact] + public void DeserializationNonPublicPropertiesAreIncluded() + { + var sut = new DeserializerBuilder().IncludeNonPublicProperties().Build(); + var deserialized = sut.Deserialize(Yaml.ReaderForText(@" + Public: public2 + Internal: internal2 + Protected: protected2 + Private: private2 + ")); + + Assert.Equal("public2,internal2,protected2,private2", deserialized.ToString()); + } + + [Fact] + public void SerializationNonPublicFieldsAreIgnored() + { + var sut = new SerializerBuilder().Build(); + var yaml = sut.Serialize(new NonPublicFieldsExample()); + Assert.Equal("Public: public", yaml.TrimNewLines()); + } + + [Fact] + public void DeserializationNonPublicFieldsAreIgnored() + { + var sut = new DeserializerBuilder().IgnoreUnmatchedProperties().Build(); + var deserialized = sut.Deserialize(Yaml.ReaderForText(@" + Public: public2 + Internal: internal2 + Protected: protected2 + Private: private2 + ")); + + Assert.Equal("public2,internal,protected,private", deserialized.ToString()); + } + + [Fact] + public void ShouldNotIndentSequences() + { + var sut = new SerializerBuilder() + .Build(); + + var yaml = sut.Serialize(new + { + first = "first", + items = new[] + { + "item1", + "item2" + }, + nested = new[] + { + new + { + name = "name1", + more = new[] + { + "nested1", + "nested2" + } + } + } + }); + + var expected = Yaml.Text(@" + first: first + items: + - item1 + - item2 + nested: + - name: name1 + more: + - nested1 + - nested2 + "); + + Assert.Equal(expected.NormalizeNewLines(), yaml.NormalizeNewLines().TrimNewLines()); + } + + [Fact] + public void ShouldIndentSequences() + { + var sut = new SerializerBuilder() + .WithIndentedSequences() + .Build(); + + var yaml = sut.Serialize(new + { + first = "first", + items = new[] + { + "item1", + "item2" + }, + nested = new[] + { + new + { + name = "name1", + more = new[] + { + "nested1", + "nested2" + } + } + } + }); + + var expected = Yaml.Text(@" + first: first + items: + - item1 + - item2 + nested: + - name: name1 + more: + - nested1 + - nested2 + "); + + Assert.Equal(expected.NormalizeNewLines(), yaml.NormalizeNewLines().TrimNewLines()); + } + + [Fact] + public void ExampleFromSpecificationIsHandledCorrectlyWithLateDefine() + { + var parser = new MergingParser(Yaml.ParserForText(@" + # All the following maps are equal: + results: + - # Explicit keys + x: 1 + y: 2 + r: 10 + label: center/big + + - # Merge one map + << : *CENTER + r: 10 + label: center/big + + - # Merge multiple maps + << : [ *CENTER, *BIG ] + label: center/big + + - # Override + << : [ *BIG, *LEFT, *SMALL ] + x: 1 + label: center/big + + obj: + - &CENTER { x: 1, y: 2 } + - &LEFT { x: 0, y: 2 } + - &SMALL { r: 1 } + - &BIG { r: 10 } + ")); + + var result = Deserializer.Deserialize>>>(parser); + + int index = 0; + foreach (var mapping in result["results"]) + { + mapping.Should() + .Contain("x", "1", "'x' should be '1' in result #{0}", index) + .And.Contain("y", "2", "'y' should be '2' in result #{0}", index) + .And.Contain("r", "10", "'r' should be '10' in result #{0}", index) + .And.Contain("label", "center/big", "'label' should be 'center/big' in result #{0}", index); + + ++index; + } + } + + public class CycleTestEntity + { + public CycleTestEntity Cycle { get; set; } + } + + [Fact] + public void SerializeCycleWithAlias() + { + var sut = new SerializerBuilder() + .WithTagMapping("!CycleTag", typeof(CycleTestEntity)) + .Build(); + + var entity = new CycleTestEntity(); + entity.Cycle = entity; + var yaml = sut.Serialize(entity); + var expected = Yaml.Text(@"&o0 !CycleTag +Cycle: *o0"); + + Assert.Equal(expected.NormalizeNewLines(), yaml.NormalizeNewLines().TrimNewLines()); + } + + [Fact] + public void DeserializeCycleWithAlias() + { + var sut = new DeserializerBuilder() + .WithTagMapping("!CycleTag", typeof(CycleTestEntity)) + .Build(); + + var yaml = Yaml.Text(@"&o0 !CycleTag +Cycle: *o0"); + var obj = sut.Deserialize(yaml); + + Assert.Same(obj, obj.Cycle); + } + + [Fact] + public void DeserializeCycleWithoutAlias() + { + var sut = new DeserializerBuilder() + .Build(); + + var yaml = Yaml.Text(@"&o0 +Cycle: *o0"); + var obj = sut.Deserialize(yaml); + + Assert.Same(obj, obj.Cycle); + } + + public static IEnumerable Depths => Enumerable.Range(1, 10).Select(i => new[] { (object)i }); + + [Theory] + [MemberData(nameof(Depths))] + public void DeserializeCycleWithAnchorsWithDepth(int? depth) + { + var sut = new DeserializerBuilder() + .WithTagMapping("!CycleTag", typeof(CycleTestEntity)) + .Build(); + + StringBuilder builder = new StringBuilder(@"&o0 !CycleTag"); + builder.AppendLine(); + string indentation; + for (int i = 0; i < depth - 1; ++i) + { + indentation = string.Concat(Enumerable.Repeat(" ", i)); + builder.AppendLine($"{indentation}Cycle: !CycleTag"); + } + indentation = string.Concat(Enumerable.Repeat(" ", depth.Value - 1)); + builder.AppendLine($"{indentation}Cycle: *o0"); + var yaml = Yaml.Text(builder.ToString()); + var obj = sut.Deserialize(yaml); + CycleTestEntity iterator = obj; + for (int i = 0; i < depth; ++i) + { + iterator = iterator.Cycle; + } + Assert.Same(obj, iterator); + } + + [Fact] + public void RoundtripWindowsNewlines() + { + var text = $"Line1{Environment.NewLine}Line2{Environment.NewLine}Line3{Environment.NewLine}{Environment.NewLine}Line4"; + + var sut = new SerializerBuilder().Build(); + var dut = new DeserializerBuilder().Build(); + + using var writer = new StringWriter { NewLine = Environment.NewLine }; + sut.Serialize(writer, new StringContainer { Text = text }); + var serialized = writer.ToString(); + + using var reader = new StringReader(serialized); + var roundtrippedText = dut.Deserialize(reader).Text.NormalizeNewLines(); + Assert.Equal(text, roundtrippedText); + } + + [Theory] + [InlineData("NULL")] + [InlineData("Null")] + [InlineData("null")] + [InlineData("~")] + [InlineData("true")] + [InlineData("false")] + [InlineData("True")] + [InlineData("False")] + [InlineData("TRUE")] + [InlineData("FALSE")] + [InlineData("0o77")] + [InlineData("0x7A")] + [InlineData("+1e10")] + [InlineData("1E10")] + [InlineData("+.inf")] + [InlineData("-.inf")] + [InlineData(".inf")] + [InlineData(".nan")] + [InlineData(".NaN")] + [InlineData(".NAN")] + public void StringsThatMatchKeywordsAreQuoted(string input) + { + var serializer = new SerializerBuilder().WithQuotingNecessaryStrings().Build(); + var o = new { text = input }; + var yaml = serializer.Serialize(o); + Assert.Equal($"text: \"{input}\"{Environment.NewLine}", yaml); + } + + public static IEnumerable Yaml1_1SpecialStringsData = new[] + { + "-.inf", "-.Inf", "-.INF", "-0", "-0100_200", "-0b101", "-0x30", "-190:20:30", "-23", "-3.14", + "._", "._14", ".", ".0", ".1_4", ".14", ".3E-1", ".3e+3", ".inf", ".Inf", + ".INF", ".nan", ".NaN", ".NAN", "+.inf", "+.Inf", "+.INF", "+0.3e+3", "+0", + "+0100_200", "+0b100", "+190:20:30", "+23", "+3.14", "~", "0.0", "0", "00", "001.23", + "0011", "010", "02_0", "07", "0b0", "0b100_101", "0o0", "0o10", "0o7", "0x0", + "0x10", "0x2_0", "0x42", "0xa", "100_000", "190:20:30.15", "190:20:30", "23", "3.", "3.14", "3.3e+3", + "85_230.15", "85.230_15e+03", "false", "False", "FALSE", "n", "N", "no", "No", "NO", + "null", "Null", "NULL", "off", "Off", "OFF", "on", "On", "ON", "true", "True", "TRUE", + "y", "Y", "yes", "Yes", "YES" + }.Select(v => new object[] { v }).ToList(); + + [Theory] + [MemberData(nameof(Yaml1_1SpecialStringsData))] + public void StringsThatMatchYaml1_1KeywordsAreQuoted(string input) + { + var serializer = new SerializerBuilder().WithQuotingNecessaryStrings(true).Build(); + var o = new { text = input }; + var yaml = serializer.Serialize(o); + Assert.Equal($"text: \"{input}\"{Environment.NewLine}", yaml); + } + + [Fact] + public void KeysOnConcreteClassDontGetQuoted_TypeStringGetsQuoted() + { + var serializer = new SerializerBuilder().WithQuotingNecessaryStrings().Build(); + var deserializer = new DeserializerBuilder().WithAttemptingUnquotedStringTypeDeserialization().Build(); + var yaml = @" +True: null +False: hello +Null: true +"; + var obj = deserializer.Deserialize>(yaml); + var result = serializer.Serialize(obj); + obj.True.Should().BeNull(); + obj.False.Should().Be("hello"); + obj.Null.Should().Be("true"); + result.Should().Be($"True: {Environment.NewLine}False: hello{Environment.NewLine}Null: \"true\"{Environment.NewLine}"); + } + + [Fact] + public void KeysOnConcreteClassDontGetQuoted_TypeBoolDoesNotGetQuoted() + { + var serializer = new SerializerBuilder().WithQuotingNecessaryStrings().Build(); + var deserializer = new DeserializerBuilder().WithAttemptingUnquotedStringTypeDeserialization().Build(); + var yaml = @" +True: null +False: hello +Null: true +"; + var obj = deserializer.Deserialize>(yaml); + var result = serializer.Serialize(obj); + obj.True.Should().BeNull(); + obj.False.Should().Be("hello"); + obj.Null.Should().BeTrue(); + result.Should().Be($"True: {Environment.NewLine}False: hello{Environment.NewLine}Null: true{Environment.NewLine}"); + } + + [Fact] + public void SerializeStateMethodsGetCalledOnce() + { + var serializer = new SerializerBuilder().Build(); + var test = new TestState(); + serializer.Serialize(test); + + Assert.Equal(1, test.OnSerializedCallCount); + Assert.Equal(1, test.OnSerializingCallCount); + } + + [Fact] + public void SerializeEnumAsNumber() + { + var serializer = new SerializerBuilder().WithYamlFormatter(new YamlFormatter + { + FormatEnum = (o, typeInspector, namingConvention) => ((int)o).ToString(), + PotentiallyQuoteEnums = (_) => false + }).Build(); + var deserializer = DeserializerBuilder.Build(); + + var value = serializer.Serialize(TestEnumAsNumber.Test1); + Assert.Equal("1", value.TrimNewLines()); + var v = deserializer.Deserialize(value); + Assert.Equal(TestEnumAsNumber.Test1, v); + + value = serializer.Serialize(TestEnumAsNumber.Test1 | TestEnumAsNumber.Test2); + Assert.Equal("3", value.TrimNewLines()); + v = deserializer.Deserialize(value); + Assert.Equal(TestEnumAsNumber.Test1 | TestEnumAsNumber.Test2, v); + } + + [Fact] + public void TabsGetQuotedWhenQuoteNecessaryStringsIsOn() + { + var serializer = new SerializerBuilder() + .WithQuotingNecessaryStrings() + .Build(); + + var s = "\t, something"; + var yaml = serializer.Serialize(s); + var deserializer = new DeserializerBuilder().Build(); + var value = deserializer.Deserialize(yaml); + Assert.Equal(s, value); + } + + [Fact] + public void SpacesGetQuotedWhenQuoteNecessaryStringsIsOn() + { + var serializer = new SerializerBuilder() + .WithQuotingNecessaryStrings() + .Build(); + + var s = " , something"; + var yaml = serializer.Serialize(s); + var deserializer = new DeserializerBuilder().Build(); + var value = deserializer.Deserialize(yaml); + Assert.Equal(s, value); + } + + [Flags] + private enum TestEnumAsNumber + { + Test1 = 1, + Test2 = 2 + } + + [Fact] + public void NamingConventionAppliedToEnum() + { + var serializer = new SerializerBuilder().WithEnumNamingConvention(CamelCaseNamingConvention.Instance).Build(); + ScalarStyle style = ScalarStyle.Plain; + var serialized = serializer.Serialize(style); + Assert.Equal("plain", serialized.RemoveNewLines()); + } + + [Fact] + public void NamingConventionAppliedToEnumWhenDeserializing() + { + var serializer = new DeserializerBuilder().WithEnumNamingConvention(UnderscoredNamingConvention.Instance).Build(); + var yaml = "Double_Quoted"; + ScalarStyle expected = ScalarStyle.DoubleQuoted; + var actual = serializer.Deserialize(yaml); + Assert.Equal(expected, actual); + } + + [Fact] + [Trait("motive", "issue #656")] + public void NestedDictionaryTypes_ShouldRoundtrip() + { + var serializer = new SerializerBuilder().EnsureRoundtrip().Build(); + var yaml = serializer.Serialize(new HasNestedDictionary { Lookups = { [1] = new HasNestedDictionary.Payload { I = 1 } } }, typeof(HasNestedDictionary)); + var dct = new DeserializerBuilder().Build().Deserialize(yaml); + Assert.Contains(new KeyValuePair(1, new HasNestedDictionary.Payload { I = 1 }), dct.Lookups); + } + + public class TestState + { + public int OnSerializedCallCount { get; set; } + public int OnSerializingCallCount { get; set; } + + public string Test { get; set; } = string.Empty; + + [OnSerialized] + public void Serialized() => OnSerializedCallCount++; + + [OnSerializing] + public void Serializing() => OnSerializingCallCount++; + } + + public class ReservedWordsTestClass + { + public string True { get; set; } + public string False { get; set; } + public TNullType Null { get; set; } + } + + [TypeConverter(typeof(DoublyConvertedTypeConverter))] + public class DoublyConverted + { + public string Value { get; set; } + } + + public class DoublyConvertedTypeConverter : TypeConverter + { + public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) + { + return destinationType == typeof(int); + } + + public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) + { + return ((DoublyConverted)value).Value.Length; + } + + public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + { + return sourceType == typeof(string); + } + + public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + { + return new DoublyConverted { Value = (string)value }; + } + } + + public class NamingConventionDisabled + { + [YamlMember(ApplyNamingConventions = false)] + public string NoConvention { get; set; } + } + + public class NonSerializableContainer + { + public NonSerializable Value { get; set; } + } + + public class NonSerializable + { + public string WillThrow { get { throw new Exception(); } } + + public string Text { get; set; } + } + + public class StringContainer + { + public string Text { get; set; } + } + + public class NonSerializableTypeConverter : IYamlTypeConverter + { + public bool Accepts(Type type) + { + return typeof(NonSerializable).IsAssignableFrom(type); + } + + public object ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) + { + var scalar = parser.Consume(); + return new NonSerializable { Text = scalar.Value }; + } + + public void WriteYaml(IEmitter emitter, object value, Type type, ObjectSerializer serializer) + { + emitter.Emit(new Scalar(((NonSerializable)value).Text)); + } + } + + public sealed class HasNestedDictionary + { + public Dictionary Lookups { get; set; } = new Dictionary(); + + public struct Payload + { + public int I { get; set; } + } + } + } +} diff --git a/YamlDotNet.Test/Serialization/TimeOnlyConverterTests.cs b/YamlDotNet.Test/Serialization/TimeOnlyConverterTests.cs index 7d1be6358..947cd1378 100644 --- a/YamlDotNet.Test/Serialization/TimeOnlyConverterTests.cs +++ b/YamlDotNet.Test/Serialization/TimeOnlyConverterTests.cs @@ -73,7 +73,7 @@ public void Given_Yaml_WithInvalidDateTimeFormat_WithDefaultParameters_ReadYaml_ var converter = new TimeOnlyConverter(); - Action action = () => { converter.ReadYaml(parser, typeof(TimeOnly)); }; + Action action = () => { converter.ReadYaml(parser, typeof(TimeOnly), null); }; action.ShouldThrow(); } @@ -96,7 +96,7 @@ public void Given_Yaml_WithValidDateTimeFormat_WithDefaultParameters_ReadYaml_Sh var converter = new TimeOnlyConverter(); - var result = converter.ReadYaml(parser, typeof(TimeOnly)); + var result = converter.ReadYaml(parser, typeof(TimeOnly), null); result.Should().BeOfType(); ((TimeOnly)result).Hour.Should().Be(hour); @@ -123,7 +123,7 @@ public void Given_Yaml_WithValidDateTimeFormat_ReadYaml_ShouldReturn_Result(int var converter = new TimeOnlyConverter(formats: new[] { format1, format2 }); - var result = converter.ReadYaml(parser, typeof(TimeOnly)); + var result = converter.ReadYaml(parser, typeof(TimeOnly), null); result.Should().BeOfType(); ((TimeOnly)result).Hour.Should().Be(6); @@ -151,7 +151,7 @@ public void Given_Yaml_WithSpecificCultureAndValidDateTimeFormat_ReadYaml_Should var culture = new CultureInfo("ko-KR"); // Sample specific culture var converter = new TimeOnlyConverter(provider: culture, formats: new[] { format1, format2 }); - var result = converter.ReadYaml(parser, typeof(TimeOnly)); + var result = converter.ReadYaml(parser, typeof(TimeOnly), null); result.Should().BeOfType(); ((TimeOnly)result).Hour.Should().Be(6); @@ -179,7 +179,7 @@ public void Given_Yaml_WithTimeFormat_ReadYaml_ShouldReturn_Result(string format var parser = A.Fake(); A.CallTo(() => parser.Current).ReturnsLazily(() => new Scalar(value)); - var result = converter.ReadYaml(parser, typeof(TimeOnly)); + var result = converter.ReadYaml(parser, typeof(TimeOnly), null); result.Should().Be(expected); } @@ -223,7 +223,7 @@ public void Given_Yaml_WithLocaleAndTimeFormat_ReadYaml_ShouldReturn_Result(stri var parser = A.Fake(); A.CallTo(() => parser.Current).ReturnsLazily(() => new Scalar(value)); - var result = converter.ReadYaml(parser, typeof(TimeOnly)); + var result = converter.ReadYaml(parser, typeof(TimeOnly), null); result.Should().Be(expected); } diff --git a/YamlDotNet.Test/Serialization/TypeConverterAttributeTests.cs b/YamlDotNet.Test/Serialization/TypeConverterAttributeTests.cs new file mode 100644 index 000000000..583174b32 --- /dev/null +++ b/YamlDotNet.Test/Serialization/TypeConverterAttributeTests.cs @@ -0,0 +1,107 @@ +// This file is part of YamlDotNet - A .NET library for YAML. +// Copyright (c) Antoine Aubry and contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; +using YamlDotNet.Core; +using YamlDotNet.Core.Events; +using YamlDotNet.Serialization; + +namespace YamlDotNet.Test.Serialization +{ + public class TypeConverterAttributeTests + { + [Fact] + public void TestConverterOnAttribute_Deserializes() + { + var deserializer = new DeserializerBuilder().WithTypeConverter(new AttributedTypeConverter()).Build(); + var yaml = @"Value: + abc: def"; + var actual = deserializer.Deserialize(yaml); + Assert.Equal("abc", actual.Value.Key); + Assert.Equal("def", actual.Value.Value); + } + + [Fact] + public void TestConverterOnAttribute_Serializes() + { + var serializer = new SerializerBuilder().WithTypeConverter(new AttributedTypeConverter()).Build(); + var o = new OuterClass + { + Value = new ValueClass + { + Key = "abc", + Value = "def" + } + }; + var actual = serializer.Serialize(o).NormalizeNewLines().TrimNewLines(); + var expected = @"Value: + abc: def"; + Assert.Equal(expected, actual); + } + + public class AttributedTypeConverter : IYamlTypeConverter + { + public bool Accepts(Type type) => false; + + public object ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) + { + parser.Consume(); + var key = parser.Consume(); + var value = parser.Consume(); + parser.Consume(); + + var result = new ValueClass + { + Key = key.Value, + Value = value.Value + }; + return result; + } + + public void WriteYaml(IEmitter emitter, object value, Type type, ObjectSerializer serializer) + { + var v = (ValueClass)value; + + emitter.Emit(new MappingStart()); + emitter.Emit(new Scalar(v.Key)); + emitter.Emit(new Scalar(v.Value)); + emitter.Emit(new MappingEnd()); + } + } + + public class OuterClass + { + [YamlConverter(typeof(AttributedTypeConverter))] + public ValueClass Value { get; set; } + } + + public class ValueClass + { + public string Key { get; set; } + public string Value { get; set; } + } + } +} diff --git a/YamlDotNet.Test/Serialization/TypeConverterTests.cs b/YamlDotNet.Test/Serialization/TypeConverterTests.cs index b9dc93fa6..37e6eea17 100644 --- a/YamlDotNet.Test/Serialization/TypeConverterTests.cs +++ b/YamlDotNet.Test/Serialization/TypeConverterTests.cs @@ -21,6 +21,8 @@ using Xunit; using YamlDotNet.Serialization.NamingConventions; +using YamlDotNet.Serialization.TypeInspectors; +using YamlDotNet.Serialization.TypeResolvers; using YamlDotNet.Serialization.Utilities; namespace YamlDotNet.Test.Serialization @@ -61,7 +63,9 @@ public static explicit operator int(ExplicitConversionIntWrapper wrapper) public void Implicit_conversion_operator_is_used() { var data = new ImplicitConversionIntWrapper(2); - var actual = TypeConverter.ChangeType(data, NullNamingConvention.Instance); + var typeResolver = new DynamicTypeResolver(); + var typeInspector = new WritablePropertiesTypeInspector(typeResolver); + var actual = TypeConverter.ChangeType(data, NullNamingConvention.Instance, typeInspector); Assert.Equal(data.value, actual); } @@ -69,7 +73,9 @@ public void Implicit_conversion_operator_is_used() public void Explicit_conversion_operator_is_used() { var data = new ExplicitConversionIntWrapper(2); - var actual = TypeConverter.ChangeType(data, NullNamingConvention.Instance); + var typeResolver = new DynamicTypeResolver(); + var typeInspector = new WritablePropertiesTypeInspector(typeResolver); + var actual = TypeConverter.ChangeType(data, NullNamingConvention.Instance, typeInspector); Assert.Equal(data.value, actual); } } diff --git a/YamlDotNet.Test/YamlDotNet.Test.csproj b/YamlDotNet.Test/YamlDotNet.Test.csproj index 788ce8f0d..1ccafd6ea 100644 --- a/YamlDotNet.Test/YamlDotNet.Test.csproj +++ b/YamlDotNet.Test/YamlDotNet.Test.csproj @@ -1,10 +1,10 @@  - net8.0;net7.0;net6.0;net47 + net8.0;net6.0;net47 false ..\YamlDotNet.snk true - 8.0 + 11.0 true diff --git a/YamlDotNet/Core/Cursor.cs b/YamlDotNet/Core/Cursor.cs index 6c479e572..c2822b9cf 100644 --- a/YamlDotNet/Core/Cursor.cs +++ b/YamlDotNet/Core/Cursor.cs @@ -26,9 +26,9 @@ namespace YamlDotNet.Core [DebuggerStepThrough] public sealed class Cursor { - public int Index { get; private set; } - public int Line { get; private set; } - public int LineOffset { get; private set; } + public long Index { get; private set; } + public long Line { get; private set; } + public long LineOffset { get; private set; } public Cursor() { diff --git a/YamlDotNet/Core/Mark.cs b/YamlDotNet/Core/Mark.cs index 874924393..8375edc0f 100644 --- a/YamlDotNet/Core/Mark.cs +++ b/YamlDotNet/Core/Mark.cs @@ -37,19 +37,19 @@ namespace YamlDotNet.Core /// /// Gets / sets the absolute offset in the file /// - public int Index { get; } + public long Index { get; } /// /// Gets / sets the number of the line /// - public int Line { get; } + public long Line { get; } /// /// Gets / sets the index of the column /// - public int Column { get; } + public long Column { get; } - public Mark(int index, int line, int column) + public Mark(long index, long line, long column) { if (index < 0) { diff --git a/YamlDotNet/Core/Scanner.cs b/YamlDotNet/Core/Scanner.cs index 7e743dc05..0cb53b133 100644 --- a/YamlDotNet/Core/Scanner.cs +++ b/YamlDotNet/Core/Scanner.cs @@ -58,7 +58,7 @@ public class Scanner : IScanner { 'P', '\x2029' } }; - private readonly Stack indents = new Stack(); + private readonly Stack indents = new Stack(); private readonly InsertionQueue tokens = new InsertionQueue(); private readonly Stack simpleKeys = new Stack(); private readonly CharacterAnalyzer analyzer; @@ -67,10 +67,10 @@ public class Scanner : IScanner private bool streamStartProduced; private bool streamEndProduced; private bool plainScalarFollowedByComment; - private int flowSequenceStartLine; + private long flowSequenceStartLine; private bool flowCollectionFetched = false; private bool startFlowCollectionFetched = false; - private int indent = -1; + private long indent = -1; private bool flowScalarFetched; private bool simpleKeyAllowed; private int flowLevel; @@ -696,7 +696,7 @@ private void FetchStreamStart() /// the BLOCK-END token. /// - private void UnrollIndent(int column) + private void UnrollIndent(long column) { // In the flow context, do nothing. @@ -1220,7 +1220,7 @@ private void FetchValue() /// the current column is greater than the indentation level. In this case, /// append or insert the specified token into the token queue. /// - private void RollIndent(int column, int number, bool isSequence, Mark position) + private void RollIndent(long column, int number, bool isSequence, Mark position) { // In the flow context, do nothing. @@ -1492,7 +1492,7 @@ Token ScanBlockScalar(bool isLiteral) var chomping = 0; var increment = 0; - var currentIndent = 0; + var currentIndent = (long)0; var leadingBlank = false; bool? isFirstLine = null; @@ -1683,10 +1683,10 @@ Token ScanBlockScalar(bool isLiteral) /// indentation level if needed. /// - private int ScanBlockScalarBreaks(int currentIndent, StringBuilder breaks, bool isLiteral, ref Mark end, ref bool? isFirstLine) + private long ScanBlockScalarBreaks(long currentIndent, StringBuilder breaks, bool isLiteral, ref Mark end, ref bool? isFirstLine) { - var maxIndent = 0; - var indentOfFirstLine = -1; + var maxIndent = (long)0; + var indentOfFirstLine = (long)-1; end = cursor.Mark(); diff --git a/YamlDotNet/Core/SimpleKey.cs b/YamlDotNet/Core/SimpleKey.cs index f7f31ad6e..36d5a7bb9 100644 --- a/YamlDotNet/Core/SimpleKey.cs +++ b/YamlDotNet/Core/SimpleKey.cs @@ -34,9 +34,9 @@ public void MarkAsImpossible() public bool IsRequired { get; } public int TokenNumber { get; } - public int Index => cursor.Index; - public int Line => cursor.Line; - public int LineOffset => cursor.LineOffset; + public long Index => cursor.Index; + public long Line => cursor.Line; + public long LineOffset => cursor.LineOffset; public Mark Mark => cursor.Mark(); diff --git a/YamlDotNet/Helpers/DictionaryExtensions.cs b/YamlDotNet/Helpers/DictionaryExtensions.cs index 0e40b5f69..831bfee20 100644 --- a/YamlDotNet/Helpers/DictionaryExtensions.cs +++ b/YamlDotNet/Helpers/DictionaryExtensions.cs @@ -1,4 +1,4 @@ -// This file is part of YamlDotNet - A .NET library for YAML. +// This file is part of YamlDotNet - A .NET library for YAML. // Copyright (c) Antoine Aubry and contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy of @@ -24,16 +24,16 @@ namespace YamlDotNet.Helpers internal static class DictionaryExtensions { #if NETSTANDARD2_0 || NETFRAMEWORK - public static bool TryAdd(this System.Collections.Generic.Dictionary dictionary, T key, V value) - { - if (dictionary.ContainsKey(key)) + public static bool TryAdd(this System.Collections.Generic.Dictionary dictionary, T key, V value) { - return false; - } + if (dictionary.ContainsKey(key)) + { + return false; + } - dictionary.Add(key, value); - return true; - } + dictionary.Add(key, value); + return true; + } #endif } } diff --git a/YamlDotNet/ReflectionExtensions.cs b/YamlDotNet/ReflectionExtensions.cs index 25b836569..bf575f5d5 100644 --- a/YamlDotNet/ReflectionExtensions.cs +++ b/YamlDotNet/ReflectionExtensions.cs @@ -20,6 +20,7 @@ // SOFTWARE. using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -58,6 +59,16 @@ public static bool IsEnum(this Type type) return type.GetTypeInfo().IsEnum; } + public static bool IsRequired(this MemberInfo member) + { +#if NET8_0_OR_GREATER + var result = member.GetCustomAttributes().Any(); +#else + var result = member.GetCustomAttributes(true).Any(x => x.GetType().FullName == "System.Runtime.CompilerServices.RequiredMemberAttribute"); +#endif + return result; + } + /// /// Determines whether the specified type has a default constructor. /// @@ -270,16 +281,56 @@ public static Attribute[] GetAllCustomAttributes(this PropertyInfo m // on netstandard1.3 var result = new List(); var type = member.DeclaringType; + var name = member.Name; while (type != null) { - type.GetPublicProperty(member.Name); - result.AddRange(member.GetCustomAttributes(typeof(TAttribute))); + var property = type.GetPublicProperty(name); + + if (property != null) + { + result.AddRange(property.GetCustomAttributes(typeof(TAttribute))); + } type = type.BaseType(); } return result.ToArray(); } + private static readonly ConcurrentDictionary typesHaveNullContext = new ConcurrentDictionary(); + public static bool AcceptsNull(this MemberInfo member) + { + var result = true; //default to allowing nulls, this will be set to false if there is a null context on the type +#if NET8_0_OR_GREATER + var typeHasNullContext = typesHaveNullContext.GetOrAdd(member.DeclaringType, (Type t) => + { + var attributes = t.GetCustomAttributes(typeof(System.Runtime.CompilerServices.NullableContextAttribute), true); + return (attributes?.Length ?? 0) > 0; + }); + + if (typeHasNullContext) + { + // we have a nullable context on that type, only allow null if the NullableAttribute is on the member. + var memberAttributes = member.GetCustomAttributes(typeof(System.Runtime.CompilerServices.NullableAttribute), true); + result = (memberAttributes?.Length ?? 0) > 0; + } + + return result; +#else + var typeHasNullContext = typesHaveNullContext.GetOrAdd(member.DeclaringType, (Type t) => + { + var attributes = t.GetCustomAttributes(true); + return attributes.Any(x => x.GetType().FullName == "System.Runtime.CompilerServices.NullableContextAttribute"); + }); + + if (typeHasNullContext) + { + var memberAttributes = member.GetCustomAttributes(true); + result = memberAttributes.Any(x => x.GetType().FullName == "System.Runtime.CompilerServices.NullableAttribute"); + } + + return result; +#endif + } } } diff --git a/YamlDotNet/Serialization/BufferedDeserialization/TypeDiscriminatingNodeDeserializer.cs b/YamlDotNet/Serialization/BufferedDeserialization/TypeDiscriminatingNodeDeserializer.cs index b8aa25fb7..6ddf84387 100644 --- a/YamlDotNet/Serialization/BufferedDeserialization/TypeDiscriminatingNodeDeserializer.cs +++ b/YamlDotNet/Serialization/BufferedDeserialization/TypeDiscriminatingNodeDeserializer.cs @@ -56,7 +56,7 @@ public TypeDiscriminatingNodeDeserializer(IList innerDeserial this.maxLengthToBuffer = maxLengthToBuffer; } - public bool Deserialize(IParser reader, Type expectedType, Func nestedObjectDeserializer, out object? value) + public bool Deserialize(IParser reader, Type expectedType, Func nestedObjectDeserializer, out object? value, ObjectDeserializer rootDeserializer) { if (!reader.Accept(out var mapping)) { @@ -107,7 +107,7 @@ public bool Deserialize(IParser reader, Type expectedType, Func /// instance. /// to convert. + /// The deserializer to use to deserialize complex types. /// Returns the instance converted. /// On deserializing, all formats in the list are used for conversion. - public object ReadYaml(IParser parser, Type type) + public object ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) { var value = parser.Consume().Value; @@ -83,8 +84,9 @@ public object ReadYaml(IParser parser, Type type) /// instance. /// Value to write. /// to convert. + /// The root serializer that can be used to serialize complex types. /// On serializing, the first format in the list is used. - public void WriteYaml(IEmitter emitter, object? value, Type type) + public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer) { var dateOnly = (DateOnly)value!; var formatted = dateOnly.ToString(this.formats.First(), this.provider); // Always take the first format of the list. diff --git a/YamlDotNet/Serialization/Converters/DateTime8601Converter.cs b/YamlDotNet/Serialization/Converters/DateTime8601Converter.cs new file mode 100644 index 000000000..8dd7084f0 --- /dev/null +++ b/YamlDotNet/Serialization/Converters/DateTime8601Converter.cs @@ -0,0 +1,95 @@ +// This file is part of YamlDotNet - A .NET library for YAML. +// Copyright (c) Antoine Aubry and contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System; +using System.Globalization; +using YamlDotNet.Core; +using YamlDotNet.Core.Events; + +namespace YamlDotNet.Serialization.Converters +{ + /// + /// This represents the YAML converter entity for using the ISO-8601 standard format. + /// + public class DateTime8601Converter : IYamlTypeConverter + { + private readonly IFormatProvider provider; + private readonly ScalarStyle scalarStyle; + + /// + /// Initializes a new instance of the class using the default any scalar style. + /// + public DateTime8601Converter() + : this(ScalarStyle.Any) + { + } + + /// + /// Initializes a new instance of the class. + /// + public DateTime8601Converter(ScalarStyle scalarStyle) + { + this.provider = CultureInfo.InvariantCulture; + this.scalarStyle = scalarStyle; + } + + /// + /// Gets a value indicating whether the current converter supports converting the specified type. + /// + /// to check. + /// Returns True, if the current converter supports; otherwise returns False. + public bool Accepts(Type type) + { + return type == typeof(DateTime); + } + + /// + /// Reads an object's state from a YAML parser. + /// + /// instance. + /// to convert. + /// The deserializer to use to deserialize complex types. + /// Returns the instance converted. + /// On deserializing, all formats in the list are used for conversion. + public object ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) + { + var value = parser.Consume().Value; + var result = DateTime.ParseExact(value, "O", CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind); + + return result; + } + + /// + /// Writes the specified object's state to a YAML emitter. + /// + /// instance. + /// Value to write. + /// to convert. + /// A serializer to serializer complext objects. + /// On serializing, the first format in the list is used. + public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer) + { + var formatted = ((DateTime)value!).ToString("O", CultureInfo.InvariantCulture); + + emitter.Emit(new Scalar(AnchorName.Empty, TagName.Empty, formatted, scalarStyle, true, false)); + } + } +} diff --git a/YamlDotNet/Serialization/Converters/DateTimeConverter.cs b/YamlDotNet/Serialization/Converters/DateTimeConverter.cs index 0b340aaf2..d70f70b1a 100644 --- a/YamlDotNet/Serialization/Converters/DateTimeConverter.cs +++ b/YamlDotNet/Serialization/Converters/DateTimeConverter.cs @@ -69,9 +69,10 @@ public bool Accepts(Type type) /// /// instance. /// to convert. + /// The deserializer to use to deserialize complex types. /// Returns the instance converted. /// On deserializing, all formats in the list are used for conversion. - public object ReadYaml(IParser parser, Type type) + public object ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) { var value = parser.Consume().Value; var style = this.kind == DateTimeKind.Local ? DateTimeStyles.AssumeLocal : DateTimeStyles.AssumeUniversal; @@ -87,8 +88,9 @@ public object ReadYaml(IParser parser, Type type) /// instance. /// Value to write. /// to convert. + /// A serializer to serializer complext objects. /// On serializing, the first format in the list is used. - public void WriteYaml(IEmitter emitter, object? value, Type type) + public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer) { var dt = (DateTime)value!; var adjusted = this.kind == DateTimeKind.Local ? dt.ToLocalTime() : dt.ToUniversalTime(); diff --git a/YamlDotNet/Serialization/Converters/DateTimeOffsetConverter.cs b/YamlDotNet/Serialization/Converters/DateTimeOffsetConverter.cs index 81f7c51a8..80b5ccfe3 100644 --- a/YamlDotNet/Serialization/Converters/DateTimeOffsetConverter.cs +++ b/YamlDotNet/Serialization/Converters/DateTimeOffsetConverter.cs @@ -74,9 +74,10 @@ public bool Accepts(Type type) /// /// instance. /// to convert. + /// The deserializer to use to deserialize complex types. /// Returns the instance converted. /// On deserializing, all formats in the list are used for conversion. - public object ReadYaml(IParser parser, Type type) + public object ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) { var value = parser.Consume().Value; var result = DateTimeOffset.ParseExact(value, formats, provider, dateStyle); @@ -90,8 +91,9 @@ public object ReadYaml(IParser parser, Type type) /// instance. /// Value to write. /// to convert. + /// A serializer to serializer complext objects. /// On serializing, the first format in the list is used. - public void WriteYaml(IEmitter emitter, object? value, Type type) + public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer) { var dt = (DateTimeOffset)value!; var formatted = dt.ToString(formats.First(), this.provider); // Always take the first format of the list. diff --git a/YamlDotNet/Serialization/Converters/GuidConverter.cs b/YamlDotNet/Serialization/Converters/GuidConverter.cs index e93707bf8..7b38ecfb6 100644 --- a/YamlDotNet/Serialization/Converters/GuidConverter.cs +++ b/YamlDotNet/Serialization/Converters/GuidConverter.cs @@ -42,13 +42,13 @@ public bool Accepts(Type type) return type == typeof(Guid); } - public object ReadYaml(IParser parser, Type type) + public object ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) { var value = parser.Consume().Value; return new Guid(value); } - public void WriteYaml(IEmitter emitter, object? value, Type type) + public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer) { var guid = (Guid)value!; emitter.Emit(new Scalar(AnchorName.Empty, TagName.Empty, guid.ToString("D"), jsonCompatible ? ScalarStyle.DoubleQuoted : ScalarStyle.Any, true, false)); diff --git a/YamlDotNet/Serialization/Converters/SystemTypeConverter.cs b/YamlDotNet/Serialization/Converters/SystemTypeConverter.cs index c819b0bae..841664bba 100644 --- a/YamlDotNet/Serialization/Converters/SystemTypeConverter.cs +++ b/YamlDotNet/Serialization/Converters/SystemTypeConverter.cs @@ -38,13 +38,13 @@ public bool Accepts(Type type) return typeof(Type).IsAssignableFrom(type); } - public object ReadYaml(IParser parser, Type type) + public object ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) { var value = parser.Consume().Value; return Type.GetType(value, throwOnError: true)!; // Will throw instead of returning null } - public void WriteYaml(IEmitter emitter, object? value, Type type) + public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer) { var systemType = (Type)value!; emitter.Emit(new Scalar(AnchorName.Empty, TagName.Empty, systemType.AssemblyQualifiedName!, ScalarStyle.Any, true, false)); diff --git a/YamlDotNet/Serialization/Converters/TimeOnlyConverter.cs b/YamlDotNet/Serialization/Converters/TimeOnlyConverter.cs index 05e53a2d0..437c634ec 100644 --- a/YamlDotNet/Serialization/Converters/TimeOnlyConverter.cs +++ b/YamlDotNet/Serialization/Converters/TimeOnlyConverter.cs @@ -67,9 +67,10 @@ public bool Accepts(Type type) /// /// instance. /// to convert. + /// The deserializer to use to deserialize complex types. /// Returns the instance converted. /// On deserializing, all formats in the list are used for conversion. - public object ReadYaml(IParser parser, Type type) + public object ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) { var value = parser.Consume().Value; @@ -83,8 +84,9 @@ public object ReadYaml(IParser parser, Type type) /// instance. /// Value to write. /// to convert. + /// A serializer to serializer complext objects. /// On serializing, the first format in the list is used. - public void WriteYaml(IEmitter emitter, object? value, Type type) + public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer) { var timeOnly = (TimeOnly)value!; var formatted = timeOnly.ToString(this.formats.First(), this.provider); // Always take the first format of the list. diff --git a/YamlDotNet/Serialization/DeserializerBuilder.cs b/YamlDotNet/Serialization/DeserializerBuilder.cs index 38b4e9a23..2c8257997 100755 --- a/YamlDotNet/Serialization/DeserializerBuilder.cs +++ b/YamlDotNet/Serialization/DeserializerBuilder.cs @@ -60,6 +60,9 @@ public sealed class DeserializerBuilder : BuilderSkeleton private bool ignoreUnmatched; private bool duplicateKeyChecking; private bool attemptUnknownTypeDeserialization; + private bool enforceNullability; + private bool caseInsensitivePropertyMatching; + private bool enforceRequiredProperties; /// /// Initializes a new using the default component registrations. @@ -92,10 +95,10 @@ public DeserializerBuilder() { typeof(YamlSerializableNodeDeserializer), _ => new YamlSerializableNodeDeserializer(objectFactory.Value) }, { typeof(TypeConverterNodeDeserializer), _ => new TypeConverterNodeDeserializer(BuildTypeConverters()) }, { typeof(NullNodeDeserializer), _ => new NullNodeDeserializer() }, - { typeof(ScalarNodeDeserializer), _ => new ScalarNodeDeserializer(attemptUnknownTypeDeserialization, typeConverter, yamlFormatter, enumNamingConvention) }, - { typeof(ArrayNodeDeserializer), _ => new ArrayNodeDeserializer(enumNamingConvention) }, + { typeof(ScalarNodeDeserializer), _ => new ScalarNodeDeserializer(attemptUnknownTypeDeserialization, typeConverter, BuildTypeInspector(), yamlFormatter, enumNamingConvention) }, + { typeof(ArrayNodeDeserializer), _ => new ArrayNodeDeserializer(enumNamingConvention, BuildTypeInspector()) }, { typeof(DictionaryNodeDeserializer), _ => new DictionaryNodeDeserializer(objectFactory.Value, duplicateKeyChecking) }, - { typeof(CollectionNodeDeserializer), _ => new CollectionNodeDeserializer(objectFactory.Value, enumNamingConvention) }, + { typeof(CollectionNodeDeserializer), _ => new CollectionNodeDeserializer(objectFactory.Value, enumNamingConvention, BuildTypeInspector()) }, { typeof(EnumerableNodeDeserializer), _ => new EnumerableNodeDeserializer() }, { typeof(ObjectNodeDeserializer), _ => new ObjectNodeDeserializer(objectFactory.Value, @@ -103,9 +106,13 @@ public DeserializerBuilder() ignoreUnmatched, duplicateKeyChecking, typeConverter, - enumNamingConvention) + enumNamingConvention, + enforceNullability, + caseInsensitivePropertyMatching, + enforceRequiredProperties, + BuildTypeConverters()) }, - { typeof(FsharpListNodeDeserializer), _ => new FsharpListNodeDeserializer(enumNamingConvention) }, + { typeof(FsharpListNodeDeserializer), _ => new FsharpListNodeDeserializer(BuildTypeInspector(), enumNamingConvention) }, }; nodeTypeResolverFactories = new LazyComponentRegistrationList @@ -123,7 +130,11 @@ public DeserializerBuilder() protected override DeserializerBuilder Self { get { return this; } } - internal ITypeInspector BuildTypeInspector() + /// + /// Builds the type inspector used by various classes to get information about types and their members. + /// + /// + public ITypeInspector BuildTypeInspector() { ITypeInspector innerInspector = new WritablePropertiesTypeInspector(typeResolver, includeNonPublicProperties); @@ -334,6 +345,35 @@ Action> where return this; } + /// + /// Ignore case when matching property names. + /// + /// + public DeserializerBuilder WithCaseInsensitivePropertyMatching() + { + caseInsensitivePropertyMatching = true; + return this; + } + + /// + /// Enforce whether null values can be set on non-nullable properties and fields. + /// + /// This deserializer builder. + public DeserializerBuilder WithEnforceNullability() + { + enforceNullability = true; + return this; + } + /// + /// Require that all members with the 'required' keyword be set by YAML. + /// + /// + public DeserializerBuilder WithEnforceRequiredMembers() + { + enforceRequiredProperties = true; + return this; + } + /// /// Unregisters an existing of type . /// @@ -463,7 +503,8 @@ public IValueDeserializer BuildValueDeserializer() nodeDeserializerFactories.BuildComponentList(), nodeTypeResolverFactories.BuildComponentList(), typeConverter, - enumNamingConvention + enumNamingConvention, + BuildTypeInspector() ) ); } diff --git a/YamlDotNet/Serialization/EventEmitters/JsonEventEmitter.cs b/YamlDotNet/Serialization/EventEmitters/JsonEventEmitter.cs index 3214a5378..d1f413c85 100644 --- a/YamlDotNet/Serialization/EventEmitters/JsonEventEmitter.cs +++ b/YamlDotNet/Serialization/EventEmitters/JsonEventEmitter.cs @@ -20,6 +20,7 @@ // SOFTWARE. using System; +using System.Text.RegularExpressions; using YamlDotNet.Core; using YamlDotNet.Core.Events; using YamlDotNet.Serialization.NamingConventions; @@ -30,12 +31,15 @@ public sealed class JsonEventEmitter : ChainedEventEmitter { private readonly YamlFormatter formatter; private readonly INamingConvention enumNamingConvention; + private readonly ITypeInspector typeInspector; + private static readonly Regex numericRegex = new Regex(@"^-?\d+\.?\d+$", RegexOptions.Compiled); - public JsonEventEmitter(IEventEmitter nextEmitter, YamlFormatter formatter, INamingConvention enumNamingConvention) + public JsonEventEmitter(IEventEmitter nextEmitter, YamlFormatter formatter, INamingConvention enumNamingConvention, ITypeInspector typeInspector) : base(nextEmitter) { this.formatter = formatter; this.enumNamingConvention = enumNamingConvention; + this.typeInspector = typeInspector; } public override void Emit(AliasEventInfo eventInfo, IEmitter emitter) @@ -73,7 +77,7 @@ public override void Emit(ScalarEventInfo eventInfo, IEmitter emitter) var valueIsEnum = eventInfo.Source.Type.IsEnum(); if (valueIsEnum) { - eventInfo.RenderedValue = formatter.FormatEnum(value, enumNamingConvention); + eventInfo.RenderedValue = formatter.FormatEnum(value, typeInspector, enumNamingConvention); eventInfo.Style = formatter.PotentiallyQuoteEnums(value) ? ScalarStyle.DoubleQuoted : ScalarStyle.Plain; break; } @@ -85,6 +89,12 @@ public override void Emit(ScalarEventInfo eventInfo, IEmitter emitter) case TypeCode.Double: case TypeCode.Decimal: eventInfo.RenderedValue = formatter.FormatNumber(value); + + if (!numericRegex.IsMatch(eventInfo.RenderedValue)) + { + eventInfo.Style = ScalarStyle.DoubleQuoted; + } + break; case TypeCode.String: diff --git a/YamlDotNet/Serialization/EventEmitters/TypeAssigningEventEmitter.cs b/YamlDotNet/Serialization/EventEmitters/TypeAssigningEventEmitter.cs index 702907ff5..5dae3fbf5 100644 --- a/YamlDotNet/Serialization/EventEmitters/TypeAssigningEventEmitter.cs +++ b/YamlDotNet/Serialization/EventEmitters/TypeAssigningEventEmitter.cs @@ -71,6 +71,7 @@ public sealed class TypeAssigningEventEmitter : ChainedEventEmitter private readonly ScalarStyle defaultScalarStyle = ScalarStyle.Any; private readonly YamlFormatter formatter; private readonly INamingConvention enumNamingConvention; + private readonly ITypeInspector typeInspector; public TypeAssigningEventEmitter(IEventEmitter nextEmitter, IDictionary tagMappings, @@ -78,7 +79,8 @@ public TypeAssigningEventEmitter(IEventEmitter nextEmitter, bool quoteYaml1_1Strings, ScalarStyle defaultScalarStyle, YamlFormatter formatter, - INamingConvention enumNamingConvention) + INamingConvention enumNamingConvention, + ITypeInspector typeInspector) : base(nextEmitter) { this.defaultScalarStyle = defaultScalarStyle; @@ -95,6 +97,7 @@ public TypeAssigningEventEmitter(IEventEmitter nextEmitter, isSpecialStringValue_Regex = new Regex(specialStringValuePattern, RegexOptions.Compiled); #endif this.enumNamingConvention = enumNamingConvention; + this.typeInspector = typeInspector; } public override void Emit(ScalarEventInfo eventInfo, IEmitter emitter) @@ -129,7 +132,7 @@ public override void Emit(ScalarEventInfo eventInfo, IEmitter emitter) if (eventInfo.Source.Type.IsEnum) { eventInfo.Tag = FailsafeSchema.Tags.Str; - eventInfo.RenderedValue = formatter.FormatEnum(value, enumNamingConvention); + eventInfo.RenderedValue = formatter.FormatEnum(value, typeInspector, enumNamingConvention); if (quoteNecessaryStrings && IsSpecialStringValue(eventInfo.RenderedValue) && diff --git a/YamlDotNet/Serialization/INodeDeserializer.cs b/YamlDotNet/Serialization/INodeDeserializer.cs index 9e691fcdc..1ab21cbe1 100644 --- a/YamlDotNet/Serialization/INodeDeserializer.cs +++ b/YamlDotNet/Serialization/INodeDeserializer.cs @@ -26,6 +26,7 @@ namespace YamlDotNet.Serialization { public interface INodeDeserializer { - bool Deserialize(IParser reader, Type expectedType, Func nestedObjectDeserializer, out object? value); + + bool Deserialize(IParser reader, Type expectedType, Func nestedObjectDeserializer, out object? value, ObjectDeserializer rootDeserializer); } } diff --git a/YamlDotNet/Serialization/IObjectGraphTraversalStrategy.cs b/YamlDotNet/Serialization/IObjectGraphTraversalStrategy.cs index dece648f7..a8fd00f1d 100644 --- a/YamlDotNet/Serialization/IObjectGraphTraversalStrategy.cs +++ b/YamlDotNet/Serialization/IObjectGraphTraversalStrategy.cs @@ -32,6 +32,7 @@ public interface IObjectGraphTraversalStrategy /// The graph. /// An that is to be notified during the traversal. /// A that will be passed to the . - void Traverse(IObjectDescriptor graph, IObjectGraphVisitor visitor, TContext context); + /// The serializer to use to serialize complex objects. + void Traverse(IObjectDescriptor graph, IObjectGraphVisitor visitor, TContext context, ObjectSerializer serializer); } } diff --git a/YamlDotNet/Serialization/IObjectGraphVisitor.cs b/YamlDotNet/Serialization/IObjectGraphVisitor.cs index 87455f107..a2e8d9b92 100644 --- a/YamlDotNet/Serialization/IObjectGraphVisitor.cs +++ b/YamlDotNet/Serialization/IObjectGraphVisitor.cs @@ -34,8 +34,10 @@ public interface IObjectGraphVisitor /// /// The value that is about to be entered. /// The context that this implementation depend on. + /// A serializer that can be used to serialize complex objects. + /// The descriptor for the property that the value belongs to. /// If the value is to be entered, returns true; otherwise returns false; - bool Enter(IObjectDescriptor value, TContext context); + bool Enter(IPropertyDescriptor? propertyDescriptor, IObjectDescriptor value, TContext context, ObjectSerializer serializer); /// /// Indicates whether the specified mapping should be entered. This allows the visitor to @@ -44,8 +46,9 @@ public interface IObjectGraphVisitor /// The key of the mapping that is about to be entered. /// The value of the mapping that is about to be entered. /// The context that this implementation depend on. + /// A serializer that can be used to serialize complex objects. /// If the mapping is to be entered, returns true; otherwise returns false; - bool EnterMapping(IObjectDescriptor key, IObjectDescriptor value, TContext context); + bool EnterMapping(IObjectDescriptor key, IObjectDescriptor value, TContext context, ObjectSerializer serializer); /// /// Indicates whether the specified mapping should be entered. This allows the visitor to @@ -55,15 +58,17 @@ public interface IObjectGraphVisitor /// The that provided access to . /// The value of the mapping that is about to be entered. /// The context that this implementation depend on. + /// A serializer that can be used to serialize complex objects. /// If the mapping is to be entered, returns true; otherwise returns false; - bool EnterMapping(IPropertyDescriptor key, IObjectDescriptor value, TContext context); + bool EnterMapping(IPropertyDescriptor key, IObjectDescriptor value, TContext context, ObjectSerializer serializer); /// /// Notifies the visitor that a scalar value has been encountered. /// /// The value of the scalar. /// The context that this implementation depend on. - void VisitScalar(IObjectDescriptor scalar, TContext context); + /// A serializer that can be used to serialize complex objects. + void VisitScalar(IObjectDescriptor scalar, TContext context, ObjectSerializer serializer); /// /// Notifies the visitor that the traversal of a mapping is about to begin. @@ -72,14 +77,16 @@ public interface IObjectGraphVisitor /// The static type of the keys of the mapping. /// The static type of the values of the mapping. /// The context that this implementation depend on. - void VisitMappingStart(IObjectDescriptor mapping, Type keyType, Type valueType, TContext context); + /// A serializer that can be used to serialize complex objects. + void VisitMappingStart(IObjectDescriptor mapping, Type keyType, Type valueType, TContext context, ObjectSerializer serializer); /// /// Notifies the visitor that the traversal of a mapping has ended. /// /// The value that corresponds to the mapping. /// The context that this implementation depend on. - void VisitMappingEnd(IObjectDescriptor mapping, TContext context); + /// A serializer that can be used to serialize complex objects. + void VisitMappingEnd(IObjectDescriptor mapping, TContext context, ObjectSerializer serializer); /// /// Notifies the visitor that the traversal of a sequence is about to begin. @@ -87,13 +94,15 @@ public interface IObjectGraphVisitor /// The value that corresponds to the sequence. /// The static type of the elements of the sequence. /// The context that this implementation depend on. - void VisitSequenceStart(IObjectDescriptor sequence, Type elementType, TContext context); + /// A serializer that can be used to serialize complex objects. + void VisitSequenceStart(IObjectDescriptor sequence, Type elementType, TContext context, ObjectSerializer serializer); /// /// Notifies the visitor that the traversal of a sequence has ended. /// /// The value that corresponds to the sequence. /// The context that this implementation depend on. - void VisitSequenceEnd(IObjectDescriptor sequence, TContext context); + /// A serializer that can be used to serialize complex objects. + void VisitSequenceEnd(IObjectDescriptor sequence, TContext context, ObjectSerializer serializer); } } diff --git a/YamlDotNet/Serialization/IPropertyDescriptor.cs b/YamlDotNet/Serialization/IPropertyDescriptor.cs index 7739c7363..b2e6381e9 100644 --- a/YamlDotNet/Serialization/IPropertyDescriptor.cs +++ b/YamlDotNet/Serialization/IPropertyDescriptor.cs @@ -27,11 +27,14 @@ namespace YamlDotNet.Serialization public interface IPropertyDescriptor { string Name { get; } + bool AllowNulls { get; } bool CanWrite { get; } Type Type { get; } Type? TypeOverride { get; set; } int Order { get; set; } ScalarStyle ScalarStyle { get; set; } + bool Required { get; } + Type? ConverterType { get; } T? GetCustomAttribute() where T : Attribute; diff --git a/YamlDotNet/Serialization/ITypeInspector.cs b/YamlDotNet/Serialization/ITypeInspector.cs index 7e73796df..c1603d728 100644 --- a/YamlDotNet/Serialization/ITypeInspector.cs +++ b/YamlDotNet/Serialization/ITypeInspector.cs @@ -48,7 +48,23 @@ public interface ITypeInspector /// Determines if an exception or null should be returned if can't be /// found in /// + /// If true use case-insitivity when choosing the property or field. /// - IPropertyDescriptor GetProperty(Type type, object? container, string name, [MaybeNullWhen(true)] bool ignoreUnmatched); + IPropertyDescriptor GetProperty(Type type, object? container, string name, [MaybeNullWhen(true)] bool ignoreUnmatched, bool caseInsensitivePropertyMatching); + + /// + /// Returns the actual name from the EnumMember attribute + /// + /// The type of the enum. + /// The name to lookup. + /// The actual name of the enum value. + string GetEnumName(Type enumType, string name); + + /// + /// Return the value of the enum + /// + /// + /// + string GetEnumValue(object enumValue); } } diff --git a/YamlDotNet/Serialization/IYamlTypeConverter.cs b/YamlDotNet/Serialization/IYamlTypeConverter.cs index 497657b01..4e172f80a 100644 --- a/YamlDotNet/Serialization/IYamlTypeConverter.cs +++ b/YamlDotNet/Serialization/IYamlTypeConverter.cs @@ -37,11 +37,11 @@ public interface IYamlTypeConverter /// /// Reads an object's state from a YAML parser. /// - object? ReadYaml(IParser parser, Type type); + object? ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer); /// /// Writes the specified object's state to a YAML emitter. /// - void WriteYaml(IEmitter emitter, object? value, Type type); + void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer); } } diff --git a/YamlDotNet/Serialization/NodeDeserializers/ArrayNodeDeserializer.cs b/YamlDotNet/Serialization/NodeDeserializers/ArrayNodeDeserializer.cs index a73239cd9..571b9664f 100644 --- a/YamlDotNet/Serialization/NodeDeserializers/ArrayNodeDeserializer.cs +++ b/YamlDotNet/Serialization/NodeDeserializers/ArrayNodeDeserializer.cs @@ -29,13 +29,15 @@ namespace YamlDotNet.Serialization.NodeDeserializers public sealed class ArrayNodeDeserializer : INodeDeserializer { private readonly INamingConvention enumNamingConvention; + private readonly ITypeInspector typeInspector; - public ArrayNodeDeserializer(INamingConvention enumNamingConvention) + public ArrayNodeDeserializer(INamingConvention enumNamingConvention, ITypeInspector typeInspector) { this.enumNamingConvention = enumNamingConvention; + this.typeInspector = typeInspector; } - public bool Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value) + public bool Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value, ObjectDeserializer rootDeserializer) { if (!expectedType.IsArray) { @@ -47,7 +49,7 @@ public bool Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value) + public bool Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value, ObjectDeserializer rootDeserializer) { IList? list; var canUpdate = true; @@ -74,12 +76,19 @@ public bool Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, IList result, bool canUpdate, INamingConvention enumNamingConvention, Action? promiseResolvedHandler = null) + internal static void DeserializeHelper(Type tItem, + IParser parser, + Func nestedObjectDeserializer, + IList result, + bool canUpdate, + INamingConvention enumNamingConvention, + ITypeInspector typeInspector, + Action? promiseResolvedHandler = null) { parser.Consume(); while (!parser.TryConsume(out var _)) @@ -92,13 +101,14 @@ internal static void DeserializeHelper(Type tItem, IParser parser, Func promiseResolvedHandler(index, v); } else { - promise.ValueAvailable += v => result[index] = TypeConverter.ChangeType(v, tItem, enumNamingConvention); + promise.ValueAvailable += v => result[index] = TypeConverter.ChangeType(v, tItem, enumNamingConvention, typeInspector); } } else @@ -112,7 +122,7 @@ internal static void DeserializeHelper(Type tItem, IParser parser, Func nestedObjectDeserializer, IDictionary result) + protected virtual void Deserialize(Type tKey, Type tValue, IParser parser, Func nestedObjectDeserializer, IDictionary result, ObjectDeserializer rootDeserializer) { var property = parser.Consume(); while (!parser.TryConsume(out var _)) diff --git a/YamlDotNet/Serialization/NodeDeserializers/DictionaryNodeDeserializer.cs b/YamlDotNet/Serialization/NodeDeserializers/DictionaryNodeDeserializer.cs index ddc706b32..43d8c9993 100644 --- a/YamlDotNet/Serialization/NodeDeserializers/DictionaryNodeDeserializer.cs +++ b/YamlDotNet/Serialization/NodeDeserializers/DictionaryNodeDeserializer.cs @@ -39,7 +39,7 @@ public DictionaryNodeDeserializer(IObjectFactory objectFactory, bool duplicateKe this.objectFactory = objectFactory ?? throw new ArgumentNullException(nameof(objectFactory)); } - public bool Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value) + public bool Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value, ObjectDeserializer rootDeserializer) { IDictionary? dictionary; Type keyType, valueType; @@ -73,7 +73,7 @@ public bool Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value) + public bool Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value, ObjectDeserializer rootDeserializer) { Type itemsType; if (expectedType == typeof(IEnumerable)) diff --git a/YamlDotNet/Serialization/NodeDeserializers/FsharpListNodeDeserializer.cs b/YamlDotNet/Serialization/NodeDeserializers/FsharpListNodeDeserializer.cs index b2f0f613f..274f2258a 100644 --- a/YamlDotNet/Serialization/NodeDeserializers/FsharpListNodeDeserializer.cs +++ b/YamlDotNet/Serialization/NodeDeserializers/FsharpListNodeDeserializer.cs @@ -30,14 +30,16 @@ namespace YamlDotNet.Serialization.NodeDeserializers { public sealed class FsharpListNodeDeserializer : INodeDeserializer { + private readonly ITypeInspector typeInspector; private readonly INamingConvention enumNamingConvention; - public FsharpListNodeDeserializer(INamingConvention enumNamingConvention) + public FsharpListNodeDeserializer(ITypeInspector typeInspector, INamingConvention enumNamingConvention) { + this.typeInspector = typeInspector; this.enumNamingConvention = enumNamingConvention; } - public bool Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value) + public bool Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value, ObjectDeserializer rootDeserializer) { if (!FsharpHelper.IsFsharpListType(expectedType)) { @@ -49,7 +51,7 @@ public bool Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value) + public bool Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value, ObjectDeserializer rootDeserializer) { value = null; if (parser.Accept(out var evt)) diff --git a/YamlDotNet/Serialization/NodeDeserializers/ObjectNodeDeserializer.cs b/YamlDotNet/Serialization/NodeDeserializers/ObjectNodeDeserializer.cs index 06dbb6b0e..b2a179c5d 100644 --- a/YamlDotNet/Serialization/NodeDeserializers/ObjectNodeDeserializer.cs +++ b/YamlDotNet/Serialization/NodeDeserializers/ObjectNodeDeserializer.cs @@ -21,6 +21,8 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Reflection; using System.Runtime.Serialization; using YamlDotNet.Core; using YamlDotNet.Core.Events; @@ -32,28 +34,40 @@ namespace YamlDotNet.Serialization.NodeDeserializers public sealed class ObjectNodeDeserializer : INodeDeserializer { private readonly IObjectFactory objectFactory; - private readonly ITypeInspector typeDescriptor; + private readonly ITypeInspector typeInspector; private readonly bool ignoreUnmatched; private readonly bool duplicateKeyChecking; private readonly ITypeConverter typeConverter; private readonly INamingConvention enumNamingConvention; + private readonly bool enforceNullability; + private readonly bool caseInsensitivePropertyMatching; + private readonly bool enforceRequiredProperties; + private readonly IEnumerable typeConverters; public ObjectNodeDeserializer(IObjectFactory objectFactory, - ITypeInspector typeDescriptor, + ITypeInspector typeInspector, bool ignoreUnmatched, bool duplicateKeyChecking, ITypeConverter typeConverter, - INamingConvention enumNamingConvention) + INamingConvention enumNamingConvention, + bool enforceNullability, + bool caseInsensitivePropertyMatching, + bool enforceRequiredProperties, + IEnumerable typeConverters) { this.objectFactory = objectFactory ?? throw new ArgumentNullException(nameof(objectFactory)); - this.typeDescriptor = typeDescriptor ?? throw new ArgumentNullException(nameof(typeDescriptor)); + this.typeInspector = typeInspector ?? throw new ArgumentNullException(nameof(ObjectNodeDeserializer.typeInspector)); this.ignoreUnmatched = ignoreUnmatched; this.duplicateKeyChecking = duplicateKeyChecking; this.typeConverter = typeConverter ?? throw new ArgumentNullException(nameof(typeConverter)); this.enumNamingConvention = enumNamingConvention ?? throw new ArgumentNullException(nameof(enumNamingConvention)); + this.enforceNullability = enforceNullability; + this.caseInsensitivePropertyMatching = caseInsensitivePropertyMatching; + this.enforceRequiredProperties = enforceRequiredProperties; + this.typeConverters = typeConverters; } - public bool Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value) + public bool Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value, ObjectDeserializer rootDeserializer) { if (!parser.TryConsume(out var mapping)) { @@ -70,6 +84,9 @@ public bool Deserialize(IParser parser, Type expectedType, Func(StringComparer.Ordinal); + var consumedObjectProperties = new HashSet(StringComparer.Ordinal); + var mark = Mark.Empty; + while (!parser.TryConsume(out var _)) { var propertyName = parser.Consume(); @@ -79,26 +96,43 @@ public bool Deserialize(IParser parser, Type expectedType, Func x.GetType() == property.ConverterType)!; + propertyValue = typeConverter.ReadYaml(parser, property.Type, rootDeserializer); + } + else + { + propertyValue = nestedObjectDeserializer(parser, property.Type); + } - var propertyValue = nestedObjectDeserializer(parser, property.Type); if (propertyValue is IValuePromise propertyValuePromise) { var valueRef = value; propertyValuePromise.ValueAvailable += v => { - var convertedValue = typeConverter.ChangeType(v, property.Type, enumNamingConvention); + var convertedValue = typeConverter.ChangeType(v, property.Type, enumNamingConvention, typeInspector); + + NullCheck(convertedValue, property, propertyName); + property.Write(valueRef, convertedValue); }; } else { - var convertedValue = typeConverter.ChangeType(propertyValue, property.Type, enumNamingConvention); + var convertedValue = typeConverter.ChangeType(propertyValue, property.Type, enumNamingConvention, typeInspector); + + NullCheck(convertedValue, property, propertyName); + property.Write(value, convertedValue); } } @@ -114,10 +148,42 @@ public bool Deserialize(IParser parser, Type expectedType, Func(); + foreach (var property in properties) + { + if (property.Required && !consumedObjectProperties.Contains(property.Name)) + { + missingPropertyNames.Add(property.Name); + } + } + + if (missingPropertyNames.Count > 0) + { + var propertyNames = string.Join(",", missingPropertyNames); + throw new YamlException(mark, mark, $"Missing properties, '{propertyNames}' in source yaml."); + } } objectFactory.ExecuteOnDeserialized(value); return true; } + + public void NullCheck(object value, IPropertyDescriptor property, Scalar propertyName) + { + if (enforceNullability && + value == null && + !property.AllowNulls) + { + throw new YamlException(propertyName.Start, propertyName.End, "Strict nullability enforcement error.", new NullReferenceException("Yaml value is null when target property requires non null values.")); + } + } } } diff --git a/YamlDotNet/Serialization/NodeDeserializers/ScalarNodeDeserializer.cs b/YamlDotNet/Serialization/NodeDeserializers/ScalarNodeDeserializer.cs index f7049ecd7..175376ee0 100644 --- a/YamlDotNet/Serialization/NodeDeserializers/ScalarNodeDeserializer.cs +++ b/YamlDotNet/Serialization/NodeDeserializers/ScalarNodeDeserializer.cs @@ -36,18 +36,24 @@ public sealed class ScalarNodeDeserializer : INodeDeserializer private const string BooleanFalsePattern = "^(false|n|no|off)$"; private readonly bool attemptUnknownTypeDeserialization; private readonly ITypeConverter typeConverter; + private readonly ITypeInspector typeInspector; private readonly YamlFormatter formatter; private readonly INamingConvention enumNamingConvention; - public ScalarNodeDeserializer(bool attemptUnknownTypeDeserialization, ITypeConverter typeConverter, YamlFormatter formatter, INamingConvention enumNamingConvention) + public ScalarNodeDeserializer(bool attemptUnknownTypeDeserialization, + ITypeConverter typeConverter, + ITypeInspector typeInspector, + YamlFormatter formatter, + INamingConvention enumNamingConvention) { this.attemptUnknownTypeDeserialization = attemptUnknownTypeDeserialization; this.typeConverter = typeConverter ?? throw new ArgumentNullException(nameof(typeConverter)); + this.typeInspector = typeInspector; this.formatter = formatter; this.enumNamingConvention = enumNamingConvention; } - public bool Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value) + public bool Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value, ObjectDeserializer rootDeserializer) { if (!parser.TryConsume(out var scalar)) { @@ -63,6 +69,9 @@ public bool Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value) + public bool Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value, ObjectDeserializer rootDeserializer) { if (!factory.IsArray(expectedType)) { diff --git a/YamlDotNet/Serialization/NodeDeserializers/StaticCollectionNodeDeserializer.cs b/YamlDotNet/Serialization/NodeDeserializers/StaticCollectionNodeDeserializer.cs index 643e8691a..150f5bc9c 100644 --- a/YamlDotNet/Serialization/NodeDeserializers/StaticCollectionNodeDeserializer.cs +++ b/YamlDotNet/Serialization/NodeDeserializers/StaticCollectionNodeDeserializer.cs @@ -39,7 +39,7 @@ public StaticCollectionNodeDeserializer(StaticObjectFactory factory) this.factory = factory ?? throw new ArgumentNullException(nameof(factory)); } - public bool Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value) + public bool Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value, ObjectDeserializer rootDeserializer) { if (!factory.IsList(expectedType)) { diff --git a/YamlDotNet/Serialization/NodeDeserializers/StaticDictionaryNodeDeserializer.cs b/YamlDotNet/Serialization/NodeDeserializers/StaticDictionaryNodeDeserializer.cs index 0eef081bf..8d9c2c8e4 100644 --- a/YamlDotNet/Serialization/NodeDeserializers/StaticDictionaryNodeDeserializer.cs +++ b/YamlDotNet/Serialization/NodeDeserializers/StaticDictionaryNodeDeserializer.cs @@ -35,7 +35,7 @@ public StaticDictionaryNodeDeserializer(ObjectFactories.StaticObjectFactory obje _objectFactory = objectFactory ?? throw new ArgumentNullException(nameof(objectFactory)); } - public bool Deserialize(IParser reader, Type expectedType, Func nestedObjectDeserializer, out object? value) + public bool Deserialize(IParser reader, Type expectedType, Func nestedObjectDeserializer, out object? value, ObjectDeserializer rootDeserializer) { if (_objectFactory.IsDictionary(expectedType)) { @@ -49,7 +49,7 @@ public bool Deserialize(IParser reader, Type expectedType, Func converters) this.converters = converters ?? throw new ArgumentNullException(nameof(converters)); } - public bool Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value) + public bool Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value, ObjectDeserializer rootDeserializer) { var converter = converters.FirstOrDefault(c => c.Accepts(expectedType)); if (converter == null) @@ -44,7 +44,7 @@ public bool Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value) + public bool Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value, ObjectDeserializer rootDeserializer) { if (typeof(IYamlConvertible).IsAssignableFrom(expectedType)) { diff --git a/YamlDotNet/Serialization/NodeDeserializers/YamlSerializableNodeDeserializer.cs b/YamlDotNet/Serialization/NodeDeserializers/YamlSerializableNodeDeserializer.cs index 62d622b25..ffd454347 100644 --- a/YamlDotNet/Serialization/NodeDeserializers/YamlSerializableNodeDeserializer.cs +++ b/YamlDotNet/Serialization/NodeDeserializers/YamlSerializableNodeDeserializer.cs @@ -33,7 +33,7 @@ public YamlSerializableNodeDeserializer(IObjectFactory objectFactory) this.objectFactory = objectFactory; } - public bool Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value) + public bool Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value, ObjectDeserializer rootDeserializer) { #pragma warning disable 0618 // IYamlSerializable is obsolete if (typeof(IYamlSerializable).IsAssignableFrom(expectedType)) diff --git a/YamlDotNet/Serialization/ObjectGraphTraversalStrategies/FullObjectGraphTraversalStrategy.cs b/YamlDotNet/Serialization/ObjectGraphTraversalStrategies/FullObjectGraphTraversalStrategy.cs index 491ed170b..8491b27bc 100644 --- a/YamlDotNet/Serialization/ObjectGraphTraversalStrategies/FullObjectGraphTraversalStrategy.cs +++ b/YamlDotNet/Serialization/ObjectGraphTraversalStrategies/FullObjectGraphTraversalStrategy.cs @@ -57,9 +57,9 @@ public FullObjectGraphTraversalStrategy(ITypeInspector typeDescriptor, ITypeReso this.objectFactory = objectFactory ?? throw new ArgumentNullException(nameof(objectFactory)); } - void IObjectGraphTraversalStrategy.Traverse(IObjectDescriptor graph, IObjectGraphVisitor visitor, TContext context) + void IObjectGraphTraversalStrategy.Traverse(IObjectDescriptor graph, IObjectGraphVisitor visitor, TContext context, ObjectSerializer serializer) { - Traverse("", graph, visitor, context, new Stack(maxRecursion)); + Traverse(null, "", graph, visitor, context, new Stack(maxRecursion), serializer); } protected struct ObjectPathSegment @@ -74,7 +74,7 @@ public ObjectPathSegment(object name, IObjectDescriptor value) } } - protected virtual void Traverse(object name, IObjectDescriptor value, IObjectGraphVisitor visitor, TContext context, Stack path) + protected virtual void Traverse(IPropertyDescriptor? propertyDescriptor, object name, IObjectDescriptor value, IObjectGraphVisitor visitor, TContext context, Stack path, ObjectSerializer serializer) { if (path.Count >= maxRecursion) { @@ -105,7 +105,7 @@ protected virtual void Traverse(object name, IObjectDescriptor value, } - if (!visitor.Enter(value, context)) + if (!visitor.Enter(propertyDescriptor, value, context, serializer)) { return; } @@ -131,7 +131,7 @@ protected virtual void Traverse(object name, IObjectDescriptor value, case TypeCode.String: case TypeCode.Char: case TypeCode.DateTime: - visitor.VisitScalar(value, context); + visitor.VisitScalar(value, context, serializer); break; case TypeCode.Empty: @@ -140,12 +140,12 @@ protected virtual void Traverse(object name, IObjectDescriptor value, default: if (value.IsDbNull()) { - visitor.VisitScalar(new ObjectDescriptor(null, typeof(object), typeof(object)), context); + visitor.VisitScalar(new ObjectDescriptor(null, typeof(object), typeof(object)), context, serializer); } if (value.Value == null || value.Type == typeof(TimeSpan)) { - visitor.VisitScalar(value, context); + visitor.VisitScalar(value, context, serializer); break; } @@ -158,26 +158,30 @@ protected virtual void Traverse(object name, IObjectDescriptor value, // This is a nullable type, recursively handle it with its underlying type. // Note that if it contains null, the condition above already took care of it Traverse( + propertyDescriptor, "Value", - new ObjectDescriptor(value.Value, nullableUnderlyingType, value.Type, value.ScalarStyle), + new ObjectDescriptor(value.Value, nullableUnderlyingType, value.Type, value.ScalarStyle), visitor, context, - path + path, + serializer ); } else if (optionUnderlyingType != null && optionValue != null) { Traverse( + propertyDescriptor, "Value", new ObjectDescriptor(FsharpHelper.GetValue(value), optionUnderlyingType, value.Type, value.ScalarStyle), visitor, context, - path + path, + serializer ); } else { - TraverseObject(value, visitor, context, path); + TraverseObject(propertyDescriptor, value, visitor, context, path, serializer); } break; } @@ -188,32 +192,32 @@ protected virtual void Traverse(object name, IObjectDescriptor value, } } - protected virtual void TraverseObject(IObjectDescriptor value, IObjectGraphVisitor visitor, TContext context, Stack path) + protected virtual void TraverseObject(IPropertyDescriptor? propertyDescriptor, IObjectDescriptor value, IObjectGraphVisitor visitor, TContext context, Stack path, ObjectSerializer serializer) { if (typeof(IDictionary).IsAssignableFrom(value.Type)) { - TraverseDictionary(value, visitor, typeof(object), typeof(object), context, path); + TraverseDictionary(propertyDescriptor, value, visitor, typeof(object), typeof(object), context, path, serializer); return; } if (objectFactory.GetDictionary(value, out var adaptedDictionary, out var genericArguments)) { - TraverseDictionary(new ObjectDescriptor(adaptedDictionary, value.Type, value.StaticType, value.ScalarStyle), visitor, genericArguments[0], genericArguments[1], context, path); + TraverseDictionary(propertyDescriptor, new ObjectDescriptor(adaptedDictionary, value.Type, value.StaticType, value.ScalarStyle), visitor, genericArguments[0], genericArguments[1], context, path, serializer); return; } if (typeof(IEnumerable).IsAssignableFrom(value.Type)) { - TraverseList(value, visitor, context, path); + TraverseList(propertyDescriptor, value, visitor, context, path, serializer); return; } - TraverseProperties(value, visitor, context, path); + TraverseProperties(value, visitor, context, path, serializer); } - protected virtual void TraverseDictionary(IObjectDescriptor dictionary, IObjectGraphVisitor visitor, Type keyType, Type valueType, TContext context, Stack path) + protected virtual void TraverseDictionary(IPropertyDescriptor? propertyDescriptor, IObjectDescriptor dictionary, IObjectGraphVisitor visitor, Type keyType, Type valueType, TContext context, Stack path, ObjectSerializer serializer) { - visitor.VisitMappingStart(dictionary, keyType, valueType, context); + visitor.VisitMappingStart(dictionary, keyType, valueType, context, serializer); var isDynamic = dictionary.Type.FullName!.Equals("System.Dynamic.ExpandoObject"); foreach (DictionaryEntry? entry in (IDictionary)dictionary.NonNullValue()) @@ -223,54 +227,54 @@ protected virtual void TraverseDictionary(IObjectDescriptor dictionary var key = GetObjectDescriptor(keyValue, keyType); var value = GetObjectDescriptor(entryValue.Value, valueType); - if (visitor.EnterMapping(key, value, context)) + if (visitor.EnterMapping(key, value, context, serializer)) { - Traverse(keyValue, key, visitor, context, path); - Traverse(keyValue, value, visitor, context, path); + Traverse(propertyDescriptor, keyValue, key, visitor, context, path, serializer); + Traverse(propertyDescriptor, keyValue, value, visitor, context, path, serializer); } } - visitor.VisitMappingEnd(dictionary, context); + visitor.VisitMappingEnd(dictionary, context, serializer); } - private void TraverseList(IObjectDescriptor value, IObjectGraphVisitor visitor, TContext context, Stack path) + private void TraverseList(IPropertyDescriptor propertyDescriptor, IObjectDescriptor value, IObjectGraphVisitor visitor, TContext context, Stack path, ObjectSerializer serializer) { var itemType = objectFactory.GetValueType(value.Type); - visitor.VisitSequenceStart(value, itemType, context); + visitor.VisitSequenceStart(value, itemType, context, serializer); var index = 0; foreach (var item in (IEnumerable)value.NonNullValue()) { - Traverse(index, GetObjectDescriptor(item, itemType), visitor, context, path); + Traverse(propertyDescriptor, index, GetObjectDescriptor(item, itemType), visitor, context, path, serializer); ++index; } - visitor.VisitSequenceEnd(value, context); + visitor.VisitSequenceEnd(value, context, serializer); } - protected virtual void TraverseProperties(IObjectDescriptor value, IObjectGraphVisitor visitor, TContext context, Stack path) + protected virtual void TraverseProperties(IObjectDescriptor value, IObjectGraphVisitor visitor, TContext context, Stack path, ObjectSerializer serializer) { if (context.GetType() != typeof(Nothing)) { objectFactory.ExecuteOnSerializing(value.Value); } - visitor.VisitMappingStart(value, typeof(string), typeof(object), context); + visitor.VisitMappingStart(value, typeof(string), typeof(object), context, serializer); var source = value.NonNullValue(); foreach (var propertyDescriptor in typeDescriptor.GetProperties(value.Type, source)) { var propertyValue = propertyDescriptor.Read(source); - if (visitor.EnterMapping(propertyDescriptor, propertyValue, context)) + if (visitor.EnterMapping(propertyDescriptor, propertyValue, context, serializer)) { - Traverse(propertyDescriptor.Name, new ObjectDescriptor(propertyDescriptor.Name, typeof(string), typeof(string), ScalarStyle.Plain), visitor, context, path); - Traverse(propertyDescriptor.Name, propertyValue, visitor, context, path); + Traverse(null, propertyDescriptor.Name, new ObjectDescriptor(propertyDescriptor.Name, typeof(string), typeof(string), ScalarStyle.Plain), visitor, context, path, serializer); + Traverse(propertyDescriptor, propertyDescriptor.Name, propertyValue, visitor, context, path, serializer); } } - visitor.VisitMappingEnd(value, context); + visitor.VisitMappingEnd(value, context, serializer); if (context.GetType() != typeof(Nothing)) { diff --git a/YamlDotNet/Serialization/ObjectGraphTraversalStrategies/RoundtripObjectGraphTraversalStrategy.cs b/YamlDotNet/Serialization/ObjectGraphTraversalStrategies/RoundtripObjectGraphTraversalStrategy.cs index 925ea8156..22e82354b 100644 --- a/YamlDotNet/Serialization/ObjectGraphTraversalStrategies/RoundtripObjectGraphTraversalStrategy.cs +++ b/YamlDotNet/Serialization/ObjectGraphTraversalStrategies/RoundtripObjectGraphTraversalStrategy.cs @@ -44,14 +44,14 @@ public RoundtripObjectGraphTraversalStrategy(IEnumerable con this.settings = settings; } - protected override void TraverseProperties(IObjectDescriptor value, IObjectGraphVisitor visitor, TContext context, Stack path) + protected override void TraverseProperties(IObjectDescriptor value, IObjectGraphVisitor visitor, TContext context, Stack path, ObjectSerializer serializer) { if (!value.Type.HasDefaultConstructor(settings.AllowPrivateConstructors) && !converters.Any(c => c.Accepts(value.Type))) { throw new InvalidOperationException($"Type '{value.Type}' cannot be deserialized because it does not have a default constructor or a type converter."); } - base.TraverseProperties(value, visitor, context, path); + base.TraverseProperties(value, visitor, context, path, serializer); } } } diff --git a/YamlDotNet/Serialization/ObjectGraphVisitors/AnchorAssigner.cs b/YamlDotNet/Serialization/ObjectGraphVisitors/AnchorAssigner.cs index 5c9105091..4f59ad808 100644 --- a/YamlDotNet/Serialization/ObjectGraphVisitors/AnchorAssigner.cs +++ b/YamlDotNet/Serialization/ObjectGraphVisitors/AnchorAssigner.cs @@ -41,7 +41,7 @@ public AnchorAssigner(IEnumerable typeConverters) { } - protected override bool Enter(IObjectDescriptor value) + protected override bool Enter(IObjectDescriptor value, ObjectSerializer serializer) { if (value.Value != null && assignments.TryGetValue(value.Value, out var assignment)) { @@ -56,34 +56,34 @@ protected override bool Enter(IObjectDescriptor value) return true; } - protected override bool EnterMapping(IObjectDescriptor key, IObjectDescriptor value) + protected override bool EnterMapping(IObjectDescriptor key, IObjectDescriptor value, ObjectSerializer serializer) { return true; } - protected override bool EnterMapping(IPropertyDescriptor key, IObjectDescriptor value) + protected override bool EnterMapping(IPropertyDescriptor key, IObjectDescriptor value, ObjectSerializer serializer) { return true; } - protected override void VisitScalar(IObjectDescriptor scalar) + protected override void VisitScalar(IObjectDescriptor scalar, ObjectSerializer serializer) { // Do not assign anchors to scalars } - protected override void VisitMappingStart(IObjectDescriptor mapping, Type keyType, Type valueType) + protected override void VisitMappingStart(IObjectDescriptor mapping, Type keyType, Type valueType, ObjectSerializer serializer) { VisitObject(mapping); } - protected override void VisitMappingEnd(IObjectDescriptor mapping) { } + protected override void VisitMappingEnd(IObjectDescriptor mapping, ObjectSerializer serializer) { } - protected override void VisitSequenceStart(IObjectDescriptor sequence, Type elementType) + protected override void VisitSequenceStart(IObjectDescriptor sequence, Type elementType, ObjectSerializer serializer) { VisitObject(sequence); } - protected override void VisitSequenceEnd(IObjectDescriptor sequence) { } + protected override void VisitSequenceEnd(IObjectDescriptor sequence, ObjectSerializer serializer) { } private void VisitObject(IObjectDescriptor value) { diff --git a/YamlDotNet/Serialization/ObjectGraphVisitors/AnchorAssigningObjectGraphVisitor.cs b/YamlDotNet/Serialization/ObjectGraphVisitors/AnchorAssigningObjectGraphVisitor.cs index 7882ac8b8..7c69470a2 100644 --- a/YamlDotNet/Serialization/ObjectGraphVisitors/AnchorAssigningObjectGraphVisitor.cs +++ b/YamlDotNet/Serialization/ObjectGraphVisitors/AnchorAssigningObjectGraphVisitor.cs @@ -38,7 +38,7 @@ public AnchorAssigningObjectGraphVisitor(IObjectGraphVisitor nextVisit this.aliasProvider = aliasProvider; } - public override bool Enter(IObjectDescriptor value, IEmitter context) + public override bool Enter(IPropertyDescriptor? propertyDescriptor, IObjectDescriptor value, IEmitter context, ObjectSerializer serializer) { if (value.Value != null) { @@ -50,22 +50,22 @@ public override bool Enter(IObjectDescriptor value, IEmitter context) return aliasEventInfo.NeedsExpansion; } } - return base.Enter(value, context); + return base.Enter(propertyDescriptor, value, context, serializer); } - public override void VisitMappingStart(IObjectDescriptor mapping, Type keyType, Type valueType, IEmitter context) + public override void VisitMappingStart(IObjectDescriptor mapping, Type keyType, Type valueType, IEmitter context, ObjectSerializer serializer) { var anchor = aliasProvider.GetAlias(mapping.NonNullValue()); eventEmitter.Emit(new MappingStartEventInfo(mapping) { Anchor = anchor }, context); } - public override void VisitSequenceStart(IObjectDescriptor sequence, Type elementType, IEmitter context) + public override void VisitSequenceStart(IObjectDescriptor sequence, Type elementType, IEmitter context, ObjectSerializer serializer) { var anchor = aliasProvider.GetAlias(sequence.NonNullValue()); eventEmitter.Emit(new SequenceStartEventInfo(sequence) { Anchor = anchor }, context); } - public override void VisitScalar(IObjectDescriptor scalar, IEmitter context) + public override void VisitScalar(IObjectDescriptor scalar, IEmitter context, ObjectSerializer serializer) { var scalarInfo = new ScalarEventInfo(scalar); if (scalar.Value != null) diff --git a/YamlDotNet/Serialization/ObjectGraphVisitors/ChainedObjectGraphVisitor.cs b/YamlDotNet/Serialization/ObjectGraphVisitors/ChainedObjectGraphVisitor.cs index 231a78077..9ed0b2aba 100644 --- a/YamlDotNet/Serialization/ObjectGraphVisitors/ChainedObjectGraphVisitor.cs +++ b/YamlDotNet/Serialization/ObjectGraphVisitors/ChainedObjectGraphVisitor.cs @@ -33,44 +33,44 @@ protected ChainedObjectGraphVisitor(IObjectGraphVisitor nextVisitor) this.nextVisitor = nextVisitor; } - public virtual bool Enter(IObjectDescriptor value, IEmitter context) + public virtual bool Enter(IPropertyDescriptor? propertyDescriptor, IObjectDescriptor value, IEmitter context, ObjectSerializer serializer) { - return nextVisitor.Enter(value, context); + return nextVisitor.Enter(propertyDescriptor, value, context, serializer); } - public virtual bool EnterMapping(IObjectDescriptor key, IObjectDescriptor value, IEmitter context) + public virtual bool EnterMapping(IObjectDescriptor key, IObjectDescriptor value, IEmitter context, ObjectSerializer serializer) { - return nextVisitor.EnterMapping(key, value, context); + return nextVisitor.EnterMapping(key, value, context, serializer); } - public virtual bool EnterMapping(IPropertyDescriptor key, IObjectDescriptor value, IEmitter context) + public virtual bool EnterMapping(IPropertyDescriptor key, IObjectDescriptor value, IEmitter context, ObjectSerializer serializer) { - return nextVisitor.EnterMapping(key, value, context); + return nextVisitor.EnterMapping(key, value, context, serializer); } - public virtual void VisitScalar(IObjectDescriptor scalar, IEmitter context) + public virtual void VisitScalar(IObjectDescriptor scalar, IEmitter context, ObjectSerializer serializer) { - nextVisitor.VisitScalar(scalar, context); + nextVisitor.VisitScalar(scalar, context, serializer); } - public virtual void VisitMappingStart(IObjectDescriptor mapping, Type keyType, Type valueType, IEmitter context) + public virtual void VisitMappingStart(IObjectDescriptor mapping, Type keyType, Type valueType, IEmitter context, ObjectSerializer serializer) { - nextVisitor.VisitMappingStart(mapping, keyType, valueType, context); + nextVisitor.VisitMappingStart(mapping, keyType, valueType, context, serializer); } - public virtual void VisitMappingEnd(IObjectDescriptor mapping, IEmitter context) + public virtual void VisitMappingEnd(IObjectDescriptor mapping, IEmitter context, ObjectSerializer serializer) { - nextVisitor.VisitMappingEnd(mapping, context); + nextVisitor.VisitMappingEnd(mapping, context, serializer); } - public virtual void VisitSequenceStart(IObjectDescriptor sequence, Type elementType, IEmitter context) + public virtual void VisitSequenceStart(IObjectDescriptor sequence, Type elementType, IEmitter context, ObjectSerializer serializer) { - nextVisitor.VisitSequenceStart(sequence, elementType, context); + nextVisitor.VisitSequenceStart(sequence, elementType, context, serializer); } - public virtual void VisitSequenceEnd(IObjectDescriptor sequence, IEmitter context) + public virtual void VisitSequenceEnd(IObjectDescriptor sequence, IEmitter context, ObjectSerializer serializer) { - nextVisitor.VisitSequenceEnd(sequence, context); + nextVisitor.VisitSequenceEnd(sequence, context, serializer); } } } diff --git a/YamlDotNet/Serialization/ObjectGraphVisitors/CommentsObjectGraphVisitor.cs b/YamlDotNet/Serialization/ObjectGraphVisitors/CommentsObjectGraphVisitor.cs index c5e9f57f3..072781a72 100644 --- a/YamlDotNet/Serialization/ObjectGraphVisitors/CommentsObjectGraphVisitor.cs +++ b/YamlDotNet/Serialization/ObjectGraphVisitors/CommentsObjectGraphVisitor.cs @@ -30,7 +30,7 @@ public CommentsObjectGraphVisitor(IObjectGraphVisitor nextVisitor) { } - public override bool EnterMapping(IPropertyDescriptor key, IObjectDescriptor value, IEmitter context) + public override bool EnterMapping(IPropertyDescriptor key, IObjectDescriptor value, IEmitter context, ObjectSerializer serializer) { var yamlMember = key.GetCustomAttribute(); if (yamlMember?.Description != null) @@ -38,7 +38,7 @@ public override bool EnterMapping(IPropertyDescriptor key, IObjectDescriptor val context.Emit(new Core.Events.Comment(yamlMember.Description, false)); } - return base.EnterMapping(key, value, context); + return base.EnterMapping(key, value, context, serializer); } } } diff --git a/YamlDotNet/Serialization/ObjectGraphVisitors/CustomSerializationObjectGraphVisitor.cs b/YamlDotNet/Serialization/ObjectGraphVisitors/CustomSerializationObjectGraphVisitor.cs index 56e3bf6ad..81856e8bf 100644 --- a/YamlDotNet/Serialization/ObjectGraphVisitors/CustomSerializationObjectGraphVisitor.cs +++ b/YamlDotNet/Serialization/ObjectGraphVisitors/CustomSerializationObjectGraphVisitor.cs @@ -19,6 +19,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +using System; using System.Collections.Generic; using System.Linq; using YamlDotNet.Core; @@ -40,12 +41,20 @@ public CustomSerializationObjectGraphVisitor(IObjectGraphVisitor nextV this.nestedObjectSerializer = nestedObjectSerializer; } - public override bool Enter(IObjectDescriptor value, IEmitter context) + public override bool Enter(IPropertyDescriptor? propertyDescriptor, IObjectDescriptor value, IEmitter context, ObjectSerializer serializer) { + //propertydescriptor will be null on the root graph object + if (propertyDescriptor?.ConverterType != null) + { + var converter = typeConverters.Single(x => x.GetType() == propertyDescriptor.ConverterType); + converter.WriteYaml(context, value.Value, value.Type, serializer); + return false; + } + var typeConverter = typeConverters.FirstOrDefault(t => t.Accepts(value.Type)); if (typeConverter != null) { - typeConverter.WriteYaml(context, value.Value, value.Type); + typeConverter.WriteYaml(context, value.Value, value.Type, serializer); return false; } @@ -63,7 +72,7 @@ public override bool Enter(IObjectDescriptor value, IEmitter context) } #pragma warning restore - return base.Enter(value, context); + return base.Enter(propertyDescriptor, value, context, serializer); } } } diff --git a/YamlDotNet/Serialization/ObjectGraphVisitors/DefaultExclusiveObjectGraphVisitor.cs b/YamlDotNet/Serialization/ObjectGraphVisitors/DefaultExclusiveObjectGraphVisitor.cs index 91acd03f0..3acfd5eb6 100644 --- a/YamlDotNet/Serialization/ObjectGraphVisitors/DefaultExclusiveObjectGraphVisitor.cs +++ b/YamlDotNet/Serialization/ObjectGraphVisitors/DefaultExclusiveObjectGraphVisitor.cs @@ -38,13 +38,13 @@ public DefaultExclusiveObjectGraphVisitor(IObjectGraphVisitor nextVisi return type.IsValueType() ? Activator.CreateInstance(type) : null; } - public override bool EnterMapping(IObjectDescriptor key, IObjectDescriptor value, IEmitter context) + public override bool EnterMapping(IObjectDescriptor key, IObjectDescriptor value, IEmitter context, ObjectSerializer serializer) { return !Equals(value.Value, GetDefault(value.Type)) - && base.EnterMapping(key, value, context); + && base.EnterMapping(key, value, context, serializer); } - public override bool EnterMapping(IPropertyDescriptor key, IObjectDescriptor value, IEmitter context) + public override bool EnterMapping(IPropertyDescriptor key, IObjectDescriptor value, IEmitter context, ObjectSerializer serializer) { var defaultValueAttribute = key.GetCustomAttribute(); var defaultValue = defaultValueAttribute != null @@ -52,7 +52,7 @@ public override bool EnterMapping(IPropertyDescriptor key, IObjectDescriptor val : GetDefault(key.Type); return !Equals(value.Value, defaultValue) - && base.EnterMapping(key, value, context); + && base.EnterMapping(key, value, context, serializer); } } } diff --git a/YamlDotNet/Serialization/ObjectGraphVisitors/DefaultValuesObjectGraphVisitor.cs b/YamlDotNet/Serialization/ObjectGraphVisitors/DefaultValuesObjectGraphVisitor.cs index 6974a5996..0e4f1d86c 100644 --- a/YamlDotNet/Serialization/ObjectGraphVisitors/DefaultValuesObjectGraphVisitor.cs +++ b/YamlDotNet/Serialization/ObjectGraphVisitors/DefaultValuesObjectGraphVisitor.cs @@ -40,7 +40,7 @@ public DefaultValuesObjectGraphVisitor(DefaultValuesHandling handling, IObjectGr private object? GetDefault(Type type) => factory.CreatePrimitive(type); - public override bool EnterMapping(IPropertyDescriptor key, IObjectDescriptor value, IEmitter context) + public override bool EnterMapping(IPropertyDescriptor key, IObjectDescriptor value, IEmitter context, ObjectSerializer serializer) { var configuration = handling; var yamlMember = key.GetCustomAttribute(); @@ -84,7 +84,7 @@ public override bool EnterMapping(IPropertyDescriptor key, IObjectDescriptor val } } - return base.EnterMapping(key, value, context); + return base.EnterMapping(key, value, context, serializer); } } } diff --git a/YamlDotNet/Serialization/ObjectGraphVisitors/EmittingObjectGraphVisitor.cs b/YamlDotNet/Serialization/ObjectGraphVisitors/EmittingObjectGraphVisitor.cs index 76dfa22d3..35036ee15 100644 --- a/YamlDotNet/Serialization/ObjectGraphVisitors/EmittingObjectGraphVisitor.cs +++ b/YamlDotNet/Serialization/ObjectGraphVisitors/EmittingObjectGraphVisitor.cs @@ -33,42 +33,42 @@ public EmittingObjectGraphVisitor(IEventEmitter eventEmitter) this.eventEmitter = eventEmitter; } - bool IObjectGraphVisitor.Enter(IObjectDescriptor value, IEmitter context) + bool IObjectGraphVisitor.Enter(IPropertyDescriptor? propertyDescriptor, IObjectDescriptor value, IEmitter context, ObjectSerializer serializer) { return true; } - bool IObjectGraphVisitor.EnterMapping(IObjectDescriptor key, IObjectDescriptor value, IEmitter context) + bool IObjectGraphVisitor.EnterMapping(IObjectDescriptor key, IObjectDescriptor value, IEmitter context, ObjectSerializer serializer) { return true; } - bool IObjectGraphVisitor.EnterMapping(IPropertyDescriptor key, IObjectDescriptor value, IEmitter context) + bool IObjectGraphVisitor.EnterMapping(IPropertyDescriptor key, IObjectDescriptor value, IEmitter context, ObjectSerializer serializer) { return true; } - void IObjectGraphVisitor.VisitScalar(IObjectDescriptor scalar, IEmitter context) + void IObjectGraphVisitor.VisitScalar(IObjectDescriptor scalar, IEmitter context, ObjectSerializer serializer) { eventEmitter.Emit(new ScalarEventInfo(scalar), context); } - void IObjectGraphVisitor.VisitMappingStart(IObjectDescriptor mapping, Type keyType, Type valueType, IEmitter context) + void IObjectGraphVisitor.VisitMappingStart(IObjectDescriptor mapping, Type keyType, Type valueType, IEmitter context, ObjectSerializer serializer) { eventEmitter.Emit(new MappingStartEventInfo(mapping), context); } - void IObjectGraphVisitor.VisitMappingEnd(IObjectDescriptor mapping, IEmitter context) + void IObjectGraphVisitor.VisitMappingEnd(IObjectDescriptor mapping, IEmitter context, ObjectSerializer serializer) { eventEmitter.Emit(new MappingEndEventInfo(mapping), context); } - void IObjectGraphVisitor.VisitSequenceStart(IObjectDescriptor sequence, Type elementType, IEmitter context) + void IObjectGraphVisitor.VisitSequenceStart(IObjectDescriptor sequence, Type elementType, IEmitter context, ObjectSerializer serializer) { eventEmitter.Emit(new SequenceStartEventInfo(sequence), context); } - void IObjectGraphVisitor.VisitSequenceEnd(IObjectDescriptor sequence, IEmitter context) + void IObjectGraphVisitor.VisitSequenceEnd(IObjectDescriptor sequence, IEmitter context, ObjectSerializer serializer) { eventEmitter.Emit(new SequenceEndEventInfo(sequence), context); } diff --git a/YamlDotNet/Serialization/ObjectGraphVisitors/PreProcessingPhaseObjectGraphVisitorSkeleton.cs b/YamlDotNet/Serialization/ObjectGraphVisitors/PreProcessingPhaseObjectGraphVisitorSkeleton.cs index 619de4da2..82255a5cb 100644 --- a/YamlDotNet/Serialization/ObjectGraphVisitors/PreProcessingPhaseObjectGraphVisitorSkeleton.cs +++ b/YamlDotNet/Serialization/ObjectGraphVisitors/PreProcessingPhaseObjectGraphVisitorSkeleton.cs @@ -39,7 +39,7 @@ public PreProcessingPhaseObjectGraphVisitorSkeleton(IEnumerable(); } - bool IObjectGraphVisitor.Enter(IObjectDescriptor value, Nothing context) + bool IObjectGraphVisitor.Enter(IPropertyDescriptor? propertyDescriptor, IObjectDescriptor value, Nothing context, ObjectSerializer serializer) { var typeConverter = typeConverters.FirstOrDefault(t => t.Accepts(value.Type)); if (typeConverter != null) @@ -59,51 +59,51 @@ bool IObjectGraphVisitor.Enter(IObjectDescriptor value, Nothing context } #pragma warning restore - return Enter(value); + return Enter(value, serializer); } - bool IObjectGraphVisitor.EnterMapping(IPropertyDescriptor key, IObjectDescriptor value, Nothing context) + bool IObjectGraphVisitor.EnterMapping(IPropertyDescriptor key, IObjectDescriptor value, Nothing context, ObjectSerializer serializer) { - return EnterMapping(key, value); + return EnterMapping(key, value, serializer); } - bool IObjectGraphVisitor.EnterMapping(IObjectDescriptor key, IObjectDescriptor value, Nothing context) + bool IObjectGraphVisitor.EnterMapping(IObjectDescriptor key, IObjectDescriptor value, Nothing context, ObjectSerializer serializer) { - return EnterMapping(key, value); + return EnterMapping(key, value, serializer); } - void IObjectGraphVisitor.VisitMappingEnd(IObjectDescriptor mapping, Nothing context) + void IObjectGraphVisitor.VisitMappingEnd(IObjectDescriptor mapping, Nothing context, ObjectSerializer serializer) { - VisitMappingEnd(mapping); + VisitMappingEnd(mapping, serializer); } - void IObjectGraphVisitor.VisitMappingStart(IObjectDescriptor mapping, Type keyType, Type valueType, Nothing context) + void IObjectGraphVisitor.VisitMappingStart(IObjectDescriptor mapping, Type keyType, Type valueType, Nothing context, ObjectSerializer serializer) { - VisitMappingStart(mapping, keyType, valueType); + VisitMappingStart(mapping, keyType, valueType, serializer); } - void IObjectGraphVisitor.VisitScalar(IObjectDescriptor scalar, Nothing context) + void IObjectGraphVisitor.VisitScalar(IObjectDescriptor scalar, Nothing context, ObjectSerializer serializer) { - VisitScalar(scalar); + VisitScalar(scalar, serializer); } - void IObjectGraphVisitor.VisitSequenceEnd(IObjectDescriptor sequence, Nothing context) + void IObjectGraphVisitor.VisitSequenceEnd(IObjectDescriptor sequence, Nothing context, ObjectSerializer serializer) { - VisitSequenceEnd(sequence); + VisitSequenceEnd(sequence, serializer); } - void IObjectGraphVisitor.VisitSequenceStart(IObjectDescriptor sequence, Type elementType, Nothing context) + void IObjectGraphVisitor.VisitSequenceStart(IObjectDescriptor sequence, Type elementType, Nothing context, ObjectSerializer serializer) { - VisitSequenceStart(sequence, elementType); + VisitSequenceStart(sequence, elementType, serializer); } - protected abstract bool Enter(IObjectDescriptor value); - protected abstract bool EnterMapping(IPropertyDescriptor key, IObjectDescriptor value); - protected abstract bool EnterMapping(IObjectDescriptor key, IObjectDescriptor value); - protected abstract void VisitMappingEnd(IObjectDescriptor mapping); - protected abstract void VisitMappingStart(IObjectDescriptor mapping, Type keyType, Type valueType); - protected abstract void VisitScalar(IObjectDescriptor scalar); - protected abstract void VisitSequenceEnd(IObjectDescriptor sequence); - protected abstract void VisitSequenceStart(IObjectDescriptor sequence, Type elementType); + protected abstract bool Enter(IObjectDescriptor value, ObjectSerializer serializer); + protected abstract bool EnterMapping(IPropertyDescriptor key, IObjectDescriptor value, ObjectSerializer serializer); + protected abstract bool EnterMapping(IObjectDescriptor key, IObjectDescriptor value, ObjectSerializer serializer); + protected abstract void VisitMappingEnd(IObjectDescriptor mapping, ObjectSerializer serializer); + protected abstract void VisitMappingStart(IObjectDescriptor mapping, Type keyType, Type valueType, ObjectSerializer serializer); + protected abstract void VisitScalar(IObjectDescriptor scalar, ObjectSerializer serializer); + protected abstract void VisitSequenceEnd(IObjectDescriptor sequence, ObjectSerializer serializer); + protected abstract void VisitSequenceStart(IObjectDescriptor sequence, Type elementType, ObjectSerializer serializer); } } diff --git a/YamlDotNet/Serialization/PropertyDescriptor.cs b/YamlDotNet/Serialization/PropertyDescriptor.cs index 32b82cceb..39906fc3b 100644 --- a/YamlDotNet/Serialization/PropertyDescriptor.cs +++ b/YamlDotNet/Serialization/PropertyDescriptor.cs @@ -27,15 +27,24 @@ namespace YamlDotNet.Serialization public sealed class PropertyDescriptor : IPropertyDescriptor { private readonly IPropertyDescriptor baseDescriptor; - public PropertyDescriptor(IPropertyDescriptor baseDescriptor) { this.baseDescriptor = baseDescriptor; Name = baseDescriptor.Name; } + public bool AllowNulls + { + get + { + return baseDescriptor.AllowNulls; + } + } + public string Name { get; set; } + public bool Required { get => baseDescriptor.Required; } + public Type Type { get { return baseDescriptor.Type; } } public Type? TypeOverride @@ -44,6 +53,8 @@ public Type? TypeOverride set { baseDescriptor.TypeOverride = value; } } + public Type? ConverterType => baseDescriptor.ConverterType; + public int Order { get; set; } public ScalarStyle ScalarStyle diff --git a/YamlDotNet/Serialization/SerializerBuilder.cs b/YamlDotNet/Serialization/SerializerBuilder.cs index 74b927f69..73974f38f 100755 --- a/YamlDotNet/Serialization/SerializerBuilder.cs +++ b/YamlDotNet/Serialization/SerializerBuilder.cs @@ -106,7 +106,8 @@ public SerializerBuilder() quoteYaml1_1Strings, defaultScalarStyle, yamlFormatter, - enumNamingConvention) + enumNamingConvention, + BuildTypeInspector()) } }; @@ -157,10 +158,14 @@ public SerializerBuilder WithMaximumRecursion(int maximumRecursion) /// /// A function that instantiates the event emitter. public SerializerBuilder WithEventEmitter(Func eventEmitterFactory) - where TEventEmitter : IEventEmitter - { - return WithEventEmitter(eventEmitterFactory, w => w.OnTop()); - } + where TEventEmitter : IEventEmitter => WithEventEmitter(eventEmitterFactory, w => w.OnTop()); + + /// + /// Registers an additional to be used by the serializer. + /// + /// A function that instantiates the event emitter. + public SerializerBuilder WithEventEmitter(Func eventEmitterFactory) + where TEventEmitter : IEventEmitter => WithEventEmitter(eventEmitterFactory, w => w.OnTop()); /// /// Registers an additional to be used by the serializer. @@ -170,6 +175,16 @@ public SerializerBuilder WithEventEmitter(Func( Func eventEmitterFactory, Action> where + ) where TEventEmitter : IEventEmitter => WithEventEmitter((IEventEmitter e, ITypeInspector _) => eventEmitterFactory(e), where); + + /// + /// Registers an additional to be used by the serializer. + /// + /// A function that instantiates the event emitter. + /// Configures the location where to insert the + public SerializerBuilder WithEventEmitter( + Func eventEmitterFactory, + Action> where ) where TEventEmitter : IEventEmitter { @@ -183,10 +198,11 @@ Action> where throw new ArgumentNullException(nameof(where)); } - where(eventEmitterFactories.CreateRegistrationLocationSelector(typeof(TEventEmitter), inner => eventEmitterFactory(inner))); + where(eventEmitterFactories.CreateRegistrationLocationSelector(typeof(TEventEmitter), inner => eventEmitterFactory(inner, BuildTypeInspector()))); return Self; } + /// /// Registers an additional to be used by the serializer. /// @@ -299,7 +315,8 @@ public SerializerBuilder EnsureRoundtrip() quoteYaml1_1Strings, defaultScalarStyle, yamlFormatter, - enumNamingConvention), loc => loc.InsteadOf()); + enumNamingConvention, + BuildTypeInspector()), loc => loc.InsteadOf()); return WithTypeInspector(inner => new ReadableAndWritablePropertiesTypeInspector(inner), loc => loc.OnBottom()); } @@ -352,12 +369,12 @@ public SerializerBuilder JsonCompatible() return this .WithTypeConverter(new GuidConverter(true), w => w.InsteadOf()) - .WithTypeConverter(new DateTimeConverter(doubleQuotes: true)) + .WithTypeConverter(new DateTime8601Converter(ScalarStyle.DoubleQuoted)) #if NET6_0_OR_GREATER .WithTypeConverter(new DateOnlyConverter(doubleQuotes: true)) .WithTypeConverter(new TimeOnlyConverter(doubleQuotes: true)) #endif - .WithEventEmitter(inner => new JsonEventEmitter(inner, yamlFormatter, enumNamingConvention), loc => loc.InsteadOf()); + .WithEventEmitter(inner => new JsonEventEmitter(inner, yamlFormatter, enumNamingConvention, BuildTypeInspector()), loc => loc.InsteadOf()); } /// @@ -693,7 +710,11 @@ public IValueSerializer BuildValueSerializer() ); } - internal ITypeInspector BuildTypeInspector() + /// + /// Builds the type inspector used by various classes to get information about types and their members. + /// + /// + public ITypeInspector BuildTypeInspector() { ITypeInspector innerInspector = new ReadablePropertiesTypeInspector(typeResolver, includeNonPublicProperties); @@ -735,14 +756,9 @@ public void SerializeValue(IEmitter emitter, object? value, Type? type) { var actualType = type ?? (value != null ? value.GetType() : typeof(object)); var staticType = type ?? typeof(object); - var graph = new ObjectDescriptor(value, actualType, staticType); var preProcessingPhaseObjectGraphVisitors = preProcessingPhaseObjectGraphVisitorFactories.BuildComponentList(typeConverters); - foreach (var visitor in preProcessingPhaseObjectGraphVisitors) - { - traversalStrategy.Traverse(graph, visitor, default); - } void NestedObjectSerializer(object? v, Type? t) => SerializeValue(emitter, v, t); @@ -751,7 +767,12 @@ public void SerializeValue(IEmitter emitter, object? value, Type? type) inner => new EmissionPhaseObjectGraphVisitorArgs(inner, eventEmitter, preProcessingPhaseObjectGraphVisitors, typeConverters, NestedObjectSerializer) ); - traversalStrategy.Traverse(graph, emittingVisitor, emitter); + foreach (var visitor in preProcessingPhaseObjectGraphVisitors) + { + traversalStrategy.Traverse(graph, visitor, default, NestedObjectSerializer); + } + + traversalStrategy.Traverse(graph, emittingVisitor, emitter, NestedObjectSerializer); } } } diff --git a/YamlDotNet/Serialization/StaticDeserializerBuilder.cs b/YamlDotNet/Serialization/StaticDeserializerBuilder.cs index e50120232..d09d0012b 100644 --- a/YamlDotNet/Serialization/StaticDeserializerBuilder.cs +++ b/YamlDotNet/Serialization/StaticDeserializerBuilder.cs @@ -21,6 +21,8 @@ using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; + #if NET7_0_OR_GREATER using System.Diagnostics.CodeAnalysis; #endif @@ -56,6 +58,8 @@ public sealed class StaticDeserializerBuilder : StaticBuilderSkeleton /// Initializes a new using the default component registrations. @@ -87,11 +91,21 @@ public StaticDeserializerBuilder(StaticContext context) { typeof(YamlSerializableNodeDeserializer), _ => new YamlSerializableNodeDeserializer(factory) }, { typeof(TypeConverterNodeDeserializer), _ => new TypeConverterNodeDeserializer(BuildTypeConverters()) }, { typeof(NullNodeDeserializer), _ => new NullNodeDeserializer() }, - { typeof(ScalarNodeDeserializer), _ => new ScalarNodeDeserializer(attemptUnknownTypeDeserialization, typeConverter, yamlFormatter, enumNamingConvention) }, + { typeof(ScalarNodeDeserializer), _ => new ScalarNodeDeserializer(attemptUnknownTypeDeserialization, typeConverter, BuildTypeInspector(), yamlFormatter, enumNamingConvention) }, { typeof(StaticArrayNodeDeserializer), _ => new StaticArrayNodeDeserializer(factory) }, { typeof(StaticDictionaryNodeDeserializer), _ => new StaticDictionaryNodeDeserializer(factory, duplicateKeyChecking) }, { typeof(StaticCollectionNodeDeserializer), _ => new StaticCollectionNodeDeserializer(factory) }, - { typeof(ObjectNodeDeserializer), _ => new ObjectNodeDeserializer(factory, BuildTypeInspector(), ignoreUnmatched, duplicateKeyChecking, typeConverter, enumNamingConvention) }, + { typeof(ObjectNodeDeserializer), _ => new ObjectNodeDeserializer(factory, + BuildTypeInspector(), + ignoreUnmatched, + duplicateKeyChecking, + typeConverter, + enumNamingConvention, + enforceNullability, + caseInsensitivePropertyMatching, + false, // the static builder doesn't support required attributes + BuildTypeConverters()) + }, }; nodeTypeResolverFactories = new LazyComponentRegistrationList @@ -109,7 +123,11 @@ public StaticDeserializerBuilder(StaticContext context) protected override StaticDeserializerBuilder Self { get { return this; } } - internal ITypeInspector BuildTypeInspector() + /// + /// Builds the type inspector used by various classes to get information about types and their members. + /// + /// + public ITypeInspector BuildTypeInspector() { ITypeInspector innerInspector = context.GetTypeInspector(); @@ -183,6 +201,26 @@ Action> where return this; } + /// + /// Ignore case when matching property names. + /// + /// + public StaticDeserializerBuilder WithCaseInsensitivePropertyMatching() + { + caseInsensitivePropertyMatching = true; + return this; + } + + /// + /// Enforce whether null values can be set on non-nullable properties and fields. + /// + /// This static deserializer builder. + public StaticDeserializerBuilder WithEnforceNullability() + { + enforceNullability = true; + return this; + } + /// /// Unregisters an existing of type . /// @@ -414,7 +452,8 @@ public IValueDeserializer BuildValueDeserializer() nodeDeserializerFactories.BuildComponentList(), nodeTypeResolverFactories.BuildComponentList(), typeConverter, - enumNamingConvention + enumNamingConvention, + BuildTypeInspector() ) ); } diff --git a/YamlDotNet/Serialization/StaticSerializerBuilder.cs b/YamlDotNet/Serialization/StaticSerializerBuilder.cs index a0de9755c..296bfa131 100644 --- a/YamlDotNet/Serialization/StaticSerializerBuilder.cs +++ b/YamlDotNet/Serialization/StaticSerializerBuilder.cs @@ -105,7 +105,8 @@ public StaticSerializerBuilder(StaticContext context) quoteYaml1_1Strings, defaultScalarStyle, yamlFormatter, - enumNamingConvention) + enumNamingConvention, + BuildTypeInspector()) } }; @@ -163,10 +164,14 @@ public StaticSerializerBuilder WithMaximumRecursion(int maximumRecursion) /// /// A function that instantiates the event emitter. public StaticSerializerBuilder WithEventEmitter(Func eventEmitterFactory) - where TEventEmitter : IEventEmitter - { - return WithEventEmitter(eventEmitterFactory, w => w.OnTop()); - } + where TEventEmitter : IEventEmitter => WithEventEmitter(eventEmitterFactory, w => w.OnTop()); + + /// + /// Registers an additional to be used by the serializer. + /// + /// A function that instantiates the event emitter. + public StaticSerializerBuilder WithEventEmitter(Func eventEmitterFactory) + where TEventEmitter : IEventEmitter => WithEventEmitter(eventEmitterFactory, w => w.OnTop()); /// /// Registers an additional to be used by the serializer. @@ -176,6 +181,16 @@ public StaticSerializerBuilder WithEventEmitter(Func( Func eventEmitterFactory, Action> where + ) where TEventEmitter : IEventEmitter => WithEventEmitter((IEventEmitter e, ITypeInspector _) => eventEmitterFactory(e), where); + + /// + /// Registers an additional to be used by the serializer. + /// + /// A function that instantiates the event emitter. + /// Configures the location where to insert the + public StaticSerializerBuilder WithEventEmitter( + Func eventEmitterFactory, + Action> where ) where TEventEmitter : IEventEmitter { @@ -189,10 +204,11 @@ Action> where throw new ArgumentNullException(nameof(where)); } - where(eventEmitterFactories.CreateRegistrationLocationSelector(typeof(TEventEmitter), inner => eventEmitterFactory(inner))); + where(eventEmitterFactories.CreateRegistrationLocationSelector(typeof(TEventEmitter), inner => eventEmitterFactory(inner, BuildTypeInspector()))); return Self; } + /// /// Registers an additional to be used by the serializer. /// @@ -218,6 +234,7 @@ Action> where return Self; } + /// /// Unregisters an existing of type . /// @@ -303,7 +320,8 @@ public StaticSerializerBuilder EnsureRoundtrip() false, ScalarStyle.Plain, YamlFormatter.Default, - enumNamingConvention + enumNamingConvention, + BuildTypeInspector() ), loc => loc.InsteadOf()); return WithTypeInspector(inner => new ReadableAndWritablePropertiesTypeInspector(inner), loc => loc.OnBottom()); } @@ -356,12 +374,12 @@ public StaticSerializerBuilder JsonCompatible() return this .WithTypeConverter(new GuidConverter(true), w => w.InsteadOf()) - .WithTypeConverter(new DateTimeConverter(doubleQuotes: true)) + .WithTypeConverter(new DateTime8601Converter(ScalarStyle.DoubleQuoted)) #if NET6_0_OR_GREATER .WithTypeConverter(new DateOnlyConverter(doubleQuotes: true)) .WithTypeConverter(new TimeOnlyConverter(doubleQuotes: true)) #endif - .WithEventEmitter(inner => new JsonEventEmitter(inner, yamlFormatter, enumNamingConvention), loc => loc.InsteadOf()); + .WithEventEmitter(inner => new JsonEventEmitter(inner, yamlFormatter, enumNamingConvention, BuildTypeInspector()), loc => loc.InsteadOf()); } /// @@ -696,7 +714,11 @@ public IValueSerializer BuildValueSerializer() ); } - internal ITypeInspector BuildTypeInspector() + /// + /// Builds the type inspector used by various classes to get information about types and their members. + /// + /// + public ITypeInspector BuildTypeInspector() { var typeInspector = context.GetTypeInspector(); @@ -733,20 +755,21 @@ public void SerializeValue(IEmitter emitter, object? value, Type? type) var graph = new ObjectDescriptor(value, actualType, staticType); + void NestedObjectSerializer(object? v, Type? t) => SerializeValue(emitter, v, t); + var preProcessingPhaseObjectGraphVisitors = preProcessingPhaseObjectGraphVisitorFactories.BuildComponentList(typeConverters); foreach (var visitor in preProcessingPhaseObjectGraphVisitors) { - traversalStrategy.Traverse(graph, visitor, default); + traversalStrategy.Traverse(graph, visitor, default, NestedObjectSerializer); } - void NestedObjectSerializer(object? v, Type? t) => SerializeValue(emitter, v, t); var emittingVisitor = emissionPhaseObjectGraphVisitorFactories.BuildComponentChain( new EmittingObjectGraphVisitor(eventEmitter), inner => new EmissionPhaseObjectGraphVisitorArgs(inner, eventEmitter, preProcessingPhaseObjectGraphVisitors, typeConverters, NestedObjectSerializer) ); - traversalStrategy.Traverse(graph, emittingVisitor, emitter); + traversalStrategy.Traverse(graph, emittingVisitor, emitter, NestedObjectSerializer); } } } diff --git a/YamlDotNet/Serialization/TypeInspectors/CachedTypeInspector.cs b/YamlDotNet/Serialization/TypeInspectors/CachedTypeInspector.cs index 727b8f193..1985b449d 100644 --- a/YamlDotNet/Serialization/TypeInspectors/CachedTypeInspector.cs +++ b/YamlDotNet/Serialization/TypeInspectors/CachedTypeInspector.cs @@ -33,12 +33,27 @@ public sealed class CachedTypeInspector : TypeInspectorSkeleton { private readonly ITypeInspector innerTypeDescriptor; private readonly ConcurrentDictionary> cache = new ConcurrentDictionary>(); + private readonly ConcurrentDictionary> enumNameCache = new ConcurrentDictionary>(); + private readonly ConcurrentDictionary enumValueCache = new ConcurrentDictionary(); public CachedTypeInspector(ITypeInspector innerTypeDescriptor) { this.innerTypeDescriptor = innerTypeDescriptor ?? throw new ArgumentNullException(nameof(innerTypeDescriptor)); } + public override string GetEnumName(Type enumType, string name) + { + var cache = enumNameCache.GetOrAdd(enumType, _ => new ConcurrentDictionary()); + var result = cache.GetOrAdd(name, _ => innerTypeDescriptor.GetEnumName(enumType, name)); + return result; + } + + public override string GetEnumValue(object enumValue) + { + var result = enumValueCache.GetOrAdd(enumValue, _ => this.innerTypeDescriptor.GetEnumValue(enumValue)); + return result; + } + public override IEnumerable GetProperties(Type type, object? container) { return cache.GetOrAdd(type, t => innerTypeDescriptor.GetProperties(t, container).ToList()); diff --git a/YamlDotNet/Serialization/TypeInspectors/CompositeTypeInspector.cs b/YamlDotNet/Serialization/TypeInspectors/CompositeTypeInspector.cs index 825cc9953..af366e696 100644 --- a/YamlDotNet/Serialization/TypeInspectors/CompositeTypeInspector.cs +++ b/YamlDotNet/Serialization/TypeInspectors/CompositeTypeInspector.cs @@ -42,6 +42,45 @@ public CompositeTypeInspector(IEnumerable typeInspectors) this.typeInspectors = typeInspectors?.ToList() ?? throw new ArgumentNullException(nameof(typeInspectors)); } + public override string GetEnumName(Type enumType, string name) + { + foreach (var inspector in typeInspectors) + { + try + { + return inspector.GetEnumName(enumType, name); + } + catch + { + //inner inspectors throw when they can't handle the type so we swallow it + } + } + + throw new ArgumentOutOfRangeException(nameof(enumType) + "," + nameof(name), "Name not found on enum type"); + } + + public override string GetEnumValue(object enumValue) + { + if (enumValue == null) + { + throw new ArgumentNullException(nameof(enumValue)); + } + + foreach (var inspector in typeInspectors) + { + try + { + return inspector.GetEnumValue(enumValue); + } + catch + { + //inner inspectors throw when they can't handle the type so we swallow it + } + } + + throw new ArgumentOutOfRangeException(nameof(enumValue), $"Value not found for ({enumValue})"); + } + public override IEnumerable GetProperties(Type type, object? container) { return typeInspectors diff --git a/YamlDotNet/Serialization/TypeInspectors/NamingConventionTypeInspector.cs b/YamlDotNet/Serialization/TypeInspectors/NamingConventionTypeInspector.cs index 709b703f5..5a4356ab1 100644 --- a/YamlDotNet/Serialization/TypeInspectors/NamingConventionTypeInspector.cs +++ b/YamlDotNet/Serialization/TypeInspectors/NamingConventionTypeInspector.cs @@ -40,6 +40,10 @@ public NamingConventionTypeInspector(ITypeInspector innerTypeDescriptor, INaming this.namingConvention = namingConvention ?? throw new ArgumentNullException(nameof(namingConvention)); } + public override string GetEnumName(Type enumType, string name) => this.innerTypeDescriptor.GetEnumName(enumType, name); + + public override string GetEnumValue(object enumValue) => this.innerTypeDescriptor.GetEnumValue(enumValue); + public override IEnumerable GetProperties(Type type, object? container) { return innerTypeDescriptor.GetProperties(type, container) diff --git a/YamlDotNet/Serialization/TypeInspectors/ReadableAndWritablePropertiesTypeInspector.cs b/YamlDotNet/Serialization/TypeInspectors/ReadableAndWritablePropertiesTypeInspector.cs index 22921e1fa..d2418af67 100644 --- a/YamlDotNet/Serialization/TypeInspectors/ReadableAndWritablePropertiesTypeInspector.cs +++ b/YamlDotNet/Serialization/TypeInspectors/ReadableAndWritablePropertiesTypeInspector.cs @@ -37,6 +37,10 @@ public ReadableAndWritablePropertiesTypeInspector(ITypeInspector innerTypeDescri this.innerTypeDescriptor = innerTypeDescriptor ?? throw new ArgumentNullException(nameof(innerTypeDescriptor)); } + public override string GetEnumName(Type enumType, string name) => innerTypeDescriptor.GetEnumName(enumType, name); + + public override string GetEnumValue(object enumValue) => this.innerTypeDescriptor.GetEnumValue(enumValue); + public override IEnumerable GetProperties(Type type, object? container) { return innerTypeDescriptor.GetProperties(type, container) diff --git a/YamlDotNet/Serialization/TypeInspectors/ReadableFieldsTypeInspector.cs b/YamlDotNet/Serialization/TypeInspectors/ReadableFieldsTypeInspector.cs index e7f77c2e1..51fbb3d0c 100644 --- a/YamlDotNet/Serialization/TypeInspectors/ReadableFieldsTypeInspector.cs +++ b/YamlDotNet/Serialization/TypeInspectors/ReadableFieldsTypeInspector.cs @@ -30,7 +30,7 @@ namespace YamlDotNet.Serialization.TypeInspectors /// /// Returns the properties and fields of a type that are readable. /// - public sealed class ReadableFieldsTypeInspector : TypeInspectorSkeleton + public sealed class ReadableFieldsTypeInspector : ReflectionTypeInspector { private readonly ITypeResolver typeResolver; @@ -55,12 +55,22 @@ public ReflectionFieldDescriptor(FieldInfo fieldInfo, ITypeResolver typeResolver { this.fieldInfo = fieldInfo; this.typeResolver = typeResolver; + + var converterAttribute = fieldInfo.GetCustomAttribute(); + if (converterAttribute != null) + { + ConverterType = converterAttribute.ConverterType; + } + ScalarStyle = ScalarStyle.Any; } public string Name { get { return fieldInfo.Name; } } + public bool Required { get => fieldInfo.IsRequired(); } public Type Type { get { return fieldInfo.FieldType; } } + public Type? ConverterType { get; } public Type? TypeOverride { get; set; } + public bool AllowNulls { get => fieldInfo.AcceptsNull(); } public int Order { get; set; } public bool CanWrite { get { return !fieldInfo.IsInitOnly; } } public ScalarStyle ScalarStyle { get; set; } diff --git a/YamlDotNet/Serialization/TypeInspectors/ReadablePropertiesTypeInspector.cs b/YamlDotNet/Serialization/TypeInspectors/ReadablePropertiesTypeInspector.cs index 90c3f2482..903781e48 100644 --- a/YamlDotNet/Serialization/TypeInspectors/ReadablePropertiesTypeInspector.cs +++ b/YamlDotNet/Serialization/TypeInspectors/ReadablePropertiesTypeInspector.cs @@ -30,7 +30,7 @@ namespace YamlDotNet.Serialization.TypeInspectors /// /// Returns the properties of a type that are readable. /// - public sealed class ReadablePropertiesTypeInspector : TypeInspectorSkeleton + public sealed class ReadablePropertiesTypeInspector : ReflectionTypeInspector { private readonly ITypeResolver typeResolver; private readonly bool includeNonPublicProperties; @@ -70,11 +70,21 @@ public ReflectionPropertyDescriptor(PropertyInfo propertyInfo, ITypeResolver typ this.propertyInfo = propertyInfo ?? throw new ArgumentNullException(nameof(propertyInfo)); this.typeResolver = typeResolver ?? throw new ArgumentNullException(nameof(typeResolver)); ScalarStyle = ScalarStyle.Any; + var converterAttribute = propertyInfo.GetCustomAttribute(); + if (converterAttribute != null) + { + ConverterType = converterAttribute.ConverterType; + } } public string Name => propertyInfo.Name; + public bool Required { get => propertyInfo.IsRequired(); } public Type Type => propertyInfo.PropertyType; public Type? TypeOverride { get; set; } + public Type? ConverterType { get; set; } + + public bool AllowNulls { get => propertyInfo.AcceptsNull(); } + public int Order { get; set; } public bool CanWrite => propertyInfo.CanWrite; public ScalarStyle ScalarStyle { get; set; } diff --git a/YamlDotNet/Serialization/TypeInspectors/ReflectionTypeInspector.cs b/YamlDotNet/Serialization/TypeInspectors/ReflectionTypeInspector.cs new file mode 100644 index 000000000..946712543 --- /dev/null +++ b/YamlDotNet/Serialization/TypeInspectors/ReflectionTypeInspector.cs @@ -0,0 +1,72 @@ +// This file is part of YamlDotNet - A .NET library for YAML. +// Copyright (c) Antoine Aubry and contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.Serialization; +using System.Text; +using System.Threading.Tasks; + +namespace YamlDotNet.Serialization.TypeInspectors +{ + public abstract class ReflectionTypeInspector : TypeInspectorSkeleton + { + public override string GetEnumName(Type enumType, string name) + { +#if NETSTANDARD2_0_OR_GREATER || NET6_0_OR_GREATER + foreach (var enumMember in enumType.GetMembers()) + { + var attribute = enumMember.GetCustomAttribute(); + if (attribute?.Value == name) + { + return enumMember.Name; + } + } +#endif + return name; + } + public override string GetEnumValue(object enumValue) + { + if (enumValue == null) + { + return string.Empty; + } + + var result = enumValue.ToString(); +#if NETSTANDARD2_0_OR_GREATER || NET6_0_OR_GREATER + var type = enumValue.GetType(); + var enumMembers = type.GetMember(result); + if (enumMembers.Length > 0) + { + var attribute = enumMembers[0].GetCustomAttribute(); + if (attribute != null) + { + result = attribute.Value; + } + } +#endif + return result!; + } + + } +} diff --git a/YamlDotNet/Serialization/TypeInspectors/TypeInspectorSkeleton.cs b/YamlDotNet/Serialization/TypeInspectors/TypeInspectorSkeleton.cs index ea9e00bb8..5f2334108 100644 --- a/YamlDotNet/Serialization/TypeInspectors/TypeInspectorSkeleton.cs +++ b/YamlDotNet/Serialization/TypeInspectors/TypeInspectorSkeleton.cs @@ -23,18 +23,33 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Reflection; using System.Runtime.Serialization; namespace YamlDotNet.Serialization.TypeInspectors { public abstract class TypeInspectorSkeleton : ITypeInspector { + public abstract string GetEnumName(Type enumType, string name); + + public abstract string GetEnumValue(object enumValue); + public abstract IEnumerable GetProperties(Type type, object? container); - public IPropertyDescriptor GetProperty(Type type, object? container, string name, [MaybeNullWhen(true)] bool ignoreUnmatched) + public IPropertyDescriptor GetProperty(Type type, object? container, string name, [MaybeNullWhen(true)] bool ignoreUnmatched, bool caseInsensitivePropertyMatching) { - var candidates = GetProperties(type, container) - .Where(p => p.Name == name); + IEnumerable candidates; + + if (caseInsensitivePropertyMatching) + { + candidates = GetProperties(type, container) + .Where(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); + } + else + { + candidates = GetProperties(type, container) + .Where(p => p.Name == name); + } using var enumerator = candidates.GetEnumerator(); if (!enumerator.MoveNext()) diff --git a/YamlDotNet/Serialization/TypeInspectors/WritablePropertiesTypeInspector.cs b/YamlDotNet/Serialization/TypeInspectors/WritablePropertiesTypeInspector.cs index 88ebee051..2111f0596 100644 --- a/YamlDotNet/Serialization/TypeInspectors/WritablePropertiesTypeInspector.cs +++ b/YamlDotNet/Serialization/TypeInspectors/WritablePropertiesTypeInspector.cs @@ -30,7 +30,7 @@ namespace YamlDotNet.Serialization.TypeInspectors /// /// Returns the properties of a type that are writable. /// - public sealed class WritablePropertiesTypeInspector : TypeInspectorSkeleton + public sealed class WritablePropertiesTypeInspector : ReflectionTypeInspector { private readonly ITypeResolver typeResolver; private readonly bool includeNonPublicProperties; @@ -71,11 +71,19 @@ public ReflectionPropertyDescriptor(PropertyInfo propertyInfo, ITypeResolver typ this.propertyInfo = propertyInfo ?? throw new ArgumentNullException(nameof(propertyInfo)); this.typeResolver = typeResolver ?? throw new ArgumentNullException(nameof(typeResolver)); ScalarStyle = ScalarStyle.Any; + var converterAttribute = propertyInfo.GetCustomAttribute(); + if (converterAttribute != null) + { + ConverterType = converterAttribute.ConverterType; + } } public string Name => propertyInfo.Name; + public bool Required { get => propertyInfo.IsRequired(); } public Type Type => propertyInfo.PropertyType; public Type? TypeOverride { get; set; } + public Type? ConverterType { get; set; } + public bool AllowNulls { get => propertyInfo.AcceptsNull(); } public int Order { get; set; } public bool CanWrite => propertyInfo.CanWrite; public ScalarStyle ScalarStyle { get; set; } diff --git a/YamlDotNet/Serialization/Utilities/ITypeConverter.cs b/YamlDotNet/Serialization/Utilities/ITypeConverter.cs index 20ec734c6..ae96977cf 100644 --- a/YamlDotNet/Serialization/Utilities/ITypeConverter.cs +++ b/YamlDotNet/Serialization/Utilities/ITypeConverter.cs @@ -31,7 +31,8 @@ public interface ITypeConverter /// /// /// Naming convention to use on enums in the type converter. + /// The type inspector to use when getting information about a type. /// - object? ChangeType(object? value, Type expectedType, INamingConvention enumNamingConvention); + object? ChangeType(object? value, Type expectedType, INamingConvention enumNamingConvention, ITypeInspector typeInspector); } } diff --git a/YamlDotNet/Serialization/Utilities/NullTypeConverter.cs b/YamlDotNet/Serialization/Utilities/NullTypeConverter.cs index b723e6cca..6731d8dd6 100644 --- a/YamlDotNet/Serialization/Utilities/NullTypeConverter.cs +++ b/YamlDotNet/Serialization/Utilities/NullTypeConverter.cs @@ -25,6 +25,6 @@ namespace YamlDotNet.Serialization.Utilities { public class NullTypeConverter : ITypeConverter { - public object? ChangeType(object? value, Type expectedType, INamingConvention enumNamingConvention) => value; + public object? ChangeType(object? value, Type expectedType, INamingConvention enumNamingConvention, ITypeInspector typeInspector) => value; } } diff --git a/YamlDotNet/Serialization/Utilities/ReflectionTypeConverter.cs b/YamlDotNet/Serialization/Utilities/ReflectionTypeConverter.cs index 735ed00cd..c42b3f8c9 100644 --- a/YamlDotNet/Serialization/Utilities/ReflectionTypeConverter.cs +++ b/YamlDotNet/Serialization/Utilities/ReflectionTypeConverter.cs @@ -26,8 +26,8 @@ namespace YamlDotNet.Serialization.Utilities { public class ReflectionTypeConverter : ITypeConverter { - public object? ChangeType(object? value, Type expectedType) => ChangeType(value, expectedType, NullNamingConvention.Instance); - public object? ChangeType(object? value, Type expectedType, INamingConvention enumNamingConvention) => TypeConverter.ChangeType(value, expectedType, enumNamingConvention); + public object? ChangeType(object? value, Type expectedType, ITypeInspector typeInspector) => ChangeType(value, expectedType, NullNamingConvention.Instance, typeInspector); + public object? ChangeType(object? value, Type expectedType, INamingConvention enumNamingConvention, ITypeInspector typeInspector) => TypeConverter.ChangeType(value, expectedType, enumNamingConvention, typeInspector); } } diff --git a/YamlDotNet/Serialization/Utilities/TypeConverter.cs b/YamlDotNet/Serialization/Utilities/TypeConverter.cs index d8efd0f17..b874593ca 100644 --- a/YamlDotNet/Serialization/Utilities/TypeConverter.cs +++ b/YamlDotNet/Serialization/Utilities/TypeConverter.cs @@ -42,36 +42,11 @@ public static partial class TypeConverter /// The type to which the value is to be converted. /// The value to convert. /// Naming convention to apply to enums. + /// The type inspector to use when getting information about a type. /// - public static T ChangeType(object? value, INamingConvention enumNamingConvention) + public static T ChangeType(object? value, INamingConvention enumNamingConvention, ITypeInspector typeInspector) { - return (T)ChangeType(value, typeof(T), enumNamingConvention)!; // This cast should always be valid - } - - /// - /// Converts the specified value. - /// - /// The type to which the value is to be converted. - /// The value to convert. - /// The provider. - /// Naming convention to apply to enums. - /// - public static T ChangeType(object? value, IFormatProvider provider, INamingConvention enumNamingConvention) - { - return (T)ChangeType(value, typeof(T), provider, enumNamingConvention)!; // This cast should always be valid - } - - /// - /// Converts the specified value. - /// - /// The type to which the value is to be converted. - /// The value to convert. - /// The culture. - /// Naming convention to apply to enums. - /// - public static T ChangeType(object? value, CultureInfo culture, INamingConvention enumNamingConvention) - { - return (T)ChangeType(value, typeof(T), culture, enumNamingConvention)!; // This cast should always be valid + return (T)ChangeType(value, typeof(T), enumNamingConvention, typeInspector)!; // This cast should always be valid } /// @@ -80,10 +55,11 @@ public static T ChangeType(object? value, CultureInfo culture, INamingConvent /// The value to convert. /// The type to which the value is to be converted. /// Naming convention to apply to enums. + /// The type inspector to use when getting information about a type. /// - public static object? ChangeType(object? value, Type destinationType, INamingConvention enumNamingConvention) + public static object? ChangeType(object? value, Type destinationType, INamingConvention enumNamingConvention, ITypeInspector typeInspector) { - return ChangeType(value, destinationType, CultureInfo.InvariantCulture, enumNamingConvention); + return ChangeType(value, destinationType, CultureInfo.InvariantCulture, enumNamingConvention, typeInspector); } /// @@ -93,10 +69,11 @@ public static T ChangeType(object? value, CultureInfo culture, INamingConvent /// The type to which the value is to be converted. /// The format provider. /// Naming convention to apply to enums. + /// The type inspector to use when getting information about a type. /// - public static object? ChangeType(object? value, Type destinationType, IFormatProvider provider, INamingConvention enumNamingConvention) + public static object? ChangeType(object? value, Type destinationType, IFormatProvider provider, INamingConvention enumNamingConvention, ITypeInspector typeInspector) { - return ChangeType(value, destinationType, new CultureInfoAdapter(CultureInfo.CurrentCulture, provider), enumNamingConvention); + return ChangeType(value, destinationType, new CultureInfoAdapter(CultureInfo.CurrentCulture, provider), enumNamingConvention, typeInspector); } /// @@ -106,8 +83,9 @@ public static T ChangeType(object? value, CultureInfo culture, INamingConvent /// The type to which the value is to be converted. /// The culture. /// Naming convention to apply to enums. + /// The type inspector to use when getting information about a type. /// - public static object? ChangeType(object? value, Type destinationType, CultureInfo culture, INamingConvention enumNamingConvention) + public static object? ChangeType(object? value, Type destinationType, CultureInfo culture, INamingConvention enumNamingConvention, ITypeInspector typeInspector) { // Handle null and DBNull if (value == null || value.IsDbNull()) @@ -127,10 +105,10 @@ public static T ChangeType(object? value, CultureInfo culture, INamingConvent if (destinationType.IsGenericType()) { var genericTypeDefinition = destinationType.GetGenericTypeDefinition(); - if (genericTypeDefinition == typeof(Nullable<>) || FsharpHelper.IsOptionType(genericTypeDefinition)) + if (genericTypeDefinition == typeof(Nullable<>) || FsharpHelper.IsOptionType(genericTypeDefinition)) { var innerType = destinationType.GetGenericArguments()[0]; - var convertedValue = ChangeType(value, innerType, culture, enumNamingConvention); + var convertedValue = ChangeType(value, innerType, culture, enumNamingConvention, typeInspector); return Activator.CreateInstance(destinationType, convertedValue); } } @@ -143,6 +121,7 @@ public static T ChangeType(object? value, CultureInfo culture, INamingConvent if (value is string valueText) { valueText = enumNamingConvention.Reverse(valueText); + valueText = typeInspector.GetEnumName(destinationType, valueText); result = Enum.Parse(destinationType, valueText, true); } @@ -239,7 +218,7 @@ public static T ChangeType(object? value, CultureInfo culture, INamingConvent // Handle TimeSpan if (destinationType == typeof(TimeSpan)) { - return TimeSpan.Parse((string)ChangeType(value, typeof(string), CultureInfo.InvariantCulture, enumNamingConvention)!); + return TimeSpan.Parse((string)ChangeType(value, typeof(string), CultureInfo.InvariantCulture, enumNamingConvention, typeInspector)!); } // Default to the Convert class diff --git a/YamlDotNet/Serialization/ValueDeserializers/NodeValueDeserializer.cs b/YamlDotNet/Serialization/ValueDeserializers/NodeValueDeserializer.cs index 81beed977..52711bffb 100644 --- a/YamlDotNet/Serialization/ValueDeserializers/NodeValueDeserializer.cs +++ b/YamlDotNet/Serialization/ValueDeserializers/NodeValueDeserializer.cs @@ -34,30 +34,36 @@ public sealed class NodeValueDeserializer : IValueDeserializer private readonly IList typeResolvers; private readonly ITypeConverter typeConverter; private readonly INamingConvention enumNamingConvention; + private readonly ITypeInspector typeInspector; public NodeValueDeserializer(IList deserializers, IList typeResolvers, ITypeConverter typeConverter, - INamingConvention enumNamingConvention) + INamingConvention enumNamingConvention, + ITypeInspector typeInspector) { this.deserializers = deserializers ?? throw new ArgumentNullException(nameof(deserializers)); this.typeResolvers = typeResolvers ?? throw new ArgumentNullException(nameof(typeResolvers)); this.typeConverter = typeConverter ?? throw new ArgumentNullException(nameof(typeConverter)); this.enumNamingConvention = enumNamingConvention ?? throw new ArgumentNullException(nameof(enumNamingConvention)); + this.typeInspector = typeInspector; } public object? DeserializeValue(IParser parser, Type expectedType, SerializerState state, IValueDeserializer nestedObjectDeserializer) { parser.Accept(out var nodeEvent); var nodeType = GetTypeFromEvent(nodeEvent, expectedType); + var rootDeserializer = new ObjectDeserializer(x => DeserializeValue(parser, x, state, nestedObjectDeserializer)); try { foreach (var deserializer in deserializers) { - if (deserializer.Deserialize(parser, nodeType, (r, t) => nestedObjectDeserializer.DeserializeValue(r, t, state, nestedObjectDeserializer), out var value)) + + var result = deserializer.Deserialize(parser, nodeType, (r, t) => nestedObjectDeserializer.DeserializeValue(r, t, state, nestedObjectDeserializer), out var value, rootDeserializer); + if (result) { - return typeConverter.ChangeType(value, expectedType, enumNamingConvention); + return typeConverter.ChangeType(value, expectedType, enumNamingConvention, typeInspector); } } } diff --git a/YamlDotNet/Serialization/YamlAttributeOverridesInspector.cs b/YamlDotNet/Serialization/YamlAttributeOverridesInspector.cs index 1df5624db..99469ec94 100644 --- a/YamlDotNet/Serialization/YamlAttributeOverridesInspector.cs +++ b/YamlDotNet/Serialization/YamlAttributeOverridesInspector.cs @@ -22,6 +22,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using YamlDotNet.Core; using YamlDotNet.Serialization.TypeInspectors; @@ -30,7 +31,7 @@ namespace YamlDotNet.Serialization /// /// Applies the Yaml attribute overrides to another . /// - public sealed class YamlAttributeOverridesInspector : TypeInspectorSkeleton + public sealed class YamlAttributeOverridesInspector : ReflectionTypeInspector { private readonly ITypeInspector innerTypeDescriptor; private readonly YamlAttributeOverrides overrides; @@ -67,6 +68,8 @@ public OverridePropertyDescriptor(IPropertyDescriptor baseDescriptor, YamlAttrib } public string Name { get { return baseDescriptor.Name; } } + public bool Required { get => baseDescriptor.Required; } + public bool AllowNulls { get => baseDescriptor.AllowNulls; } public bool CanWrite { get { return baseDescriptor.CanWrite; } } @@ -78,6 +81,8 @@ public Type? TypeOverride set { baseDescriptor.TypeOverride = value; } } + public Type? ConverterType => baseDescriptor.ConverterType; + public int Order { get { return baseDescriptor.Order; } diff --git a/YamlDotNet/Serialization/YamlAttributesTypeInspector.cs b/YamlDotNet/Serialization/YamlAttributesTypeInspector.cs index e8fe65fbc..37df8a904 100644 --- a/YamlDotNet/Serialization/YamlAttributesTypeInspector.cs +++ b/YamlDotNet/Serialization/YamlAttributesTypeInspector.cs @@ -38,6 +38,10 @@ public YamlAttributesTypeInspector(ITypeInspector innerTypeDescriptor) this.innerTypeDescriptor = innerTypeDescriptor; } + public override string GetEnumName(Type enumType, string name) => innerTypeDescriptor.GetEnumName(enumType, name); + + public override string GetEnumValue(object enumValue) => innerTypeDescriptor.GetEnumValue(enumValue); + public override IEnumerable GetProperties(Type type, object? container) { return innerTypeDescriptor.GetProperties(type, container) diff --git a/YamlDotNet/Serialization/YamlConverterAttribute.cs b/YamlDotNet/Serialization/YamlConverterAttribute.cs new file mode 100644 index 000000000..a6371d59a --- /dev/null +++ b/YamlDotNet/Serialization/YamlConverterAttribute.cs @@ -0,0 +1,36 @@ +// This file is part of YamlDotNet - A .NET library for YAML. +// Copyright (c) Antoine Aubry and contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System; + +namespace YamlDotNet.Serialization +{ + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] + public sealed class YamlConverterAttribute : Attribute + { + public Type ConverterType { get; } + + public YamlConverterAttribute(Type converterType) + { + ConverterType = converterType; + } + } +} diff --git a/YamlDotNet/Serialization/YamlFormatter.cs b/YamlDotNet/Serialization/YamlFormatter.cs index dc6ba6af4..8f3507a13 100644 --- a/YamlDotNet/Serialization/YamlFormatter.cs +++ b/YamlDotNet/Serialization/YamlFormatter.cs @@ -79,7 +79,7 @@ public string FormatTimeSpan(object timeSpan) /// By default it will be the string representation of the enum passed through the naming convention. /// /// A string representation of the enum - public virtual Func FormatEnum { get; set; } = (value, enumNamingConvention) => + public virtual Func FormatEnum { get; set; } = (value, typeInspector, enumNamingConvention) => { var result = string.Empty; @@ -89,9 +89,10 @@ public string FormatTimeSpan(object timeSpan) } else { - result = value.ToString(); + result = typeInspector.GetEnumValue(value); } + result = enumNamingConvention.Apply(result); return result; diff --git a/YamlDotNet/Serialization/YamlSerializable.cs b/YamlDotNet/Serialization/YamlSerializable.cs index f2fe9594b..1a4d59c59 100644 --- a/YamlDotNet/Serialization/YamlSerializable.cs +++ b/YamlDotNet/Serialization/YamlSerializable.cs @@ -27,7 +27,7 @@ namespace YamlDotNet.Serialization /// Put this attribute either on serializable types or on the that you want /// the static analyzer to detect and use. /// - [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)] + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Enum, Inherited = false, AllowMultiple = true)] public sealed class YamlSerializableAttribute : Attribute { /// diff --git a/YamlDotNet/YamlDotNet.csproj b/YamlDotNet/YamlDotNet.csproj index 516721292..2d44412c1 100644 --- a/YamlDotNet/YamlDotNet.csproj +++ b/YamlDotNet/YamlDotNet.csproj @@ -1,7 +1,7 @@  - net8.0;net7.0;net6.0;netstandard2.0;netstandard2.1;net47 + net8.0;net6.0;netstandard2.0;netstandard2.1;net47 https://github.com/aaubry/YamlDotNet https://github.com/aaubry/YamlDotNet @@ -15,6 +15,7 @@ 1591;1574 false true + true 8.0 enable diff --git a/YamlDotNet/YamlDotNet.nuspec b/YamlDotNet/YamlDotNet.nuspec index 4c922a3de..07b163181 100644 --- a/YamlDotNet/YamlDotNet.nuspec +++ b/YamlDotNet/YamlDotNet.nuspec @@ -19,7 +19,6 @@ - @@ -32,10 +31,6 @@ - - - - diff --git a/appveyor.yml b/appveyor.yml index 2ea55a634..1e8dc32a9 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -38,9 +38,6 @@ artifacts: - path: YamlDotNet\bin\Release\net6.0 name: Release-Net60 - - path: YamlDotNet\bin\Release\net7.0 - name: Release-Net70 - - path: YamlDotNet\bin\Release\net8.0 name: Release-Net80