From d155cf9bc3d15e331b9a3eb3b2869c36e32c2059 Mon Sep 17 00:00:00 2001 From: Tomer Amir Date: Sun, 28 May 2023 15:51:59 +0300 Subject: [PATCH 01/10] Fix infinite recursion of the hash code function of YamlMappingNode --- YamlDotNet/RepresentationModel/YamlMappingNode.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/YamlDotNet/RepresentationModel/YamlMappingNode.cs b/YamlDotNet/RepresentationModel/YamlMappingNode.cs index 3aa34d9a..49d793f6 100644 --- a/YamlDotNet/RepresentationModel/YamlMappingNode.cs +++ b/YamlDotNet/RepresentationModel/YamlMappingNode.cs @@ -297,7 +297,7 @@ public override int GetHashCode() foreach (var entry in children) { hashCode = CombineHashCodes(hashCode, entry.Key); - hashCode = CombineHashCodes(hashCode, entry.Value); + hashCode = CombineHashCodes(hashCode, entry.Value.Start); } return hashCode; } From 83beaba8c8c17344acbcc417f9aa88d8707b45ee Mon Sep 17 00:00:00 2001 From: Joe Amenta Date: Fri, 9 Feb 2024 09:02:51 -0500 Subject: [PATCH 02/10] It was actually fixed in e1986a45 So just add a test for it --- .../Serialization/SerializationTests.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/YamlDotNet.Test/Serialization/SerializationTests.cs b/YamlDotNet.Test/Serialization/SerializationTests.cs index d46bc25e..bf37668e 100644 --- a/YamlDotNet.Test/Serialization/SerializationTests.cs +++ b/YamlDotNet.Test/Serialization/SerializationTests.cs @@ -2455,6 +2455,16 @@ public void NamingConventionAppliedToEnumWhenDeserializing() 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(), 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; } @@ -2546,5 +2556,15 @@ 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 { [1] = new Payload { I = 1 } }; + + public struct Payload + { + public int I { get; set; } + } + } } } From 912e5f9ca0f6cda263d191a33db78df7e4fd68ec Mon Sep 17 00:00:00 2001 From: Joe Amenta Date: Fri, 9 Feb 2024 09:11:49 -0500 Subject: [PATCH 03/10] Ensure that this new test can't just pass by leaving the object in its default state. --- YamlDotNet.Test/Serialization/SerializationTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/YamlDotNet.Test/Serialization/SerializationTests.cs b/YamlDotNet.Test/Serialization/SerializationTests.cs index bf37668e..5345dc9c 100644 --- a/YamlDotNet.Test/Serialization/SerializationTests.cs +++ b/YamlDotNet.Test/Serialization/SerializationTests.cs @@ -2460,7 +2460,7 @@ public void NamingConventionAppliedToEnumWhenDeserializing() public void NestedDictionaryTypes_ShouldRoundtrip() { var serializer = new SerializerBuilder().EnsureRoundtrip().Build(); - var yaml = serializer.Serialize(new HasNestedDictionary(), typeof(HasNestedDictionary)); + 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); } @@ -2559,7 +2559,7 @@ public void WriteYaml(IEmitter emitter, object value, Type type) public sealed class HasNestedDictionary { - public Dictionary Lookups { get; set; } = new Dictionary { [1] = new Payload { I = 1 } }; + public Dictionary Lookups { get; set; } = new Dictionary(); public struct Payload { From c9b7638ec35eb3881cff56f9ee46aeb74a410fd6 Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Wed, 7 Feb 2024 10:59:13 +0200 Subject: [PATCH 04/10] Switch to using PackageLicenseExpression --- .../YamlDotNet.Analyzers.StaticGenerator.nuspec | 2 +- YamlDotNet/YamlDotNet.csproj | 2 +- YamlDotNet/YamlDotNet.nuspec | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/YamlDotNet.Analyzers.StaticGenerator/YamlDotNet.Analyzers.StaticGenerator.nuspec b/YamlDotNet.Analyzers.StaticGenerator/YamlDotNet.Analyzers.StaticGenerator.nuspec index bcf94d11..e902f69d 100644 --- a/YamlDotNet.Analyzers.StaticGenerator/YamlDotNet.Analyzers.StaticGenerator.nuspec +++ b/YamlDotNet.Analyzers.StaticGenerator/YamlDotNet.Analyzers.StaticGenerator.nuspec @@ -8,7 +8,7 @@ Roslyn Code Generator that will generate a static context for use with YamlDotNet to support ahead-of-time and library trimming. Static context generator for YamlDotNet. en-US - LICENSE.txt + MIT https://github.com/aaubry/YamlDotNet/wiki http://aaubry.net/images/yamldotnet.png images/yamldotnet.png diff --git a/YamlDotNet/YamlDotNet.csproj b/YamlDotNet/YamlDotNet.csproj index 482c5fb2..90b3902e 100644 --- a/YamlDotNet/YamlDotNet.csproj +++ b/YamlDotNet/YamlDotNet.csproj @@ -6,7 +6,7 @@ https://github.com/aaubry/YamlDotNet https://github.com/aaubry/YamlDotNet YamlDotNet is a .NET library for YAML. YamlDotNet provides low level parsing and emitting of YAML as well as a high level object model similar to XmlDocument. A serialization library is also included that allows to read and write objects from and to YAML streams. - LICENSE.txt + MIT Copyright (c) Antoine Aubry and contributors Debug;Release ..\YamlDotNet.snk diff --git a/YamlDotNet/YamlDotNet.nuspec b/YamlDotNet/YamlDotNet.nuspec index e5f4046d..53dc255f 100644 --- a/YamlDotNet/YamlDotNet.nuspec +++ b/YamlDotNet/YamlDotNet.nuspec @@ -7,7 +7,7 @@ A .NET library for YAML. YamlDotNet provides low level parsing and emitting of YAML as well as a high level object model similar to XmlDocument. This package contains the YAML parser and serializer. en-US - LICENSE.txt + MIT https://github.com/aaubry/YamlDotNet/wiki http://aaubry.net/images/yamldotnet.png images/yamldotnet.png From de28ca2eda2fe7cdacd3959126e7b2f5386eb3d9 Mon Sep 17 00:00:00 2001 From: kasperk81 <83082615+kasperk81@users.noreply.github.com> Date: Tue, 14 May 2024 22:33:51 +0300 Subject: [PATCH 05/10] use correct framework --- Dockerfile | 12 +- Dockerfile.NonEnglish | 14 +- .../YamlDotNet.Benchmark.csproj | 2 +- ...amlDotNet.Core7AoTCompileTest.Model.csproj | 2 +- .../YamlDotNet.Core7AoTCompileTest.csproj | 2 +- YamlDotNet.Samples/YamlDotNet.Samples.csproj | 2 +- .../Serialization/DeserializerTest.cs | 826 +-- .../Serialization/SerializationTests.cs | 5196 ++++++++--------- YamlDotNet.Test/YamlDotNet.Test.csproj | 2 +- YamlDotNet/Helpers/OrderedDictionary.cs | 468 +- YamlDotNet/YamlDotNet.csproj | 12 +- YamlDotNet/YamlDotNet.nuspec | 12 +- appveyor.yml | 6 +- tools/build/build.csproj | 2 +- 14 files changed, 3279 insertions(+), 3279 deletions(-) diff --git a/Dockerfile b/Dockerfile index af31659b..2437eff0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -40,15 +40,15 @@ COPY . . RUN dotnet build -c Release --framework net47 YamlDotNet/YamlDotNet.csproj -o /output/net47 RUN dotnet build -c Release --framework netstandard2.0 YamlDotNet/YamlDotNet.csproj -o /output/netstandard2.0 RUN dotnet build -c Release --framework netstandard2.1 YamlDotNet/YamlDotNet.csproj -o /output/netstandard2.1 -RUN dotnet build -c Release --framework net60 YamlDotNet/YamlDotNet.csproj -o /output/net60 -RUN dotnet build -c Release --framework net70 YamlDotNet/YamlDotNet.csproj -o /output/net70 -RUN dotnet build -c Release --framework net80 YamlDotNet/YamlDotNet.csproj -o /output/net80 +RUN dotnet build -c Release --framework net6.0 YamlDotNet/YamlDotNet.csproj -o /output/net6.0 +RUN dotnet build -c Release --framework net7.0 YamlDotNet/YamlDotNet.csproj -o /output/net7.0 +RUN dotnet build -c Release --framework net8.0 YamlDotNet/YamlDotNet.csproj -o /output/net8.0 RUN dotnet pack -c Release YamlDotNet/YamlDotNet.csproj -o /output/package /p:Version=$PACKAGE_VERSION -RUN dotnet test -c Release YamlDotNet.Test/YamlDotNet.Test.csproj --framework net80 --logger:"trx;LogFileName=/output/tests.net80.trx" --logger:"console;Verbosity=detailed" -RUN dotnet test -c Release YamlDotNet.Test/YamlDotNet.Test.csproj --framework net70 --logger:"trx;LogFileName=/output/tests.net70.trx" --logger:"console;Verbosity=detailed" -RUN dotnet test -c Release YamlDotNet.Test/YamlDotNet.Test.csproj --framework net60 --logger:"trx;LogFileName=/output/tests.net60.trx" --logger:"console;Verbosity=detailed" +RUN dotnet test -c Release YamlDotNet.Test/YamlDotNet.Test.csproj --framework net8.0 --logger:"trx;LogFileName=/output/tests-net8.0.trx" --logger:"console;Verbosity=detailed" +RUN dotnet test -c Release YamlDotNet.Test/YamlDotNet.Test.csproj --framework net7.0 --logger:"trx;LogFileName=/output/tests-net7.0.trx" --logger:"console;Verbosity=detailed" +RUN dotnet test -c Release YamlDotNet.Test/YamlDotNet.Test.csproj --framework net6.0 --logger:"trx;LogFileName=/output/tests-net6.0.trx" --logger:"console;Verbosity=detailed" FROM alpine VOLUME /output diff --git a/Dockerfile.NonEnglish b/Dockerfile.NonEnglish index 69c454a7..20d6cf1d 100644 --- a/Dockerfile.NonEnglish +++ b/Dockerfile.NonEnglish @@ -42,9 +42,9 @@ COPY . . RUN dotnet build -c Release --framework net47 YamlDotNet/YamlDotNet.csproj -o /output/net47 RUN dotnet build -c Release --framework netstandard2.0 YamlDotNet/YamlDotNet.csproj -o /output/netstandard2.0 RUN dotnet build -c Release --framework netstandard2.1 YamlDotNet/YamlDotNet.csproj -o /output/netstandard2.1 -RUN dotnet build -c Release --framework net60 YamlDotNet/YamlDotNet.csproj -o /output/net60 -RUN dotnet build -c Release --framework net70 YamlDotNet/YamlDotNet.csproj -o /output/net70 -RUN dotnet build -c Release --framework net80 YamlDotNet/YamlDotNet.csproj -o /output/net80 +RUN dotnet build -c Release --framework net6.0 YamlDotNet/YamlDotNet.csproj -o /output/net6.0 +RUN dotnet build -c Release --framework net7.0 YamlDotNet/YamlDotNet.csproj -o /output/net7.0 +RUN dotnet build -c Release --framework net8.0 YamlDotNet/YamlDotNet.csproj -o /output/net8.0 RUN dotnet pack -c Release YamlDotNet/YamlDotNet.csproj -o /output/package /p:Version=$PACKAGE_VERSION @@ -62,10 +62,10 @@ RUN echo -n "${LC_ALL}" > /etc/locale.gen && \ apt install -y locales && \ locale-gen -RUN dotnet test -c Release YamlDotNet.Test/YamlDotNet.Test.csproj --framework net80 --logger:"trx;LogFileName=/output/tests.net80.trx" --logger:"console;Verbosity=detailed" -RUN dotnet test -c Release YamlDotNet.Test/YamlDotNet.Test.csproj --framework net70 --logger:"trx;LogFileName=/output/tests.net70.trx" --logger:"console;Verbosity=detailed" -RUN dotnet test -c Release YamlDotNet.Test/YamlDotNet.Test.csproj --framework net60 --logger:"trx;LogFileName=/output/tests.net60.trx" --logger:"console;Verbosity=detailed" -RUN dotnet test -c Release YamlDotNet.Test/YamlDotNet.Test.csproj --framework netcoreapp3.1 --logger:"trx;LogFileName=/output/tests.netcoreapp3.1.trx" --logger:"console;Verbosity=detailed" +RUN dotnet test -c Release YamlDotNet.Test/YamlDotNet.Test.csproj --framework net8.0 --logger:"trx;LogFileName=/output/tests-net8.0.trx" --logger:"console;Verbosity=detailed" +RUN dotnet test -c Release YamlDotNet.Test/YamlDotNet.Test.csproj --framework net7.0 --logger:"trx;LogFileName=/output/tests-net7.0.trx" --logger:"console;Verbosity=detailed" +RUN dotnet test -c Release YamlDotNet.Test/YamlDotNet.Test.csproj --framework net6.0 --logger:"trx;LogFileName=/output/tests-net6.0.trx" --logger:"console;Verbosity=detailed" +RUN dotnet test -c Release YamlDotNet.Test/YamlDotNet.Test.csproj --framework netcoreapp3.1 --logger:"trx;LogFileName=/output/tests-netcoreapp3.1.trx" --logger:"console;Verbosity=detailed" FROM alpine VOLUME /output diff --git a/YamlDotNet.Benchmark/YamlDotNet.Benchmark.csproj b/YamlDotNet.Benchmark/YamlDotNet.Benchmark.csproj index 9ade6282..79208c1f 100644 --- a/YamlDotNet.Benchmark/YamlDotNet.Benchmark.csproj +++ b/YamlDotNet.Benchmark/YamlDotNet.Benchmark.csproj @@ -2,7 +2,7 @@ Exe - net80 + net8.0 enable enable diff --git a/YamlDotNet.Core7AoTCompileTest.Model/YamlDotNet.Core7AoTCompileTest.Model.csproj b/YamlDotNet.Core7AoTCompileTest.Model/YamlDotNet.Core7AoTCompileTest.Model.csproj index 32ee4a56..cfadb03d 100644 --- a/YamlDotNet.Core7AoTCompileTest.Model/YamlDotNet.Core7AoTCompileTest.Model.csproj +++ b/YamlDotNet.Core7AoTCompileTest.Model/YamlDotNet.Core7AoTCompileTest.Model.csproj @@ -1,7 +1,7 @@ - net70 + net7.0 enable enable diff --git a/YamlDotNet.Core7AoTCompileTest/YamlDotNet.Core7AoTCompileTest.csproj b/YamlDotNet.Core7AoTCompileTest/YamlDotNet.Core7AoTCompileTest.csproj index 9d42ee9a..494a5300 100644 --- a/YamlDotNet.Core7AoTCompileTest/YamlDotNet.Core7AoTCompileTest.csproj +++ b/YamlDotNet.Core7AoTCompileTest/YamlDotNet.Core7AoTCompileTest.csproj @@ -2,7 +2,7 @@ Exe - net70 + net7.0 true true enable diff --git a/YamlDotNet.Samples/YamlDotNet.Samples.csproj b/YamlDotNet.Samples/YamlDotNet.Samples.csproj index 5ca9b2c4..e8d4b0d0 100644 --- a/YamlDotNet.Samples/YamlDotNet.Samples.csproj +++ b/YamlDotNet.Samples/YamlDotNet.Samples.csproj @@ -1,7 +1,7 @@ ο»Ώ - net80 + net8.0 false diff --git a/YamlDotNet.Test/Serialization/DeserializerTest.cs b/YamlDotNet.Test/Serialization/DeserializerTest.cs index d8d54aeb..44ddf124 100644 --- a/YamlDotNet.Test/Serialization/DeserializerTest.cs +++ b/YamlDotNet.Test/Serialization/DeserializerTest.cs @@ -1,413 +1,413 @@ -ο»Ώ// 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 FluentAssertions; -using Xunit; -using YamlDotNet.Core; -using YamlDotNet.Serialization; -using YamlDotNet.Serialization.Callbacks; -using YamlDotNet.Serialization.NamingConventions; - -namespace YamlDotNet.Test.Serialization -{ - public class DeserializerTest - { - [Fact] - public void Deserialize_YamlWithInterfaceTypeAndMapping_ReturnsModel() - { - var yaml = @" -name: Jack -momentOfBirth: 1983-04-21T20:21:03.0041599Z -cars: -- name: Mercedes - year: 2018 -- name: Honda - year: 2021 -"; - - var sut = new DeserializerBuilder() - .WithNamingConvention(CamelCaseNamingConvention.Instance) - .WithTypeMapping() - .Build(); - - var person = sut.Deserialize(yaml); - person.Name.Should().Be("Jack"); - person.MomentOfBirth.Kind.Should().Be(DateTimeKind.Utc); - person.MomentOfBirth.ToUniversalTime().Year.Should().Be(1983); - person.MomentOfBirth.ToUniversalTime().Month.Should().Be(4); - person.MomentOfBirth.ToUniversalTime().Day.Should().Be(21); - person.MomentOfBirth.ToUniversalTime().Hour.Should().Be(20); - person.MomentOfBirth.ToUniversalTime().Minute.Should().Be(21); - person.MomentOfBirth.ToUniversalTime().Second.Should().Be(3); - person.Cars.Should().HaveCount(2); - person.Cars[0].Name.Should().Be("Mercedes"); - person.Cars[0].Spec.Should().BeNull(); - person.Cars[1].Name.Should().Be("Honda"); - person.Cars[1].Spec.Should().BeNull(); - } - - [Fact] - public void Deserialize_YamlWithTwoInterfaceTypesAndMappings_ReturnsModel() - { - var yaml = @" -name: Jack -momentOfBirth: 1983-04-21T20:21:03.0041599Z -cars: -- name: Mercedes - year: 2018 - spec: - engineType: V6 - driveType: AWD -- name: Honda - year: 2021 - spec: - engineType: V4 - driveType: FWD -"; - - var sut = new DeserializerBuilder() - .WithNamingConvention(CamelCaseNamingConvention.Instance) - .WithTypeMapping() - .WithTypeMapping() - .Build(); - - var person = sut.Deserialize(yaml); - person.Name.Should().Be("Jack"); - person.MomentOfBirth.Kind.Should().Be(DateTimeKind.Utc); - person.MomentOfBirth.ToUniversalTime().Year.Should().Be(1983); - person.MomentOfBirth.ToUniversalTime().Month.Should().Be(4); - person.MomentOfBirth.ToUniversalTime().Day.Should().Be(21); - person.MomentOfBirth.ToUniversalTime().Hour.Should().Be(20); - person.MomentOfBirth.ToUniversalTime().Minute.Should().Be(21); - person.MomentOfBirth.ToUniversalTime().Second.Should().Be(3); - person.Cars.Should().HaveCount(2); - person.Cars[0].Name.Should().Be("Mercedes"); - person.Cars[0].Spec.EngineType.Should().Be("V6"); - person.Cars[0].Spec.DriveType.Should().Be("AWD"); - person.Cars[1].Name.Should().Be("Honda"); - person.Cars[1].Spec.EngineType.Should().Be("V4"); - person.Cars[1].Spec.DriveType.Should().Be("FWD"); - } - - [Fact] - public void SetterOnlySetsWithoutException() - { - var yaml = @" -Value: bar -"; - var deserializer = new DeserializerBuilder().Build(); - var result = deserializer.Deserialize(yaml); - result.Actual.Should().Be("bar"); - } - - [Fact] - public void KeysOnDynamicClassDontGetQuoted() - { - var serializer = new SerializerBuilder().WithQuotingNecessaryStrings().Build(); - var deserializer = new DeserializerBuilder().WithAttemptingUnquotedStringTypeDeserialization().Build(); - var yaml = @" -True: null -False: hello -Null: true -X: -"; - var obj = deserializer.Deserialize(yaml, typeof(object)); - var result = serializer.Serialize(obj); - var dictionary = (Dictionary)obj; - var keys = dictionary.Keys.ToArray(); - Assert.Equal(keys, new[] { "True", "False", "Null", "X" }); - Assert.Equal(dictionary.Values, new object[] { null, "hello", true, null }); - } - - [Fact] - public void EmptyQuotedStringsArentNull() - { - var deserializer = new DeserializerBuilder().WithAttemptingUnquotedStringTypeDeserialization().Build(); - var yaml = "Value: \"\""; - var result = deserializer.Deserialize(yaml); - Assert.Equal(string.Empty, result.Value); - } - - [Fact] - public void KeyAnchorIsHandledWithTypeDeserialization() - { - var yaml = @"a: &some_scalar this is also a key -b: &number 1 -*some_scalar: ""will this key be handled correctly?"" -*number: 1"; - var deserializer = new DeserializerBuilder().WithAttemptingUnquotedStringTypeDeserialization().Build(); - var result = deserializer.Deserialize(yaml, typeof(object)); - Assert.IsType>(result); - var dictionary = (Dictionary)result; - Assert.Equal(new object[] { "a", "b", "this is also a key", (byte)1 }, dictionary.Keys); - Assert.Equal(new object[] { "this is also a key", (byte)1, "will this key be handled correctly?", (byte)1 }, dictionary.Values); - } - - [Fact] - public void NonScalarKeyIsHandledWithTypeDeserialization() - { - var yaml = @"scalar: foo -{ a: mapping }: bar -[ a, sequence, 1 ]: baz"; - var deserializer = new DeserializerBuilder().WithAttemptingUnquotedStringTypeDeserialization().Build(); - var result = deserializer.Deserialize(yaml, typeof(object)); - Assert.IsType>(result); - - var dictionary = (Dictionary)result; - var item = dictionary.ElementAt(0); - Assert.Equal("scalar", item.Key); - Assert.Equal("foo", item.Value); - - item = dictionary.ElementAt(1); - Assert.IsType>(item.Key); - Assert.Equal("bar", item.Value); - dictionary = (Dictionary)item.Key; - item = dictionary.ElementAt(0); - Assert.Equal("a", item.Key); - Assert.Equal("mapping", item.Value); - - dictionary = (Dictionary)result; - item = dictionary.ElementAt(2); - Assert.IsType>(item.Key); - Assert.Equal(new List { "a", "sequence", (byte)1 }, (List)item.Key); - Assert.Equal("baz", item.Value); - } - - [Fact] - public void NewLinesInKeys() - { - var yaml = @"? >- - key - - a - - b -: >- - value - - a - - b -"; - var deserializer = new DeserializerBuilder().Build(); - var o = deserializer.Deserialize(yaml, typeof(object)); - Assert.IsType>(o); - var dictionary = (Dictionary)o; - Assert.Equal($"key\na\nb", dictionary.First().Key); - Assert.Equal($"value\na\nb", dictionary.First().Value); - } - - [Theory] - [InlineData(".nan", System.Single.NaN)] - [InlineData(".NaN", System.Single.NaN)] - [InlineData(".NAN", System.Single.NaN)] - [InlineData("-.inf", System.Single.NegativeInfinity)] - [InlineData("+.inf", System.Single.PositiveInfinity)] - [InlineData(".inf", System.Single.PositiveInfinity)] - [InlineData("start.nan", "start.nan")] - [InlineData(".nano", ".nano")] - [InlineData(".infinity", ".infinity")] - [InlineData("www.infinitetechnology.com", "www.infinitetechnology.com")] - [InlineData("https://api.inference.azure.com", "https://api.inference.azure.com")] - public void UnquotedStringTypeDeserializationHandlesInfAndNaN(string yamlValue, object expected) - { - var deserializer = new DeserializerBuilder() - .WithAttemptingUnquotedStringTypeDeserialization().Build(); - var yaml = $"Value: {yamlValue}"; - - var resultDict = deserializer.Deserialize>(yaml); - Assert.True(resultDict.ContainsKey("Value")); - Assert.Equal(expected, resultDict["Value"]); - } - - public static IEnumerable DeserializeScalarEdgeCases_TestCases - { - get - { - yield return new object[] { byte.MinValue, typeof(byte) }; - yield return new object[] { byte.MaxValue, typeof(byte) }; - yield return new object[] { short.MinValue, typeof(short) }; - yield return new object[] { short.MaxValue, typeof(short) }; - yield return new object[] { int.MinValue, typeof(int) }; - yield return new object[] { int.MaxValue, typeof(int) }; - yield return new object[] { long.MinValue, typeof(long) }; - yield return new object[] { long.MaxValue, typeof(long) }; - yield return new object[] { sbyte.MinValue, typeof(sbyte) }; - yield return new object[] { sbyte.MaxValue, typeof(sbyte) }; - yield return new object[] { ushort.MinValue, typeof(ushort) }; - yield return new object[] { ushort.MaxValue, typeof(ushort) }; - yield return new object[] { uint.MinValue, typeof(uint) }; - yield return new object[] { uint.MaxValue, typeof(uint) }; - yield return new object[] { ulong.MinValue, typeof(ulong) }; - yield return new object[] { ulong.MaxValue, typeof(ulong) }; - yield return new object[] { decimal.MinValue, typeof(decimal) }; - yield return new object[] { decimal.MaxValue, typeof(decimal) }; - yield return new object[] { char.MaxValue, typeof(char) }; - -#if NETCOREAPP3_1_OR_GREATER - yield return new object[] { float.MinValue, typeof(float) }; - yield return new object[] { float.MaxValue, typeof(float) }; - yield return new object[] { double.MinValue, typeof(double) }; - yield return new object[] { double.MaxValue, typeof(double) }; -#endif - } - } - - [Theory] - [MemberData(nameof(DeserializeScalarEdgeCases_TestCases))] - public void DeserializeScalarEdgeCases(IConvertible value, Type type) - { - var deserializer = new DeserializerBuilder().Build(); - var result = deserializer.Deserialize(value.ToString(YamlFormatter.Default.NumberFormat), type); - - result.Should().Be(value); - } - - [Fact] - public void DeserializeWithDuplicateKeyChecking_YamlWithDuplicateKeys_ThrowsYamlException() - { - var yaml = @" -name: Jack -momentOfBirth: 1983-04-21T20:21:03.0041599Z -name: Jake -"; - - var sut = new DeserializerBuilder() - .WithNamingConvention(CamelCaseNamingConvention.Instance) - .WithDuplicateKeyChecking() - .Build(); - - Action act = () => sut.Deserialize(yaml); - act.ShouldThrow("Because there are duplicate name keys with concrete class"); - act = () => sut.Deserialize>(yaml); - act.ShouldThrow("Because there are duplicate name keys with dictionary"); - - var stream = Yaml.ReaderFrom("backreference.yaml"); - var parser = new MergingParser(new Parser(stream)); - act = () => sut.Deserialize>>(parser); - act.ShouldThrow("Because there are duplicate name keys with merging parser"); - } - - [Fact] - public void DeserializeWithoutDuplicateKeyChecking_YamlWithDuplicateKeys_DoesNotThrowYamlException() - { - var yaml = @" -name: Jack -momentOfBirth: 1983-04-21T20:21:03.0041599Z -name: Jake -"; - - var sut = new DeserializerBuilder() - .WithNamingConvention(CamelCaseNamingConvention.Instance) - .Build(); - - Action act = () => sut.Deserialize(yaml); - act.ShouldNotThrow("Because duplicate key checking is not enabled"); - act = () => sut.Deserialize>(yaml); - act.ShouldNotThrow("Because duplicate key checking is not enabled"); - - var stream = Yaml.ReaderFrom("backreference.yaml"); - var parser = new MergingParser(new Parser(stream)); - act = () => sut.Deserialize>>(parser); - act.ShouldNotThrow("Because duplicate key checking is not enabled"); - } - - [Fact] - public void SerializeStateMethodsGetCalledOnce() - { - var yaml = "Test: Hi"; - var deserializer = new DeserializerBuilder().Build(); - var test = deserializer.Deserialize(yaml); - - Assert.Equal(1, test.OnDeserializedCallCount); - Assert.Equal(1, test.OnDeserializingCallCount); - } - - public class TestState - { - public int OnDeserializedCallCount { get; set; } - public int OnDeserializingCallCount { get; set; } - - public string Test { get; set; } = string.Empty; - - [OnDeserialized] - public void Deserialized() => OnDeserializedCallCount++; - - [OnDeserializing] - public void Deserializing() => OnDeserializingCallCount++; - } - - public class Test - { - public string Value { get; set; } - } - - public class SetterOnly - { - private string _value; - public string Value { set => _value = value; } - public string Actual { get => _value; } - } - - public class Person - { - public string Name { get; private set; } - - public DateTime MomentOfBirth { get; private set; } - - public IList Cars { get; private set; } - } - - public class Car : ICar - { - public string Name { get; private set; } - - public int Year { get; private set; } - - public IModelSpec Spec { get; private set; } - } - - public interface ICar - { - string Name { get; } - - int Year { get; } - IModelSpec Spec { get; } - } - - public class ModelSpec : IModelSpec - { - public string EngineType { get; private set; } - - public string DriveType { get; private set; } - } - - public interface IModelSpec - { - string EngineType { get; } - - string DriveType { get; } - } - } -} +ο»Ώ// 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 FluentAssertions; +using Xunit; +using YamlDotNet.Core; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.Callbacks; +using YamlDotNet.Serialization.NamingConventions; + +namespace YamlDotNet.Test.Serialization +{ + public class DeserializerTest + { + [Fact] + public void Deserialize_YamlWithInterfaceTypeAndMapping_ReturnsModel() + { + var yaml = @" +name: Jack +momentOfBirth: 1983-04-21T20:21:03.0041599Z +cars: +- name: Mercedes + year: 2018 +- name: Honda + year: 2021 +"; + + var sut = new DeserializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .WithTypeMapping() + .Build(); + + var person = sut.Deserialize(yaml); + person.Name.Should().Be("Jack"); + person.MomentOfBirth.Kind.Should().Be(DateTimeKind.Utc); + person.MomentOfBirth.ToUniversalTime().Year.Should().Be(1983); + person.MomentOfBirth.ToUniversalTime().Month.Should().Be(4); + person.MomentOfBirth.ToUniversalTime().Day.Should().Be(21); + person.MomentOfBirth.ToUniversalTime().Hour.Should().Be(20); + person.MomentOfBirth.ToUniversalTime().Minute.Should().Be(21); + person.MomentOfBirth.ToUniversalTime().Second.Should().Be(3); + person.Cars.Should().HaveCount(2); + person.Cars[0].Name.Should().Be("Mercedes"); + person.Cars[0].Spec.Should().BeNull(); + person.Cars[1].Name.Should().Be("Honda"); + person.Cars[1].Spec.Should().BeNull(); + } + + [Fact] + public void Deserialize_YamlWithTwoInterfaceTypesAndMappings_ReturnsModel() + { + var yaml = @" +name: Jack +momentOfBirth: 1983-04-21T20:21:03.0041599Z +cars: +- name: Mercedes + year: 2018 + spec: + engineType: V6 + driveType: AWD +- name: Honda + year: 2021 + spec: + engineType: V4 + driveType: FWD +"; + + var sut = new DeserializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .WithTypeMapping() + .WithTypeMapping() + .Build(); + + var person = sut.Deserialize(yaml); + person.Name.Should().Be("Jack"); + person.MomentOfBirth.Kind.Should().Be(DateTimeKind.Utc); + person.MomentOfBirth.ToUniversalTime().Year.Should().Be(1983); + person.MomentOfBirth.ToUniversalTime().Month.Should().Be(4); + person.MomentOfBirth.ToUniversalTime().Day.Should().Be(21); + person.MomentOfBirth.ToUniversalTime().Hour.Should().Be(20); + person.MomentOfBirth.ToUniversalTime().Minute.Should().Be(21); + person.MomentOfBirth.ToUniversalTime().Second.Should().Be(3); + person.Cars.Should().HaveCount(2); + person.Cars[0].Name.Should().Be("Mercedes"); + person.Cars[0].Spec.EngineType.Should().Be("V6"); + person.Cars[0].Spec.DriveType.Should().Be("AWD"); + person.Cars[1].Name.Should().Be("Honda"); + person.Cars[1].Spec.EngineType.Should().Be("V4"); + person.Cars[1].Spec.DriveType.Should().Be("FWD"); + } + + [Fact] + public void SetterOnlySetsWithoutException() + { + var yaml = @" +Value: bar +"; + var deserializer = new DeserializerBuilder().Build(); + var result = deserializer.Deserialize(yaml); + result.Actual.Should().Be("bar"); + } + + [Fact] + public void KeysOnDynamicClassDontGetQuoted() + { + var serializer = new SerializerBuilder().WithQuotingNecessaryStrings().Build(); + var deserializer = new DeserializerBuilder().WithAttemptingUnquotedStringTypeDeserialization().Build(); + var yaml = @" +True: null +False: hello +Null: true +X: +"; + var obj = deserializer.Deserialize(yaml, typeof(object)); + var result = serializer.Serialize(obj); + var dictionary = (Dictionary)obj; + var keys = dictionary.Keys.ToArray(); + Assert.Equal(keys, new[] { "True", "False", "Null", "X" }); + Assert.Equal(dictionary.Values, new object[] { null, "hello", true, null }); + } + + [Fact] + public void EmptyQuotedStringsArentNull() + { + var deserializer = new DeserializerBuilder().WithAttemptingUnquotedStringTypeDeserialization().Build(); + var yaml = "Value: \"\""; + var result = deserializer.Deserialize(yaml); + Assert.Equal(string.Empty, result.Value); + } + + [Fact] + public void KeyAnchorIsHandledWithTypeDeserialization() + { + var yaml = @"a: &some_scalar this is also a key +b: &number 1 +*some_scalar: ""will this key be handled correctly?"" +*number: 1"; + var deserializer = new DeserializerBuilder().WithAttemptingUnquotedStringTypeDeserialization().Build(); + var result = deserializer.Deserialize(yaml, typeof(object)); + Assert.IsType>(result); + var dictionary = (Dictionary)result; + Assert.Equal(new object[] { "a", "b", "this is also a key", (byte)1 }, dictionary.Keys); + Assert.Equal(new object[] { "this is also a key", (byte)1, "will this key be handled correctly?", (byte)1 }, dictionary.Values); + } + + [Fact] + public void NonScalarKeyIsHandledWithTypeDeserialization() + { + var yaml = @"scalar: foo +{ a: mapping }: bar +[ a, sequence, 1 ]: baz"; + var deserializer = new DeserializerBuilder().WithAttemptingUnquotedStringTypeDeserialization().Build(); + var result = deserializer.Deserialize(yaml, typeof(object)); + Assert.IsType>(result); + + var dictionary = (Dictionary)result; + var item = dictionary.ElementAt(0); + Assert.Equal("scalar", item.Key); + Assert.Equal("foo", item.Value); + + item = dictionary.ElementAt(1); + Assert.IsType>(item.Key); + Assert.Equal("bar", item.Value); + dictionary = (Dictionary)item.Key; + item = dictionary.ElementAt(0); + Assert.Equal("a", item.Key); + Assert.Equal("mapping", item.Value); + + dictionary = (Dictionary)result; + item = dictionary.ElementAt(2); + Assert.IsType>(item.Key); + Assert.Equal(new List { "a", "sequence", (byte)1 }, (List)item.Key); + Assert.Equal("baz", item.Value); + } + + [Fact] + public void NewLinesInKeys() + { + var yaml = @"? >- + key + + a + + b +: >- + value + + a + + b +"; + var deserializer = new DeserializerBuilder().Build(); + var o = deserializer.Deserialize(yaml, typeof(object)); + Assert.IsType>(o); + var dictionary = (Dictionary)o; + Assert.Equal($"key\na\nb", dictionary.First().Key); + Assert.Equal($"value\na\nb", dictionary.First().Value); + } + + [Theory] + [InlineData(".nan", System.Single.NaN)] + [InlineData(".NaN", System.Single.NaN)] + [InlineData(".NAN", System.Single.NaN)] + [InlineData("-.inf", System.Single.NegativeInfinity)] + [InlineData("+.inf", System.Single.PositiveInfinity)] + [InlineData(".inf", System.Single.PositiveInfinity)] + [InlineData("start.nan", "start.nan")] + [InlineData(".nano", ".nano")] + [InlineData(".infinity", ".infinity")] + [InlineData("www.infinitetechnology.com", "www.infinitetechnology.com")] + [InlineData("https://api.inference.azure.com", "https://api.inference.azure.com")] + public void UnquotedStringTypeDeserializationHandlesInfAndNaN(string yamlValue, object expected) + { + var deserializer = new DeserializerBuilder() + .WithAttemptingUnquotedStringTypeDeserialization().Build(); + var yaml = $"Value: {yamlValue}"; + + var resultDict = deserializer.Deserialize>(yaml); + Assert.True(resultDict.ContainsKey("Value")); + Assert.Equal(expected, resultDict["Value"]); + } + + public static IEnumerable DeserializeScalarEdgeCases_TestCases + { + get + { + yield return new object[] { byte.MinValue, typeof(byte) }; + yield return new object[] { byte.MaxValue, typeof(byte) }; + yield return new object[] { short.MinValue, typeof(short) }; + yield return new object[] { short.MaxValue, typeof(short) }; + yield return new object[] { int.MinValue, typeof(int) }; + yield return new object[] { int.MaxValue, typeof(int) }; + yield return new object[] { long.MinValue, typeof(long) }; + yield return new object[] { long.MaxValue, typeof(long) }; + yield return new object[] { sbyte.MinValue, typeof(sbyte) }; + yield return new object[] { sbyte.MaxValue, typeof(sbyte) }; + yield return new object[] { ushort.MinValue, typeof(ushort) }; + yield return new object[] { ushort.MaxValue, typeof(ushort) }; + yield return new object[] { uint.MinValue, typeof(uint) }; + yield return new object[] { uint.MaxValue, typeof(uint) }; + yield return new object[] { ulong.MinValue, typeof(ulong) }; + yield return new object[] { ulong.MaxValue, typeof(ulong) }; + yield return new object[] { decimal.MinValue, typeof(decimal) }; + yield return new object[] { decimal.MaxValue, typeof(decimal) }; + yield return new object[] { char.MaxValue, typeof(char) }; + +#if NET + yield return new object[] { float.MinValue, typeof(float) }; + yield return new object[] { float.MaxValue, typeof(float) }; + yield return new object[] { double.MinValue, typeof(double) }; + yield return new object[] { double.MaxValue, typeof(double) }; +#endif + } + } + + [Theory] + [MemberData(nameof(DeserializeScalarEdgeCases_TestCases))] + public void DeserializeScalarEdgeCases(IConvertible value, Type type) + { + var deserializer = new DeserializerBuilder().Build(); + var result = deserializer.Deserialize(value.ToString(YamlFormatter.Default.NumberFormat), type); + + result.Should().Be(value); + } + + [Fact] + public void DeserializeWithDuplicateKeyChecking_YamlWithDuplicateKeys_ThrowsYamlException() + { + var yaml = @" +name: Jack +momentOfBirth: 1983-04-21T20:21:03.0041599Z +name: Jake +"; + + var sut = new DeserializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .WithDuplicateKeyChecking() + .Build(); + + Action act = () => sut.Deserialize(yaml); + act.ShouldThrow("Because there are duplicate name keys with concrete class"); + act = () => sut.Deserialize>(yaml); + act.ShouldThrow("Because there are duplicate name keys with dictionary"); + + var stream = Yaml.ReaderFrom("backreference.yaml"); + var parser = new MergingParser(new Parser(stream)); + act = () => sut.Deserialize>>(parser); + act.ShouldThrow("Because there are duplicate name keys with merging parser"); + } + + [Fact] + public void DeserializeWithoutDuplicateKeyChecking_YamlWithDuplicateKeys_DoesNotThrowYamlException() + { + var yaml = @" +name: Jack +momentOfBirth: 1983-04-21T20:21:03.0041599Z +name: Jake +"; + + var sut = new DeserializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .Build(); + + Action act = () => sut.Deserialize(yaml); + act.ShouldNotThrow("Because duplicate key checking is not enabled"); + act = () => sut.Deserialize>(yaml); + act.ShouldNotThrow("Because duplicate key checking is not enabled"); + + var stream = Yaml.ReaderFrom("backreference.yaml"); + var parser = new MergingParser(new Parser(stream)); + act = () => sut.Deserialize>>(parser); + act.ShouldNotThrow("Because duplicate key checking is not enabled"); + } + + [Fact] + public void SerializeStateMethodsGetCalledOnce() + { + var yaml = "Test: Hi"; + var deserializer = new DeserializerBuilder().Build(); + var test = deserializer.Deserialize(yaml); + + Assert.Equal(1, test.OnDeserializedCallCount); + Assert.Equal(1, test.OnDeserializingCallCount); + } + + public class TestState + { + public int OnDeserializedCallCount { get; set; } + public int OnDeserializingCallCount { get; set; } + + public string Test { get; set; } = string.Empty; + + [OnDeserialized] + public void Deserialized() => OnDeserializedCallCount++; + + [OnDeserializing] + public void Deserializing() => OnDeserializingCallCount++; + } + + public class Test + { + public string Value { get; set; } + } + + public class SetterOnly + { + private string _value; + public string Value { set => _value = value; } + public string Actual { get => _value; } + } + + public class Person + { + public string Name { get; private set; } + + public DateTime MomentOfBirth { get; private set; } + + public IList Cars { get; private set; } + } + + public class Car : ICar + { + public string Name { get; private set; } + + public int Year { get; private set; } + + public IModelSpec Spec { get; private set; } + } + + public interface ICar + { + string Name { get; } + + int Year { get; } + IModelSpec Spec { get; } + } + + public class ModelSpec : IModelSpec + { + public string EngineType { get; private set; } + + public string DriveType { get; private set; } + } + + public interface IModelSpec + { + string EngineType { get; } + + string DriveType { get; } + } + } +} diff --git a/YamlDotNet.Test/Serialization/SerializationTests.cs b/YamlDotNet.Test/Serialization/SerializationTests.cs index b2b28010..84af7a64 100644 --- a/YamlDotNet.Test/Serialization/SerializationTests.cs +++ b/YamlDotNet.Test/Serialization/SerializationTests.cs @@ -1,2598 +1,2598 @@ -ο»Ώ// 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 NETCOREAPP3_1_OR_GREATER - 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); + } + + 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; } + } + } + } +} diff --git a/YamlDotNet.Test/YamlDotNet.Test.csproj b/YamlDotNet.Test/YamlDotNet.Test.csproj index 1bb955e1..788ce8f0 100644 --- a/YamlDotNet.Test/YamlDotNet.Test.csproj +++ b/YamlDotNet.Test/YamlDotNet.Test.csproj @@ -1,6 +1,6 @@ ο»Ώ - net80;net70;net60;net47 + net8.0;net7.0;net6.0;net47 false ..\YamlDotNet.snk true diff --git a/YamlDotNet/Helpers/OrderedDictionary.cs b/YamlDotNet/Helpers/OrderedDictionary.cs index 7d5ae95f..1022475b 100644 --- a/YamlDotNet/Helpers/OrderedDictionary.cs +++ b/YamlDotNet/Helpers/OrderedDictionary.cs @@ -1,234 +1,234 @@ -ο»Ώ// 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.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Runtime.Serialization; - -namespace YamlDotNet.Helpers -{ - [Serializable] - internal sealed class OrderedDictionary : IOrderedDictionary - where TKey : notnull - { - [NonSerialized] - private Dictionary dictionary; - private readonly List> list; - private readonly IEqualityComparer comparer; - - public TValue this[TKey key] - { - get => dictionary[key]; - set - { - if (dictionary.ContainsKey(key)) - { - var index = list.FindIndex(kvp => comparer.Equals(kvp.Key, key)); - dictionary[key] = value; - list[index] = new KeyValuePair(key, value); - } - else - { - Add(key, value); - } - } - } - - public ICollection Keys => new KeyCollection(this); - - public ICollection Values => new ValueCollection(this); - - public int Count => dictionary.Count; - - public bool IsReadOnly => false; - - public KeyValuePair this[int index] - { - get => list[index]; - set => list[index] = value; - } - - public OrderedDictionary() : this(EqualityComparer.Default) - { - } - - public OrderedDictionary(IEqualityComparer comparer) - { - list = new List>(); - dictionary = new Dictionary(comparer); - this.comparer = comparer; - } - - public void Add(TKey key, TValue value) => Add(new KeyValuePair(key, value)); - - public void Add(KeyValuePair item) - { - dictionary.Add(item.Key, item.Value); - list.Add(item); - } - - public void Clear() - { - dictionary.Clear(); - list.Clear(); - } - - public bool Contains(KeyValuePair item) => dictionary.Contains(item); - - public bool ContainsKey(TKey key) => dictionary.ContainsKey(key); - - public void CopyTo(KeyValuePair[] array, int arrayIndex) => - list.CopyTo(array, arrayIndex); - - public IEnumerator> GetEnumerator() => list.GetEnumerator(); - - public void Insert(int index, TKey key, TValue value) - { - dictionary.Add(key, value); - list.Insert(index, new KeyValuePair(key, value)); - } - - public bool Remove(TKey key) - { - if (dictionary.ContainsKey(key)) - { - var index = list.FindIndex(kvp => comparer.Equals(kvp.Key, key)); - list.RemoveAt(index); - if (!dictionary.Remove(key)) - { - throw new InvalidOperationException(); - } - return true; - } - else - { - return false; - } - } - - public bool Remove(KeyValuePair item) => Remove(item.Key); - - public void RemoveAt(int index) - { - var key = list[index].Key; - dictionary.Remove(key); - list.RemoveAt(index); - } - -#if !(NETCOREAPP3_1) -#pragma warning disable 8767 // Nullability of reference types in type of parameter ... doesn't match implicitly implemented member -#endif - - public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) => - dictionary.TryGetValue(key, out value); - -#if !(NETCOREAPP3_1) -#pragma warning restore 8767 -#endif - - IEnumerator IEnumerable.GetEnumerator() => list.GetEnumerator(); - - - [OnDeserialized] - internal void OnDeserializedMethod(StreamingContext context) - { - // Reconstruct the dictionary from the serialized list - dictionary = new Dictionary(); - foreach (var kvp in list) - { - dictionary[kvp.Key] = kvp.Value; - } - } - - private class KeyCollection : ICollection - { - private readonly OrderedDictionary orderedDictionary; - - public int Count => orderedDictionary.list.Count; - - public bool IsReadOnly => true; - - public void Add(TKey item) => throw new NotSupportedException(); - - public void Clear() => throw new NotSupportedException(); - - public bool Contains(TKey item) => orderedDictionary.dictionary.Keys.Contains(item); - - public KeyCollection(OrderedDictionary orderedDictionary) - { - this.orderedDictionary = orderedDictionary; - } - - public void CopyTo(TKey[] array, int arrayIndex) - { - for (var i = 0; i < orderedDictionary.list.Count; i++) - { - array[i] = orderedDictionary.list[i + arrayIndex].Key; - } - } - - public IEnumerator GetEnumerator() => - orderedDictionary.list.Select(kvp => kvp.Key).GetEnumerator(); - - public bool Remove(TKey item) => throw new NotSupportedException(); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - } - - private class ValueCollection : ICollection - { - private readonly OrderedDictionary orderedDictionary; - - public int Count => orderedDictionary.list.Count; - - public bool IsReadOnly => true; - - public void Add(TValue item) => throw new NotSupportedException(); - - public void Clear() => throw new NotSupportedException(); - - public bool Contains(TValue item) => orderedDictionary.dictionary.Values.Contains(item); - - public ValueCollection(OrderedDictionary orderedDictionary) - { - this.orderedDictionary = orderedDictionary; - } - - public void CopyTo(TValue[] array, int arrayIndex) - { - for (var i = 0; i < orderedDictionary.list.Count; i++) - { - array[i] = orderedDictionary.list[i + arrayIndex].Value; - } - } - - public IEnumerator GetEnumerator() => - orderedDictionary.list.Select(kvp => kvp.Value).GetEnumerator(); - - public bool Remove(TValue item) => throw new NotSupportedException(); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - } - } -} +ο»Ώ// 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.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Runtime.Serialization; + +namespace YamlDotNet.Helpers +{ + [Serializable] + internal sealed class OrderedDictionary : IOrderedDictionary + where TKey : notnull + { + [NonSerialized] + private Dictionary dictionary; + private readonly List> list; + private readonly IEqualityComparer comparer; + + public TValue this[TKey key] + { + get => dictionary[key]; + set + { + if (dictionary.ContainsKey(key)) + { + var index = list.FindIndex(kvp => comparer.Equals(kvp.Key, key)); + dictionary[key] = value; + list[index] = new KeyValuePair(key, value); + } + else + { + Add(key, value); + } + } + } + + public ICollection Keys => new KeyCollection(this); + + public ICollection Values => new ValueCollection(this); + + public int Count => dictionary.Count; + + public bool IsReadOnly => false; + + public KeyValuePair this[int index] + { + get => list[index]; + set => list[index] = value; + } + + public OrderedDictionary() : this(EqualityComparer.Default) + { + } + + public OrderedDictionary(IEqualityComparer comparer) + { + list = new List>(); + dictionary = new Dictionary(comparer); + this.comparer = comparer; + } + + public void Add(TKey key, TValue value) => Add(new KeyValuePair(key, value)); + + public void Add(KeyValuePair item) + { + dictionary.Add(item.Key, item.Value); + list.Add(item); + } + + public void Clear() + { + dictionary.Clear(); + list.Clear(); + } + + public bool Contains(KeyValuePair item) => dictionary.Contains(item); + + public bool ContainsKey(TKey key) => dictionary.ContainsKey(key); + + public void CopyTo(KeyValuePair[] array, int arrayIndex) => + list.CopyTo(array, arrayIndex); + + public IEnumerator> GetEnumerator() => list.GetEnumerator(); + + public void Insert(int index, TKey key, TValue value) + { + dictionary.Add(key, value); + list.Insert(index, new KeyValuePair(key, value)); + } + + public bool Remove(TKey key) + { + if (dictionary.ContainsKey(key)) + { + var index = list.FindIndex(kvp => comparer.Equals(kvp.Key, key)); + list.RemoveAt(index); + if (!dictionary.Remove(key)) + { + throw new InvalidOperationException(); + } + return true; + } + else + { + return false; + } + } + + public bool Remove(KeyValuePair item) => Remove(item.Key); + + public void RemoveAt(int index) + { + var key = list[index].Key; + dictionary.Remove(key); + list.RemoveAt(index); + } + +#if !NET +#pragma warning disable 8767 // Nullability of reference types in type of parameter ... doesn't match implicitly implemented member +#endif + + public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) => + dictionary.TryGetValue(key, out value); + +#if !NET +#pragma warning restore 8767 +#endif + + IEnumerator IEnumerable.GetEnumerator() => list.GetEnumerator(); + + + [OnDeserialized] + internal void OnDeserializedMethod(StreamingContext context) + { + // Reconstruct the dictionary from the serialized list + dictionary = new Dictionary(); + foreach (var kvp in list) + { + dictionary[kvp.Key] = kvp.Value; + } + } + + private class KeyCollection : ICollection + { + private readonly OrderedDictionary orderedDictionary; + + public int Count => orderedDictionary.list.Count; + + public bool IsReadOnly => true; + + public void Add(TKey item) => throw new NotSupportedException(); + + public void Clear() => throw new NotSupportedException(); + + public bool Contains(TKey item) => orderedDictionary.dictionary.Keys.Contains(item); + + public KeyCollection(OrderedDictionary orderedDictionary) + { + this.orderedDictionary = orderedDictionary; + } + + public void CopyTo(TKey[] array, int arrayIndex) + { + for (var i = 0; i < orderedDictionary.list.Count; i++) + { + array[i] = orderedDictionary.list[i + arrayIndex].Key; + } + } + + public IEnumerator GetEnumerator() => + orderedDictionary.list.Select(kvp => kvp.Key).GetEnumerator(); + + public bool Remove(TKey item) => throw new NotSupportedException(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } + + private class ValueCollection : ICollection + { + private readonly OrderedDictionary orderedDictionary; + + public int Count => orderedDictionary.list.Count; + + public bool IsReadOnly => true; + + public void Add(TValue item) => throw new NotSupportedException(); + + public void Clear() => throw new NotSupportedException(); + + public bool Contains(TValue item) => orderedDictionary.dictionary.Values.Contains(item); + + public ValueCollection(OrderedDictionary orderedDictionary) + { + this.orderedDictionary = orderedDictionary; + } + + public void CopyTo(TValue[] array, int arrayIndex) + { + for (var i = 0; i < orderedDictionary.list.Count; i++) + { + array[i] = orderedDictionary.list[i + arrayIndex].Value; + } + } + + public IEnumerator GetEnumerator() => + orderedDictionary.list.Select(kvp => kvp.Value).GetEnumerator(); + + public bool Remove(TValue item) => throw new NotSupportedException(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } + } +} diff --git a/YamlDotNet/YamlDotNet.csproj b/YamlDotNet/YamlDotNet.csproj index 90b3902e..edb389d2 100644 --- a/YamlDotNet/YamlDotNet.csproj +++ b/YamlDotNet/YamlDotNet.csproj @@ -1,7 +1,7 @@ ο»Ώ - net80;net70;net60;netstandard2.0;netstandard2.1;net47 + net8.0;net7.0;net6.0;netstandard2.0;netstandard2.1;net47 https://github.com/aaubry/YamlDotNet https://github.com/aaubry/YamlDotNet @@ -23,7 +23,7 @@ $(TargetFramework) - + 1591;1574;8600;8602;8604 @@ -36,15 +36,15 @@ true - + true - + true - + true @@ -66,7 +66,7 @@ true - + true diff --git a/YamlDotNet/YamlDotNet.nuspec b/YamlDotNet/YamlDotNet.nuspec index 53dc255f..5426a697 100644 --- a/YamlDotNet/YamlDotNet.nuspec +++ b/YamlDotNet/YamlDotNet.nuspec @@ -26,14 +26,14 @@ - - + + - - + + - - + + diff --git a/appveyor.yml b/appveyor.yml index 57824383..ed09649c 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -35,13 +35,13 @@ artifacts: - path: YamlDotNet\bin\Release\net47 name: Release-Net47 - - path: YamlDotNet\bin\Release\net60 + - path: YamlDotNet\bin\Release\net6.0 name: Release-Net60 - - path: YamlDotNet\bin\Release\net70 + - path: YamlDotNet\bin\Release\net7.0 name: Release-Net70 - - path: YamlDotNet\bin\Release\net80 + - path: YamlDotNet\bin\Release\net8.0 name: Release-Net80 - path: YamlDotNet\bin\*.nupkg diff --git a/tools/build/build.csproj b/tools/build/build.csproj index 25deec28..7d1028c1 100644 --- a/tools/build/build.csproj +++ b/tools/build/build.csproj @@ -2,7 +2,7 @@ Exe - net80 + net8.0 AnyCPU Enable From 2c57ca2bc4472da4611639be9c49845b80ec53d9 Mon Sep 17 00:00:00 2001 From: barapiiro Date: Tue, 28 May 2024 15:52:05 +0300 Subject: [PATCH 06/10] fix hashcode recursion only for the recursive case (anchor) --- YamlDotNet/RepresentationModel/YamlMappingNode.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/YamlDotNet/RepresentationModel/YamlMappingNode.cs b/YamlDotNet/RepresentationModel/YamlMappingNode.cs index 49d793f6..b356f88c 100644 --- a/YamlDotNet/RepresentationModel/YamlMappingNode.cs +++ b/YamlDotNet/RepresentationModel/YamlMappingNode.cs @@ -297,7 +297,9 @@ public override int GetHashCode() foreach (var entry in children) { hashCode = CombineHashCodes(hashCode, entry.Key); - hashCode = CombineHashCodes(hashCode, entry.Value.Start); + hashCode = entry.Value.Anchor.IsEmpty + ? CombineHashCodes(hashCode, entry.Value) + : CombineHashCodes(hashCode, entry.Value.Anchor); } return hashCode; } From cc5f5a6b7af76980b17e7e44d868973c7d5f531b Mon Sep 17 00:00:00 2001 From: barapiiro Date: Tue, 28 May 2024 15:52:57 +0300 Subject: [PATCH 07/10] adding test cases that cause recursive GetHashCode recursive call --- .../RepresentationModel/YamlStreamTests.cs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/YamlDotNet.Test/RepresentationModel/YamlStreamTests.cs b/YamlDotNet.Test/RepresentationModel/YamlStreamTests.cs index d06ec33c..9a5d9379 100644 --- a/YamlDotNet.Test/RepresentationModel/YamlStreamTests.cs +++ b/YamlDotNet.Test/RepresentationModel/YamlStreamTests.cs @@ -45,22 +45,26 @@ public void LoadSimpleDocument() Assert.Equal(YamlNodeType.Scalar, stream.Documents[0].RootNode.NodeType); } - [Fact] - public void AccessingAllNodesOnInfinitelyRecursiveDocumentThrows() + [Theory] + [InlineData("&a [*a]")] + [InlineData("?\n key: &id1\n recursion: *id1\n: foo")] + public void AccessingAllNodesOnInfinitelyRecursiveDocumentThrows(string yaml) { var stream = new YamlStream(); - stream.Load(Yaml.ParserForText("&a [*a]")); + stream.Load(Yaml.ParserForText(yaml)); var accessAllNodes = new Action(() => stream.Documents.Single().AllNodes.ToList()); accessAllNodes.ShouldThrow("because the document is infinitely recursive."); } - [Fact] - public void InfinitelyRecursiveNodeToStringSucceeds() + [Theory] + [InlineData("&a [*a]")] + [InlineData("?\n key: &id1\n recursion: *id1\n: foo")] + public void InfinitelyRecursiveNodeToStringSucceeds(string yaml) { var stream = new YamlStream(); - stream.Load(Yaml.ParserForText("&a [*a]")); + stream.Load(Yaml.ParserForText(yaml)); var toString = stream.Documents.Single().RootNode.ToString(); From abd322479eb6e67da767dae26e6ea1af1a9aaa49 Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Wed, 21 Feb 2024 17:58:57 +0200 Subject: [PATCH 08/10] Include symbols and deterministic build --- .gitignore | 2 +- YamlDotNet/YamlDotNet.csproj | 26 +++++++++++--------------- YamlDotNet/YamlDotNet.nuspec | 8 ++++++++ appveyor.yml | 1 + tools/build/BuildDefinition.cs | 6 +++--- 5 files changed, 24 insertions(+), 19 deletions(-) diff --git a/.gitignore b/.gitignore index 0bc215fb..6dbeb44f 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,7 @@ TestResults *.orig Help/* release/* -/_ReSharper.* +_ReSharper.* *.pidb *.userprefs packages diff --git a/YamlDotNet/YamlDotNet.csproj b/YamlDotNet/YamlDotNet.csproj index edb389d2..51672129 100644 --- a/YamlDotNet/YamlDotNet.csproj +++ b/YamlDotNet/YamlDotNet.csproj @@ -21,6 +21,10 @@ false $(TargetFramework) + + true + true + snupkg @@ -49,6 +53,7 @@ + @@ -58,37 +63,28 @@ $(DefineConstants);RELEASE;TRACE;SIGNED - false - portable - true true + true + + - - 4.3.0 - - - 4.3.0 - - - 4.3.0 - + + + - - - diff --git a/YamlDotNet/YamlDotNet.nuspec b/YamlDotNet/YamlDotNet.nuspec index 5426a697..4c922a3d 100644 --- a/YamlDotNet/YamlDotNet.nuspec +++ b/YamlDotNet/YamlDotNet.nuspec @@ -13,6 +13,7 @@ images/yamldotnet.png yaml parser development library serialization + README.md @@ -24,23 +25,30 @@ + + + + + + + diff --git a/appveyor.yml b/appveyor.yml index ed09649c..2ea55a63 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -45,3 +45,4 @@ artifacts: name: Release-Net80 - path: YamlDotNet\bin\*.nupkg + - path: YamlDotNet\bin\*.snupkg diff --git a/tools/build/BuildDefinition.cs b/tools/build/BuildDefinition.cs index 415a4470..a409e088 100644 --- a/tools/build/BuildDefinition.cs +++ b/tools/build/BuildDefinition.cs @@ -125,7 +125,7 @@ public static Task SetMetadata(GitVersion version) public static Task Build(Options options, MetadataSet _) { var verbosity = options.Verbose ? "detailed" : "minimal"; - Run("dotnet", $"build YamlDotNet.sln --configuration Release --verbosity {verbosity}", BasePath); + Run("dotnet", $"build YamlDotNet.sln --configuration Release --verbosity {verbosity} -p:ContinuousIntegrationBuild=true", BasePath); return Task.FromResult(new SuccessfulBuild()); } @@ -143,14 +143,14 @@ public static Task> Pack(Options options, GitVersion version, var result = new List(); var verbosity = options.Verbose ? "detailed" : "minimal"; var buildDir = Path.Combine(BasePath, "YamlDotNet"); - Run("nuget", $"pack YamlDotNet.nuspec -Version {version.NuGetVersion} -OutputDirectory bin", buildDir); + Run("nuget", $"pack YamlDotNet.nuspec -Version {version.NuGetVersion} -OutputDirectory bin -Symbols -SymbolPackageFormat snupkg", buildDir); var packagePath = Path.Combine(buildDir, "bin", $"YamlDotNet.{version.NuGetVersion}.nupkg"); result.Add(new NuGetPackage(packagePath, "YamlDotNet")); if (PushSerializer) { buildDir = Path.Combine(BasePath, "YamlDotNet.Analyzers.StaticGenerator"); - Run("nuget", $"pack YamlDotNet.Analyzers.StaticGenerator.nuspec -Version {version.NuGetVersion} -OutputDirectory bin", buildDir); + Run("nuget", $"pack YamlDotNet.Analyzers.StaticGenerator.nuspec -Version {version.NuGetVersion} -OutputDirectory bin -Symbols -SymbolPackageFormat snupkg", buildDir); packagePath = Path.Combine(buildDir, "bin", $"YamlDotNet.Analyzers.StaticGenerator.{version.NuGetVersion}.nupkg"); result.Add(new NuGetPackage(packagePath, "YamlDotNet.Analyzers.StaticGenerator")); } From cb1c48ab56ddb2994b21761c905802a38dfdf578 Mon Sep 17 00:00:00 2001 From: MrLuje Date: Sat, 15 Jun 2024 11:42:16 +0000 Subject: [PATCH 09/10] feat: add FsharpOption support --- YamlDotNet.Fsharp.Test/DeserializerTests.fs | 87 ++++++++++ YamlDotNet.Fsharp.Test/SerializerTests.fs | 152 ++++++++++++++++++ .../YamlDotNet.Fsharp.Test.fsproj | 25 +++ YamlDotNet.sln | 6 + YamlDotNet/Helpers/FsharpHelper.cs | 38 +++++ .../ObjectNodeDeserializer.cs | 7 +- .../ScalarNodeDeserializer.cs | 6 +- .../FullObjectGraphTraversalStrategy.cs | 25 ++- .../Serialization/Utilities/TypeConverter.cs | 5 +- 9 files changed, 342 insertions(+), 9 deletions(-) create mode 100644 YamlDotNet.Fsharp.Test/DeserializerTests.fs create mode 100644 YamlDotNet.Fsharp.Test/SerializerTests.fs create mode 100644 YamlDotNet.Fsharp.Test/YamlDotNet.Fsharp.Test.fsproj create mode 100644 YamlDotNet/Helpers/FsharpHelper.cs diff --git a/YamlDotNet.Fsharp.Test/DeserializerTests.fs b/YamlDotNet.Fsharp.Test/DeserializerTests.fs new file mode 100644 index 00000000..3f48a4ff --- /dev/null +++ b/YamlDotNet.Fsharp.Test/DeserializerTests.fs @@ -0,0 +1,87 @@ +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 diff --git a/YamlDotNet.Fsharp.Test/SerializerTests.fs b/YamlDotNet.Fsharp.Test/SerializerTests.fs new file mode 100644 index 00000000..d612647c --- /dev/null +++ b/YamlDotNet.Fsharp.Test/SerializerTests.fs @@ -0,0 +1,152 @@ +module SerializerTests + +open System +open Xunit +open YamlDotNet.Serialization +open YamlDotNet.Serialization.NamingConventions +open FsUnit.Xunit +open YamlDotNet.Core + +[] +type Spec = { + EngineType: string + DriveType: string +} + +[] +type Car = { + Name: string + Year: int + Spec: Spec option + Nickname: string option +} + +[] +type Person = { + Name: string + MomentOfBirth: DateTime + KidsSeat: int option + Cars: Car array +} + +[] +let Serialize_YamlWithScalarOptions() = + let jackTheDriver = { + Name = "Jack" + MomentOfBirth = DateTime(1983, 4, 21, 20, 21, 03, 4) + KidsSeat = Some 1 + Cars = [| + { Name = "Mercedes" + Year = 2018 + Nickname = Some "Jessy" + Spec = None }; + { Name = "Honda" + Year = 2021 + Nickname = None + Spec = None } + |] + } + + let yaml = """name: Jack +momentOfBirth: 1983-04-21T20:21:03.0040000 +kidsSeat: 1 +cars: +- name: Mercedes + year: 2018 + spec: + nickname: Jessy +- name: Honda + year: 2021 + spec: + nickname: +""" + let sut = SerializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .Build() + + let person = sut.Serialize(jackTheDriver) + person |> should equal yaml + + +[] +let Serialize_YamlWithScalarOptions_OmitNull() = + let jackTheDriver = { + Name = "Jack" + MomentOfBirth = DateTime(1983, 4, 21, 20, 21, 03, 4) + KidsSeat = Some 1 + Cars = [| + { Name = "Mercedes" + Year = 2018 + Nickname = Some "Jessy" + Spec = None }; + { Name = "Honda" + Year = 2021 + Nickname = None + Spec = None } + |] + } + + let yaml = """name: Jack +momentOfBirth: 1983-04-21T20:21:03.0040000 +kidsSeat: 1 +cars: +- name: Mercedes + year: 2018 + nickname: Jessy +- name: Honda + year: 2021 +""" + let sut = SerializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitNull) + .Build() + + let person = sut.Serialize(jackTheDriver) + person |> should equal yaml + + +[] +let Serialize_YamlWithObjectOptions_OmitNull() = + let jackTheDriver = { + Name = "Jack" + MomentOfBirth = DateTime(1983, 4, 21, 20, 21, 03, 4) + KidsSeat = Some 1 + Cars = [| + { Name = "Mercedes" + Year = 2018 + Nickname = None + Spec = Some { + EngineType = "V6" + DriveType = "AWD" + } }; + { Name = "Honda" + Year = 2021 + Nickname = None + Spec = None } + |] + } + + let yaml = """name: Jack +momentOfBirth: 1983-04-21T20:21:03.0040000 +kidsSeat: 1 +cars: +- name: Mercedes + year: 2018 + spec: + engineType: V6 + driveType: AWD +- name: Honda + year: 2021 +""" + let sut = SerializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitNull) + .Build() + + let person = sut.Serialize(jackTheDriver) + person |> should equal yaml + +type TestOmit = { + name: string + plop: int option +} diff --git a/YamlDotNet.Fsharp.Test/YamlDotNet.Fsharp.Test.fsproj b/YamlDotNet.Fsharp.Test/YamlDotNet.Fsharp.Test.fsproj new file mode 100644 index 00000000..300cec1f --- /dev/null +++ b/YamlDotNet.Fsharp.Test/YamlDotNet.Fsharp.Test.fsproj @@ -0,0 +1,25 @@ + + + net8.0;net7.0;net6.0;net47 + false + ..\YamlDotNet.snk + true + 8.0 + true + + + + + + + + + + + + + + + + + diff --git a/YamlDotNet.sln b/YamlDotNet.sln index e7d7d763..b88747fc 100644 --- a/YamlDotNet.sln +++ b/YamlDotNet.sln @@ -31,6 +31,8 @@ Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "YamlDotNet.Samples.Fsharp", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YamlDotNet.Core7AoTCompileTest.Model", "YamlDotNet.Core7AoTCompileTest.Model\YamlDotNet.Core7AoTCompileTest.Model.csproj", "{BFE15564-7C2C-47DA-8302-9BCB39B6864B}" EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "YamlDotNet.Fsharp.Test", "YamlDotNet.Fsharp.Test\YamlDotNet.Fsharp.Test.fsproj", "{294EFEB3-4DC2-4105-ADE7-E429F5522419}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -69,6 +71,10 @@ Global {BFE15564-7C2C-47DA-8302-9BCB39B6864B}.Debug|Any CPU.Build.0 = Debug|Any CPU {BFE15564-7C2C-47DA-8302-9BCB39B6864B}.Release|Any CPU.ActiveCfg = Release|Any CPU {BFE15564-7C2C-47DA-8302-9BCB39B6864B}.Release|Any CPU.Build.0 = Release|Any CPU + {294EFEB3-4DC2-4105-ADE7-E429F5522419}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {294EFEB3-4DC2-4105-ADE7-E429F5522419}.Debug|Any CPU.Build.0 = Debug|Any CPU + {294EFEB3-4DC2-4105-ADE7-E429F5522419}.Release|Any CPU.ActiveCfg = Release|Any CPU + {294EFEB3-4DC2-4105-ADE7-E429F5522419}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/YamlDotNet/Helpers/FsharpHelper.cs b/YamlDotNet/Helpers/FsharpHelper.cs new file mode 100644 index 00000000..304164b5 --- /dev/null +++ b/YamlDotNet/Helpers/FsharpHelper.cs @@ -0,0 +1,38 @@ +using System; +using YamlDotNet.Serialization; + +namespace YamlDotNet.Helpers +{ + public static class FsharpHelper + { + private static bool IsFsharp(Type t) + { + return t.Namespace == "Microsoft.FSharp.Core"; + } + + public static bool IsOptionType(Type t) + { + return IsFsharp(t) && t.Name == "FSharpOption`1"; + } + + public static Type? GetOptionUnderlyingType(Type t) + { + return t.IsGenericType && IsOptionType(t) ? t.GenericTypeArguments[0] : null; + } + + public static object? GetValue(IObjectDescriptor objectDescriptor) + { + if (!IsOptionType(objectDescriptor.Type)) + { + throw new InvalidOperationException("Should not be called on non-Option<> type"); + } + + if (objectDescriptor.Value is null) + { + return null; + } + + return objectDescriptor.Type.GetProperty("Value").GetValue(objectDescriptor.Value); + } + } +} diff --git a/YamlDotNet/Serialization/NodeDeserializers/ObjectNodeDeserializer.cs b/YamlDotNet/Serialization/NodeDeserializers/ObjectNodeDeserializer.cs index da882c50..06dbb6b0 100644 --- a/YamlDotNet/Serialization/NodeDeserializers/ObjectNodeDeserializer.cs +++ b/YamlDotNet/Serialization/NodeDeserializers/ObjectNodeDeserializer.cs @@ -24,6 +24,7 @@ using System.Runtime.Serialization; using YamlDotNet.Core; using YamlDotNet.Core.Events; +using YamlDotNet.Helpers; using YamlDotNet.Serialization.Utilities; namespace YamlDotNet.Serialization.NodeDeserializers @@ -60,8 +61,10 @@ public bool Deserialize(IParser parser, Type expectedType, Func(object name, IObjectDescriptor value, break; } - var underlyingType = Nullable.GetUnderlyingType(value.Type); - if (underlyingType != null) + var nullableUnderlyingType = Nullable.GetUnderlyingType(value.Type); + var optionUnderlyingType = nullableUnderlyingType ?? FsharpHelper.GetOptionUnderlyingType(value.Type); + var optionValue = optionUnderlyingType != null ? FsharpHelper.GetValue(value) : null; + + if (nullableUnderlyingType != null) { // 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("Value", new ObjectDescriptor(value.Value, underlyingType, value.Type, value.ScalarStyle), visitor, context, path); + Traverse( + "Value", + new ObjectDescriptor(value.Value, nullableUnderlyingType, value.Type, value.ScalarStyle), + visitor, + context, + path + ); + } + else if (optionUnderlyingType != null && optionValue != null) + { + Traverse( + "Value", + new ObjectDescriptor(FsharpHelper.GetValue(value), optionUnderlyingType, value.Type, value.ScalarStyle), + visitor, + context, + path + ); } else { diff --git a/YamlDotNet/Serialization/Utilities/TypeConverter.cs b/YamlDotNet/Serialization/Utilities/TypeConverter.cs index 7bd79065..d8efd0f1 100644 --- a/YamlDotNet/Serialization/Utilities/TypeConverter.cs +++ b/YamlDotNet/Serialization/Utilities/TypeConverter.cs @@ -27,6 +27,7 @@ using System.Globalization; using System.Reflection; using System.ComponentModel; +using YamlDotNet.Helpers; namespace YamlDotNet.Serialization.Utilities { @@ -122,11 +123,11 @@ public static T ChangeType(object? value, CultureInfo culture, INamingConvent return value; } - // Nullable types get a special treatment + // Nullable & fsharp option types get a special treatment if (destinationType.IsGenericType()) { var genericTypeDefinition = destinationType.GetGenericTypeDefinition(); - if (genericTypeDefinition == typeof(Nullable<>)) + if (genericTypeDefinition == typeof(Nullable<>) || FsharpHelper.IsOptionType(genericTypeDefinition)) { var innerType = destinationType.GetGenericArguments()[0]; var convertedValue = ChangeType(value, innerType, culture, enumNamingConvention); From 2d332aead69dae53d43481610721c338caa51d3f Mon Sep 17 00:00:00 2001 From: Edward Cooke Date: Sun, 14 Jul 2024 07:18:53 -0600 Subject: [PATCH 10/10] Line endings in a few files? --- .../Serialization/DeserializerTest.cs | 826 +-- .../Serialization/SerializationTests.cs | 5196 ++++++++--------- YamlDotNet/Helpers/OrderedDictionary.cs | 468 +- 3 files changed, 3245 insertions(+), 3245 deletions(-) diff --git a/YamlDotNet.Test/Serialization/DeserializerTest.cs b/YamlDotNet.Test/Serialization/DeserializerTest.cs index 44ddf124..171ff957 100644 --- a/YamlDotNet.Test/Serialization/DeserializerTest.cs +++ b/YamlDotNet.Test/Serialization/DeserializerTest.cs @@ -1,413 +1,413 @@ -ο»Ώ// 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 FluentAssertions; -using Xunit; -using YamlDotNet.Core; -using YamlDotNet.Serialization; -using YamlDotNet.Serialization.Callbacks; -using YamlDotNet.Serialization.NamingConventions; - -namespace YamlDotNet.Test.Serialization -{ - public class DeserializerTest - { - [Fact] - public void Deserialize_YamlWithInterfaceTypeAndMapping_ReturnsModel() - { - var yaml = @" -name: Jack -momentOfBirth: 1983-04-21T20:21:03.0041599Z -cars: -- name: Mercedes - year: 2018 -- name: Honda - year: 2021 -"; - - var sut = new DeserializerBuilder() - .WithNamingConvention(CamelCaseNamingConvention.Instance) - .WithTypeMapping() - .Build(); - - var person = sut.Deserialize(yaml); - person.Name.Should().Be("Jack"); - person.MomentOfBirth.Kind.Should().Be(DateTimeKind.Utc); - person.MomentOfBirth.ToUniversalTime().Year.Should().Be(1983); - person.MomentOfBirth.ToUniversalTime().Month.Should().Be(4); - person.MomentOfBirth.ToUniversalTime().Day.Should().Be(21); - person.MomentOfBirth.ToUniversalTime().Hour.Should().Be(20); - person.MomentOfBirth.ToUniversalTime().Minute.Should().Be(21); - person.MomentOfBirth.ToUniversalTime().Second.Should().Be(3); - person.Cars.Should().HaveCount(2); - person.Cars[0].Name.Should().Be("Mercedes"); - person.Cars[0].Spec.Should().BeNull(); - person.Cars[1].Name.Should().Be("Honda"); - person.Cars[1].Spec.Should().BeNull(); - } - - [Fact] - public void Deserialize_YamlWithTwoInterfaceTypesAndMappings_ReturnsModel() - { - var yaml = @" -name: Jack -momentOfBirth: 1983-04-21T20:21:03.0041599Z -cars: -- name: Mercedes - year: 2018 - spec: - engineType: V6 - driveType: AWD -- name: Honda - year: 2021 - spec: - engineType: V4 - driveType: FWD -"; - - var sut = new DeserializerBuilder() - .WithNamingConvention(CamelCaseNamingConvention.Instance) - .WithTypeMapping() - .WithTypeMapping() - .Build(); - - var person = sut.Deserialize(yaml); - person.Name.Should().Be("Jack"); - person.MomentOfBirth.Kind.Should().Be(DateTimeKind.Utc); - person.MomentOfBirth.ToUniversalTime().Year.Should().Be(1983); - person.MomentOfBirth.ToUniversalTime().Month.Should().Be(4); - person.MomentOfBirth.ToUniversalTime().Day.Should().Be(21); - person.MomentOfBirth.ToUniversalTime().Hour.Should().Be(20); - person.MomentOfBirth.ToUniversalTime().Minute.Should().Be(21); - person.MomentOfBirth.ToUniversalTime().Second.Should().Be(3); - person.Cars.Should().HaveCount(2); - person.Cars[0].Name.Should().Be("Mercedes"); - person.Cars[0].Spec.EngineType.Should().Be("V6"); - person.Cars[0].Spec.DriveType.Should().Be("AWD"); - person.Cars[1].Name.Should().Be("Honda"); - person.Cars[1].Spec.EngineType.Should().Be("V4"); - person.Cars[1].Spec.DriveType.Should().Be("FWD"); - } - - [Fact] - public void SetterOnlySetsWithoutException() - { - var yaml = @" -Value: bar -"; - var deserializer = new DeserializerBuilder().Build(); - var result = deserializer.Deserialize(yaml); - result.Actual.Should().Be("bar"); - } - - [Fact] - public void KeysOnDynamicClassDontGetQuoted() - { - var serializer = new SerializerBuilder().WithQuotingNecessaryStrings().Build(); - var deserializer = new DeserializerBuilder().WithAttemptingUnquotedStringTypeDeserialization().Build(); - var yaml = @" -True: null -False: hello -Null: true -X: -"; - var obj = deserializer.Deserialize(yaml, typeof(object)); - var result = serializer.Serialize(obj); - var dictionary = (Dictionary)obj; - var keys = dictionary.Keys.ToArray(); - Assert.Equal(keys, new[] { "True", "False", "Null", "X" }); - Assert.Equal(dictionary.Values, new object[] { null, "hello", true, null }); - } - - [Fact] - public void EmptyQuotedStringsArentNull() - { - var deserializer = new DeserializerBuilder().WithAttemptingUnquotedStringTypeDeserialization().Build(); - var yaml = "Value: \"\""; - var result = deserializer.Deserialize(yaml); - Assert.Equal(string.Empty, result.Value); - } - - [Fact] - public void KeyAnchorIsHandledWithTypeDeserialization() - { - var yaml = @"a: &some_scalar this is also a key -b: &number 1 -*some_scalar: ""will this key be handled correctly?"" -*number: 1"; - var deserializer = new DeserializerBuilder().WithAttemptingUnquotedStringTypeDeserialization().Build(); - var result = deserializer.Deserialize(yaml, typeof(object)); - Assert.IsType>(result); - var dictionary = (Dictionary)result; - Assert.Equal(new object[] { "a", "b", "this is also a key", (byte)1 }, dictionary.Keys); - Assert.Equal(new object[] { "this is also a key", (byte)1, "will this key be handled correctly?", (byte)1 }, dictionary.Values); - } - - [Fact] - public void NonScalarKeyIsHandledWithTypeDeserialization() - { - var yaml = @"scalar: foo -{ a: mapping }: bar -[ a, sequence, 1 ]: baz"; - var deserializer = new DeserializerBuilder().WithAttemptingUnquotedStringTypeDeserialization().Build(); - var result = deserializer.Deserialize(yaml, typeof(object)); - Assert.IsType>(result); - - var dictionary = (Dictionary)result; - var item = dictionary.ElementAt(0); - Assert.Equal("scalar", item.Key); - Assert.Equal("foo", item.Value); - - item = dictionary.ElementAt(1); - Assert.IsType>(item.Key); - Assert.Equal("bar", item.Value); - dictionary = (Dictionary)item.Key; - item = dictionary.ElementAt(0); - Assert.Equal("a", item.Key); - Assert.Equal("mapping", item.Value); - - dictionary = (Dictionary)result; - item = dictionary.ElementAt(2); - Assert.IsType>(item.Key); - Assert.Equal(new List { "a", "sequence", (byte)1 }, (List)item.Key); - Assert.Equal("baz", item.Value); - } - - [Fact] - public void NewLinesInKeys() - { - var yaml = @"? >- - key - - a - - b -: >- - value - - a - - b -"; - var deserializer = new DeserializerBuilder().Build(); - var o = deserializer.Deserialize(yaml, typeof(object)); - Assert.IsType>(o); - var dictionary = (Dictionary)o; - Assert.Equal($"key\na\nb", dictionary.First().Key); - Assert.Equal($"value\na\nb", dictionary.First().Value); - } - - [Theory] - [InlineData(".nan", System.Single.NaN)] - [InlineData(".NaN", System.Single.NaN)] - [InlineData(".NAN", System.Single.NaN)] - [InlineData("-.inf", System.Single.NegativeInfinity)] - [InlineData("+.inf", System.Single.PositiveInfinity)] - [InlineData(".inf", System.Single.PositiveInfinity)] - [InlineData("start.nan", "start.nan")] - [InlineData(".nano", ".nano")] - [InlineData(".infinity", ".infinity")] - [InlineData("www.infinitetechnology.com", "www.infinitetechnology.com")] - [InlineData("https://api.inference.azure.com", "https://api.inference.azure.com")] - public void UnquotedStringTypeDeserializationHandlesInfAndNaN(string yamlValue, object expected) - { - var deserializer = new DeserializerBuilder() - .WithAttemptingUnquotedStringTypeDeserialization().Build(); - var yaml = $"Value: {yamlValue}"; - - var resultDict = deserializer.Deserialize>(yaml); - Assert.True(resultDict.ContainsKey("Value")); - Assert.Equal(expected, resultDict["Value"]); - } - - public static IEnumerable DeserializeScalarEdgeCases_TestCases - { - get - { - yield return new object[] { byte.MinValue, typeof(byte) }; - yield return new object[] { byte.MaxValue, typeof(byte) }; - yield return new object[] { short.MinValue, typeof(short) }; - yield return new object[] { short.MaxValue, typeof(short) }; - yield return new object[] { int.MinValue, typeof(int) }; - yield return new object[] { int.MaxValue, typeof(int) }; - yield return new object[] { long.MinValue, typeof(long) }; - yield return new object[] { long.MaxValue, typeof(long) }; - yield return new object[] { sbyte.MinValue, typeof(sbyte) }; - yield return new object[] { sbyte.MaxValue, typeof(sbyte) }; - yield return new object[] { ushort.MinValue, typeof(ushort) }; - yield return new object[] { ushort.MaxValue, typeof(ushort) }; - yield return new object[] { uint.MinValue, typeof(uint) }; - yield return new object[] { uint.MaxValue, typeof(uint) }; - yield return new object[] { ulong.MinValue, typeof(ulong) }; - yield return new object[] { ulong.MaxValue, typeof(ulong) }; - yield return new object[] { decimal.MinValue, typeof(decimal) }; - yield return new object[] { decimal.MaxValue, typeof(decimal) }; - yield return new object[] { char.MaxValue, typeof(char) }; - -#if NET - yield return new object[] { float.MinValue, typeof(float) }; - yield return new object[] { float.MaxValue, typeof(float) }; - yield return new object[] { double.MinValue, typeof(double) }; - yield return new object[] { double.MaxValue, typeof(double) }; -#endif - } - } - - [Theory] - [MemberData(nameof(DeserializeScalarEdgeCases_TestCases))] - public void DeserializeScalarEdgeCases(IConvertible value, Type type) - { - var deserializer = new DeserializerBuilder().Build(); - var result = deserializer.Deserialize(value.ToString(YamlFormatter.Default.NumberFormat), type); - - result.Should().Be(value); - } - - [Fact] - public void DeserializeWithDuplicateKeyChecking_YamlWithDuplicateKeys_ThrowsYamlException() - { - var yaml = @" -name: Jack -momentOfBirth: 1983-04-21T20:21:03.0041599Z -name: Jake -"; - - var sut = new DeserializerBuilder() - .WithNamingConvention(CamelCaseNamingConvention.Instance) - .WithDuplicateKeyChecking() - .Build(); - - Action act = () => sut.Deserialize(yaml); - act.ShouldThrow("Because there are duplicate name keys with concrete class"); - act = () => sut.Deserialize>(yaml); - act.ShouldThrow("Because there are duplicate name keys with dictionary"); - - var stream = Yaml.ReaderFrom("backreference.yaml"); - var parser = new MergingParser(new Parser(stream)); - act = () => sut.Deserialize>>(parser); - act.ShouldThrow("Because there are duplicate name keys with merging parser"); - } - - [Fact] - public void DeserializeWithoutDuplicateKeyChecking_YamlWithDuplicateKeys_DoesNotThrowYamlException() - { - var yaml = @" -name: Jack -momentOfBirth: 1983-04-21T20:21:03.0041599Z -name: Jake -"; - - var sut = new DeserializerBuilder() - .WithNamingConvention(CamelCaseNamingConvention.Instance) - .Build(); - - Action act = () => sut.Deserialize(yaml); - act.ShouldNotThrow("Because duplicate key checking is not enabled"); - act = () => sut.Deserialize>(yaml); - act.ShouldNotThrow("Because duplicate key checking is not enabled"); - - var stream = Yaml.ReaderFrom("backreference.yaml"); - var parser = new MergingParser(new Parser(stream)); - act = () => sut.Deserialize>>(parser); - act.ShouldNotThrow("Because duplicate key checking is not enabled"); - } - - [Fact] - public void SerializeStateMethodsGetCalledOnce() - { - var yaml = "Test: Hi"; - var deserializer = new DeserializerBuilder().Build(); - var test = deserializer.Deserialize(yaml); - - Assert.Equal(1, test.OnDeserializedCallCount); - Assert.Equal(1, test.OnDeserializingCallCount); - } - - public class TestState - { - public int OnDeserializedCallCount { get; set; } - public int OnDeserializingCallCount { get; set; } - - public string Test { get; set; } = string.Empty; - - [OnDeserialized] - public void Deserialized() => OnDeserializedCallCount++; - - [OnDeserializing] - public void Deserializing() => OnDeserializingCallCount++; - } - - public class Test - { - public string Value { get; set; } - } - - public class SetterOnly - { - private string _value; - public string Value { set => _value = value; } - public string Actual { get => _value; } - } - - public class Person - { - public string Name { get; private set; } - - public DateTime MomentOfBirth { get; private set; } - - public IList Cars { get; private set; } - } - - public class Car : ICar - { - public string Name { get; private set; } - - public int Year { get; private set; } - - public IModelSpec Spec { get; private set; } - } - - public interface ICar - { - string Name { get; } - - int Year { get; } - IModelSpec Spec { get; } - } - - public class ModelSpec : IModelSpec - { - public string EngineType { get; private set; } - - public string DriveType { get; private set; } - } - - public interface IModelSpec - { - string EngineType { get; } - - string DriveType { get; } - } - } -} +ο»Ώ// 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 FluentAssertions; +using Xunit; +using YamlDotNet.Core; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.Callbacks; +using YamlDotNet.Serialization.NamingConventions; + +namespace YamlDotNet.Test.Serialization +{ + public class DeserializerTest + { + [Fact] + public void Deserialize_YamlWithInterfaceTypeAndMapping_ReturnsModel() + { + var yaml = @" +name: Jack +momentOfBirth: 1983-04-21T20:21:03.0041599Z +cars: +- name: Mercedes + year: 2018 +- name: Honda + year: 2021 +"; + + var sut = new DeserializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .WithTypeMapping() + .Build(); + + var person = sut.Deserialize(yaml); + person.Name.Should().Be("Jack"); + person.MomentOfBirth.Kind.Should().Be(DateTimeKind.Utc); + person.MomentOfBirth.ToUniversalTime().Year.Should().Be(1983); + person.MomentOfBirth.ToUniversalTime().Month.Should().Be(4); + person.MomentOfBirth.ToUniversalTime().Day.Should().Be(21); + person.MomentOfBirth.ToUniversalTime().Hour.Should().Be(20); + person.MomentOfBirth.ToUniversalTime().Minute.Should().Be(21); + person.MomentOfBirth.ToUniversalTime().Second.Should().Be(3); + person.Cars.Should().HaveCount(2); + person.Cars[0].Name.Should().Be("Mercedes"); + person.Cars[0].Spec.Should().BeNull(); + person.Cars[1].Name.Should().Be("Honda"); + person.Cars[1].Spec.Should().BeNull(); + } + + [Fact] + public void Deserialize_YamlWithTwoInterfaceTypesAndMappings_ReturnsModel() + { + var yaml = @" +name: Jack +momentOfBirth: 1983-04-21T20:21:03.0041599Z +cars: +- name: Mercedes + year: 2018 + spec: + engineType: V6 + driveType: AWD +- name: Honda + year: 2021 + spec: + engineType: V4 + driveType: FWD +"; + + var sut = new DeserializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .WithTypeMapping() + .WithTypeMapping() + .Build(); + + var person = sut.Deserialize(yaml); + person.Name.Should().Be("Jack"); + person.MomentOfBirth.Kind.Should().Be(DateTimeKind.Utc); + person.MomentOfBirth.ToUniversalTime().Year.Should().Be(1983); + person.MomentOfBirth.ToUniversalTime().Month.Should().Be(4); + person.MomentOfBirth.ToUniversalTime().Day.Should().Be(21); + person.MomentOfBirth.ToUniversalTime().Hour.Should().Be(20); + person.MomentOfBirth.ToUniversalTime().Minute.Should().Be(21); + person.MomentOfBirth.ToUniversalTime().Second.Should().Be(3); + person.Cars.Should().HaveCount(2); + person.Cars[0].Name.Should().Be("Mercedes"); + person.Cars[0].Spec.EngineType.Should().Be("V6"); + person.Cars[0].Spec.DriveType.Should().Be("AWD"); + person.Cars[1].Name.Should().Be("Honda"); + person.Cars[1].Spec.EngineType.Should().Be("V4"); + person.Cars[1].Spec.DriveType.Should().Be("FWD"); + } + + [Fact] + public void SetterOnlySetsWithoutException() + { + var yaml = @" +Value: bar +"; + var deserializer = new DeserializerBuilder().Build(); + var result = deserializer.Deserialize(yaml); + result.Actual.Should().Be("bar"); + } + + [Fact] + public void KeysOnDynamicClassDontGetQuoted() + { + var serializer = new SerializerBuilder().WithQuotingNecessaryStrings().Build(); + var deserializer = new DeserializerBuilder().WithAttemptingUnquotedStringTypeDeserialization().Build(); + var yaml = @" +True: null +False: hello +Null: true +X: +"; + var obj = deserializer.Deserialize(yaml, typeof(object)); + var result = serializer.Serialize(obj); + var dictionary = (Dictionary)obj; + var keys = dictionary.Keys.ToArray(); + Assert.Equal(keys, new[] { "True", "False", "Null", "X" }); + Assert.Equal(dictionary.Values, new object[] { null, "hello", true, null }); + } + + [Fact] + public void EmptyQuotedStringsArentNull() + { + var deserializer = new DeserializerBuilder().WithAttemptingUnquotedStringTypeDeserialization().Build(); + var yaml = "Value: \"\""; + var result = deserializer.Deserialize(yaml); + Assert.Equal(string.Empty, result.Value); + } + + [Fact] + public void KeyAnchorIsHandledWithTypeDeserialization() + { + var yaml = @"a: &some_scalar this is also a key +b: &number 1 +*some_scalar: ""will this key be handled correctly?"" +*number: 1"; + var deserializer = new DeserializerBuilder().WithAttemptingUnquotedStringTypeDeserialization().Build(); + var result = deserializer.Deserialize(yaml, typeof(object)); + Assert.IsType>(result); + var dictionary = (Dictionary)result; + Assert.Equal(new object[] { "a", "b", "this is also a key", (byte)1 }, dictionary.Keys); + Assert.Equal(new object[] { "this is also a key", (byte)1, "will this key be handled correctly?", (byte)1 }, dictionary.Values); + } + + [Fact] + public void NonScalarKeyIsHandledWithTypeDeserialization() + { + var yaml = @"scalar: foo +{ a: mapping }: bar +[ a, sequence, 1 ]: baz"; + var deserializer = new DeserializerBuilder().WithAttemptingUnquotedStringTypeDeserialization().Build(); + var result = deserializer.Deserialize(yaml, typeof(object)); + Assert.IsType>(result); + + var dictionary = (Dictionary)result; + var item = dictionary.ElementAt(0); + Assert.Equal("scalar", item.Key); + Assert.Equal("foo", item.Value); + + item = dictionary.ElementAt(1); + Assert.IsType>(item.Key); + Assert.Equal("bar", item.Value); + dictionary = (Dictionary)item.Key; + item = dictionary.ElementAt(0); + Assert.Equal("a", item.Key); + Assert.Equal("mapping", item.Value); + + dictionary = (Dictionary)result; + item = dictionary.ElementAt(2); + Assert.IsType>(item.Key); + Assert.Equal(new List { "a", "sequence", (byte)1 }, (List)item.Key); + Assert.Equal("baz", item.Value); + } + + [Fact] + public void NewLinesInKeys() + { + var yaml = @"? >- + key + + a + + b +: >- + value + + a + + b +"; + var deserializer = new DeserializerBuilder().Build(); + var o = deserializer.Deserialize(yaml, typeof(object)); + Assert.IsType>(o); + var dictionary = (Dictionary)o; + Assert.Equal($"key\na\nb", dictionary.First().Key); + Assert.Equal($"value\na\nb", dictionary.First().Value); + } + + [Theory] + [InlineData(".nan", System.Single.NaN)] + [InlineData(".NaN", System.Single.NaN)] + [InlineData(".NAN", System.Single.NaN)] + [InlineData("-.inf", System.Single.NegativeInfinity)] + [InlineData("+.inf", System.Single.PositiveInfinity)] + [InlineData(".inf", System.Single.PositiveInfinity)] + [InlineData("start.nan", "start.nan")] + [InlineData(".nano", ".nano")] + [InlineData(".infinity", ".infinity")] + [InlineData("www.infinitetechnology.com", "www.infinitetechnology.com")] + [InlineData("https://api.inference.azure.com", "https://api.inference.azure.com")] + public void UnquotedStringTypeDeserializationHandlesInfAndNaN(string yamlValue, object expected) + { + var deserializer = new DeserializerBuilder() + .WithAttemptingUnquotedStringTypeDeserialization().Build(); + var yaml = $"Value: {yamlValue}"; + + var resultDict = deserializer.Deserialize>(yaml); + Assert.True(resultDict.ContainsKey("Value")); + Assert.Equal(expected, resultDict["Value"]); + } + + public static IEnumerable DeserializeScalarEdgeCases_TestCases + { + get + { + yield return new object[] { byte.MinValue, typeof(byte) }; + yield return new object[] { byte.MaxValue, typeof(byte) }; + yield return new object[] { short.MinValue, typeof(short) }; + yield return new object[] { short.MaxValue, typeof(short) }; + yield return new object[] { int.MinValue, typeof(int) }; + yield return new object[] { int.MaxValue, typeof(int) }; + yield return new object[] { long.MinValue, typeof(long) }; + yield return new object[] { long.MaxValue, typeof(long) }; + yield return new object[] { sbyte.MinValue, typeof(sbyte) }; + yield return new object[] { sbyte.MaxValue, typeof(sbyte) }; + yield return new object[] { ushort.MinValue, typeof(ushort) }; + yield return new object[] { ushort.MaxValue, typeof(ushort) }; + yield return new object[] { uint.MinValue, typeof(uint) }; + yield return new object[] { uint.MaxValue, typeof(uint) }; + yield return new object[] { ulong.MinValue, typeof(ulong) }; + yield return new object[] { ulong.MaxValue, typeof(ulong) }; + yield return new object[] { decimal.MinValue, typeof(decimal) }; + yield return new object[] { decimal.MaxValue, typeof(decimal) }; + yield return new object[] { char.MaxValue, typeof(char) }; + +#if NET + yield return new object[] { float.MinValue, typeof(float) }; + yield return new object[] { float.MaxValue, typeof(float) }; + yield return new object[] { double.MinValue, typeof(double) }; + yield return new object[] { double.MaxValue, typeof(double) }; +#endif + } + } + + [Theory] + [MemberData(nameof(DeserializeScalarEdgeCases_TestCases))] + public void DeserializeScalarEdgeCases(IConvertible value, Type type) + { + var deserializer = new DeserializerBuilder().Build(); + var result = deserializer.Deserialize(value.ToString(YamlFormatter.Default.NumberFormat), type); + + result.Should().Be(value); + } + + [Fact] + public void DeserializeWithDuplicateKeyChecking_YamlWithDuplicateKeys_ThrowsYamlException() + { + var yaml = @" +name: Jack +momentOfBirth: 1983-04-21T20:21:03.0041599Z +name: Jake +"; + + var sut = new DeserializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .WithDuplicateKeyChecking() + .Build(); + + Action act = () => sut.Deserialize(yaml); + act.ShouldThrow("Because there are duplicate name keys with concrete class"); + act = () => sut.Deserialize>(yaml); + act.ShouldThrow("Because there are duplicate name keys with dictionary"); + + var stream = Yaml.ReaderFrom("backreference.yaml"); + var parser = new MergingParser(new Parser(stream)); + act = () => sut.Deserialize>>(parser); + act.ShouldThrow("Because there are duplicate name keys with merging parser"); + } + + [Fact] + public void DeserializeWithoutDuplicateKeyChecking_YamlWithDuplicateKeys_DoesNotThrowYamlException() + { + var yaml = @" +name: Jack +momentOfBirth: 1983-04-21T20:21:03.0041599Z +name: Jake +"; + + var sut = new DeserializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .Build(); + + Action act = () => sut.Deserialize(yaml); + act.ShouldNotThrow("Because duplicate key checking is not enabled"); + act = () => sut.Deserialize>(yaml); + act.ShouldNotThrow("Because duplicate key checking is not enabled"); + + var stream = Yaml.ReaderFrom("backreference.yaml"); + var parser = new MergingParser(new Parser(stream)); + act = () => sut.Deserialize>>(parser); + act.ShouldNotThrow("Because duplicate key checking is not enabled"); + } + + [Fact] + public void SerializeStateMethodsGetCalledOnce() + { + var yaml = "Test: Hi"; + var deserializer = new DeserializerBuilder().Build(); + var test = deserializer.Deserialize(yaml); + + Assert.Equal(1, test.OnDeserializedCallCount); + Assert.Equal(1, test.OnDeserializingCallCount); + } + + public class TestState + { + public int OnDeserializedCallCount { get; set; } + public int OnDeserializingCallCount { get; set; } + + public string Test { get; set; } = string.Empty; + + [OnDeserialized] + public void Deserialized() => OnDeserializedCallCount++; + + [OnDeserializing] + public void Deserializing() => OnDeserializingCallCount++; + } + + public class Test + { + public string Value { get; set; } + } + + public class SetterOnly + { + private string _value; + public string Value { set => _value = value; } + public string Actual { get => _value; } + } + + public class Person + { + public string Name { get; private set; } + + public DateTime MomentOfBirth { get; private set; } + + public IList Cars { get; private set; } + } + + public class Car : ICar + { + public string Name { get; private set; } + + public int Year { get; private set; } + + public IModelSpec Spec { get; private set; } + } + + public interface ICar + { + string Name { get; } + + int Year { get; } + IModelSpec Spec { get; } + } + + public class ModelSpec : IModelSpec + { + public string EngineType { get; private set; } + + public string DriveType { get; private set; } + } + + public interface IModelSpec + { + string EngineType { get; } + + string DriveType { get; } + } + } +} diff --git a/YamlDotNet.Test/Serialization/SerializationTests.cs b/YamlDotNet.Test/Serialization/SerializationTests.cs index 84af7a64..15ee7192 100644 --- a/YamlDotNet.Test/Serialization/SerializationTests.cs +++ b/YamlDotNet.Test/Serialization/SerializationTests.cs @@ -1,2598 +1,2598 @@ -ο»Ώ// 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); + } + + 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; } + } + } + } +} diff --git a/YamlDotNet/Helpers/OrderedDictionary.cs b/YamlDotNet/Helpers/OrderedDictionary.cs index 1022475b..94e21758 100644 --- a/YamlDotNet/Helpers/OrderedDictionary.cs +++ b/YamlDotNet/Helpers/OrderedDictionary.cs @@ -1,234 +1,234 @@ -ο»Ώ// 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.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Runtime.Serialization; - -namespace YamlDotNet.Helpers -{ - [Serializable] - internal sealed class OrderedDictionary : IOrderedDictionary - where TKey : notnull - { - [NonSerialized] - private Dictionary dictionary; - private readonly List> list; - private readonly IEqualityComparer comparer; - - public TValue this[TKey key] - { - get => dictionary[key]; - set - { - if (dictionary.ContainsKey(key)) - { - var index = list.FindIndex(kvp => comparer.Equals(kvp.Key, key)); - dictionary[key] = value; - list[index] = new KeyValuePair(key, value); - } - else - { - Add(key, value); - } - } - } - - public ICollection Keys => new KeyCollection(this); - - public ICollection Values => new ValueCollection(this); - - public int Count => dictionary.Count; - - public bool IsReadOnly => false; - - public KeyValuePair this[int index] - { - get => list[index]; - set => list[index] = value; - } - - public OrderedDictionary() : this(EqualityComparer.Default) - { - } - - public OrderedDictionary(IEqualityComparer comparer) - { - list = new List>(); - dictionary = new Dictionary(comparer); - this.comparer = comparer; - } - - public void Add(TKey key, TValue value) => Add(new KeyValuePair(key, value)); - - public void Add(KeyValuePair item) - { - dictionary.Add(item.Key, item.Value); - list.Add(item); - } - - public void Clear() - { - dictionary.Clear(); - list.Clear(); - } - - public bool Contains(KeyValuePair item) => dictionary.Contains(item); - - public bool ContainsKey(TKey key) => dictionary.ContainsKey(key); - - public void CopyTo(KeyValuePair[] array, int arrayIndex) => - list.CopyTo(array, arrayIndex); - - public IEnumerator> GetEnumerator() => list.GetEnumerator(); - - public void Insert(int index, TKey key, TValue value) - { - dictionary.Add(key, value); - list.Insert(index, new KeyValuePair(key, value)); - } - - public bool Remove(TKey key) - { - if (dictionary.ContainsKey(key)) - { - var index = list.FindIndex(kvp => comparer.Equals(kvp.Key, key)); - list.RemoveAt(index); - if (!dictionary.Remove(key)) - { - throw new InvalidOperationException(); - } - return true; - } - else - { - return false; - } - } - - public bool Remove(KeyValuePair item) => Remove(item.Key); - - public void RemoveAt(int index) - { - var key = list[index].Key; - dictionary.Remove(key); - list.RemoveAt(index); - } - -#if !NET -#pragma warning disable 8767 // Nullability of reference types in type of parameter ... doesn't match implicitly implemented member -#endif - - public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) => - dictionary.TryGetValue(key, out value); - -#if !NET -#pragma warning restore 8767 -#endif - - IEnumerator IEnumerable.GetEnumerator() => list.GetEnumerator(); - - - [OnDeserialized] - internal void OnDeserializedMethod(StreamingContext context) - { - // Reconstruct the dictionary from the serialized list - dictionary = new Dictionary(); - foreach (var kvp in list) - { - dictionary[kvp.Key] = kvp.Value; - } - } - - private class KeyCollection : ICollection - { - private readonly OrderedDictionary orderedDictionary; - - public int Count => orderedDictionary.list.Count; - - public bool IsReadOnly => true; - - public void Add(TKey item) => throw new NotSupportedException(); - - public void Clear() => throw new NotSupportedException(); - - public bool Contains(TKey item) => orderedDictionary.dictionary.Keys.Contains(item); - - public KeyCollection(OrderedDictionary orderedDictionary) - { - this.orderedDictionary = orderedDictionary; - } - - public void CopyTo(TKey[] array, int arrayIndex) - { - for (var i = 0; i < orderedDictionary.list.Count; i++) - { - array[i] = orderedDictionary.list[i + arrayIndex].Key; - } - } - - public IEnumerator GetEnumerator() => - orderedDictionary.list.Select(kvp => kvp.Key).GetEnumerator(); - - public bool Remove(TKey item) => throw new NotSupportedException(); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - } - - private class ValueCollection : ICollection - { - private readonly OrderedDictionary orderedDictionary; - - public int Count => orderedDictionary.list.Count; - - public bool IsReadOnly => true; - - public void Add(TValue item) => throw new NotSupportedException(); - - public void Clear() => throw new NotSupportedException(); - - public bool Contains(TValue item) => orderedDictionary.dictionary.Values.Contains(item); - - public ValueCollection(OrderedDictionary orderedDictionary) - { - this.orderedDictionary = orderedDictionary; - } - - public void CopyTo(TValue[] array, int arrayIndex) - { - for (var i = 0; i < orderedDictionary.list.Count; i++) - { - array[i] = orderedDictionary.list[i + arrayIndex].Value; - } - } - - public IEnumerator GetEnumerator() => - orderedDictionary.list.Select(kvp => kvp.Value).GetEnumerator(); - - public bool Remove(TValue item) => throw new NotSupportedException(); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - } - } -} +ο»Ώ// 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.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Runtime.Serialization; + +namespace YamlDotNet.Helpers +{ + [Serializable] + internal sealed class OrderedDictionary : IOrderedDictionary + where TKey : notnull + { + [NonSerialized] + private Dictionary dictionary; + private readonly List> list; + private readonly IEqualityComparer comparer; + + public TValue this[TKey key] + { + get => dictionary[key]; + set + { + if (dictionary.ContainsKey(key)) + { + var index = list.FindIndex(kvp => comparer.Equals(kvp.Key, key)); + dictionary[key] = value; + list[index] = new KeyValuePair(key, value); + } + else + { + Add(key, value); + } + } + } + + public ICollection Keys => new KeyCollection(this); + + public ICollection Values => new ValueCollection(this); + + public int Count => dictionary.Count; + + public bool IsReadOnly => false; + + public KeyValuePair this[int index] + { + get => list[index]; + set => list[index] = value; + } + + public OrderedDictionary() : this(EqualityComparer.Default) + { + } + + public OrderedDictionary(IEqualityComparer comparer) + { + list = new List>(); + dictionary = new Dictionary(comparer); + this.comparer = comparer; + } + + public void Add(TKey key, TValue value) => Add(new KeyValuePair(key, value)); + + public void Add(KeyValuePair item) + { + dictionary.Add(item.Key, item.Value); + list.Add(item); + } + + public void Clear() + { + dictionary.Clear(); + list.Clear(); + } + + public bool Contains(KeyValuePair item) => dictionary.Contains(item); + + public bool ContainsKey(TKey key) => dictionary.ContainsKey(key); + + public void CopyTo(KeyValuePair[] array, int arrayIndex) => + list.CopyTo(array, arrayIndex); + + public IEnumerator> GetEnumerator() => list.GetEnumerator(); + + public void Insert(int index, TKey key, TValue value) + { + dictionary.Add(key, value); + list.Insert(index, new KeyValuePair(key, value)); + } + + public bool Remove(TKey key) + { + if (dictionary.ContainsKey(key)) + { + var index = list.FindIndex(kvp => comparer.Equals(kvp.Key, key)); + list.RemoveAt(index); + if (!dictionary.Remove(key)) + { + throw new InvalidOperationException(); + } + return true; + } + else + { + return false; + } + } + + public bool Remove(KeyValuePair item) => Remove(item.Key); + + public void RemoveAt(int index) + { + var key = list[index].Key; + dictionary.Remove(key); + list.RemoveAt(index); + } + +#if !NET +#pragma warning disable 8767 // Nullability of reference types in type of parameter ... doesn't match implicitly implemented member +#endif + + public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) => + dictionary.TryGetValue(key, out value); + +#if !NET +#pragma warning restore 8767 +#endif + + IEnumerator IEnumerable.GetEnumerator() => list.GetEnumerator(); + + + [OnDeserialized] + internal void OnDeserializedMethod(StreamingContext context) + { + // Reconstruct the dictionary from the serialized list + dictionary = new Dictionary(); + foreach (var kvp in list) + { + dictionary[kvp.Key] = kvp.Value; + } + } + + private class KeyCollection : ICollection + { + private readonly OrderedDictionary orderedDictionary; + + public int Count => orderedDictionary.list.Count; + + public bool IsReadOnly => true; + + public void Add(TKey item) => throw new NotSupportedException(); + + public void Clear() => throw new NotSupportedException(); + + public bool Contains(TKey item) => orderedDictionary.dictionary.Keys.Contains(item); + + public KeyCollection(OrderedDictionary orderedDictionary) + { + this.orderedDictionary = orderedDictionary; + } + + public void CopyTo(TKey[] array, int arrayIndex) + { + for (var i = 0; i < orderedDictionary.list.Count; i++) + { + array[i] = orderedDictionary.list[i + arrayIndex].Key; + } + } + + public IEnumerator GetEnumerator() => + orderedDictionary.list.Select(kvp => kvp.Key).GetEnumerator(); + + public bool Remove(TKey item) => throw new NotSupportedException(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } + + private class ValueCollection : ICollection + { + private readonly OrderedDictionary orderedDictionary; + + public int Count => orderedDictionary.list.Count; + + public bool IsReadOnly => true; + + public void Add(TValue item) => throw new NotSupportedException(); + + public void Clear() => throw new NotSupportedException(); + + public bool Contains(TValue item) => orderedDictionary.dictionary.Values.Contains(item); + + public ValueCollection(OrderedDictionary orderedDictionary) + { + this.orderedDictionary = orderedDictionary; + } + + public void CopyTo(TValue[] array, int arrayIndex) + { + for (var i = 0; i < orderedDictionary.list.Count; i++) + { + array[i] = orderedDictionary.list[i + arrayIndex].Value; + } + } + + public IEnumerator GetEnumerator() => + orderedDictionary.list.Select(kvp => kvp.Value).GetEnumerator(); + + public bool Remove(TValue item) => throw new NotSupportedException(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } + } +}