Skip to content

Commit

Permalink
Merge pull request #699 from EdwardCooke/ec-roundtripnulls
Browse files Browse the repository at this point in the history
Allow quoting of specialized strings like null/true/false/numbers etc
  • Loading branch information
EdwardCooke committed Jul 15, 2022
2 parents ad3b6d8 + 2121071 commit 38b6ff5
Show file tree
Hide file tree
Showing 17 changed files with 432 additions and 19 deletions.
2 changes: 1 addition & 1 deletion YamlDotNet.Test/Core/EventsHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ protected void AssertEvent(ParsingEvent expected, ParsingEvent actual, int event

foreach (var property in expected.GetType().GetTypeInfo().GetProperties())
{
if (property.PropertyType == typeof(Mark) || !property.CanRead)
if (property.PropertyType == typeof(Mark) || !property.CanRead || property.Name == "IsKey")
{
continue;
}
Expand Down
2 changes: 1 addition & 1 deletion YamlDotNet.Test/Core/ScannerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,7 @@ private void AssertToken(Token expected, Token actual, int tokenNumber)

foreach (var property in expected.GetType().GetTypeInfo().GetProperties())
{
if (property.PropertyType != typeof(Mark) && property.CanRead)
if (property.PropertyType != typeof(Mark) && property.CanRead && property.Name != "IsKey")
{
var value = property.GetValue(actual, null);
var expectedValue = property.GetValue(expected, null);
Expand Down
79 changes: 79 additions & 0 deletions YamlDotNet.Test/Serialization/DeserializerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

using System;
using System.Collections.Generic;
using System.Linq;
using FluentAssertions;
using Xunit;
using YamlDotNet.Serialization;
Expand Down Expand Up @@ -118,6 +119,84 @@ public void SetterOnlySetsWithoutException()
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<object, object>)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<Test>(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<Dictionary<object, object>>(result);
var dictionary = (Dictionary<object, object>)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<Dictionary<object, object>>(result);

var dictionary = (Dictionary<object, object>)result;
var item = dictionary.ElementAt(0);
Assert.Equal("scalar", item.Key);
Assert.Equal("foo", item.Value);

item = dictionary.ElementAt(1);
Assert.IsType<Dictionary<object, object>>(item.Key);
Assert.Equal("bar", item.Value);
dictionary = (Dictionary<object, object>)item.Key;
item = dictionary.ElementAt(0);
Assert.Equal("a", item.Key);
Assert.Equal("mapping", item.Value);

dictionary = (Dictionary<object, object>)result;
item = dictionary.ElementAt(2);
Assert.IsType<List<object>>(item.Key);
Assert.Equal(new List<object> { "a", "sequence", (byte)1 }, (List<object>)item.Key);
Assert.Equal("baz", item.Value);
}

public class Test
{
public string Value { get; set; }
}

public class SetterOnly
{
private string _value;
Expand Down
95 changes: 95 additions & 0 deletions YamlDotNet.Test/Serialization/SerializationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -829,6 +829,18 @@ public void DeserializationOfDefaultsWorkInJson()
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<Example>(UsingReaderFor(writer));

result.MyString.Should().BeNull();
}

[Theory]
[InlineData(typeof(SByteEnum))]
[InlineData(typeof(ByteEnum))]
Expand Down Expand Up @@ -1358,6 +1370,16 @@ public void SpecialFloatsAreHandledCorrectly(FloatTestCase testCase)
Assert.Equal(testCase.Value, deserializedValue);
}

[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 class FloatTestCase
{
private readonly string description;
Expand Down Expand Up @@ -1395,6 +1417,7 @@ public static IEnumerable<object[]> SpecialFloats
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)),
Expand Down Expand Up @@ -2166,6 +2189,78 @@ public void RoundtripWindowsNewlines()
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);
}

[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<ReservedWordsTestClass<string>>(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<ReservedWordsTestClass<bool>>(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}");
}

public class ReservedWordsTestClass<TNullType>
{
public string True { get; set; }
public string False { get; set; }
public TNullType Null { get; set; }
}

[TypeConverter(typeof(DoublyConvertedTypeConverter))]
public class DoublyConverted
{
Expand Down
6 changes: 5 additions & 1 deletion YamlDotNet.Test/Spec/ParserSpecTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,12 @@ private sealed class ParserSpecTestsData : SpecTestsData
};

[Theory, ClassData(typeof(ParserSpecTestsData))]
public void ConformsWithYamlSpec(string name, string description, string inputFile, string expectedEventFile, bool error)
#pragma warning disable xUnit1026 // Theory methods should use all of their parameters
public void ConformsWithYamlSpec(string name, string description, string inputFile, string expectedEventFile, bool error, bool quoting)
#pragma warning restore xUnit1026 // Theory methods should use all of their parameters
{
// we don't care about quoting for this test.

var expectedResult = File.ReadAllText(expectedEventFile)
.Replace("+MAP {}", "+MAP")
.Replace("+SEQ []", "+SEQ");
Expand Down
16 changes: 13 additions & 3 deletions YamlDotNet.Test/Spec/SerializerSpecTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,22 @@ internal sealed class SerializerSpecTestsData : SpecTestsData
// no false-positives known as of https://github.com/yaml/yaml-test-suite/releases/tag/data-2020-02-11
};

private readonly IDeserializer deserializer = new DeserializerBuilder().Build();
private readonly ISerializer serializer = new SerializerBuilder().Build();

[Theory, ClassData(typeof(SerializerSpecTestsData))]
public void ConformsWithYamlSpec(string name, string description, string inputFile, string outputFile, bool error)
public void ConformsWithYamlSpec(string name, string description, string inputFile, string outputFile, bool error, bool quoting)
{
var deserializerBuilder = new DeserializerBuilder();
var serializerBuilder = new SerializerBuilder();

if (quoting)
{
deserializerBuilder.WithAttemptingUnquotedStringTypeDeserialization();
serializerBuilder.WithQuotingNecessaryStrings();
}

var serializer = serializerBuilder.Build();
var deserializer = deserializerBuilder.Build();

var expectedResult = File.ReadAllText(outputFile);
using var writer = new StringWriter();
try
Expand Down
12 changes: 11 additions & 1 deletion YamlDotNet.Test/Spec/SpecTestData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,17 @@ public IEnumerator<object[]> GetEnumerator()
File.ReadAllText(descriptionFile).TrimEnd(),
inputFile,
(this is SerializerSpecTests.SerializerSpecTestsData) ? outputFile : expectedEventFile,
hasErrorFile
hasErrorFile,
true //quoting enabled
};
yield return new object[]
{
testName,
File.ReadAllText(descriptionFile).TrimEnd(),
inputFile,
(this is SerializerSpecTests.SerializerSpecTestsData) ? outputFile : expectedEventFile,
hasErrorFile,
false //quoting not enabled
};
}
}
Expand Down
9 changes: 8 additions & 1 deletion YamlDotNet/Core/Events/Scalar.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ public sealed class Scalar : NodeEvent
/// <value></value>
public override bool IsCanonical => !IsPlainImplicit && !IsQuotedImplicit;

/// <summary>
/// Gets whether this scalar event is a key
/// </summary>
public bool IsKey { get; }

/// <summary>
/// Initializes a new instance of the <see cref="Scalar"/> class.
/// </summary>
Expand All @@ -70,13 +75,15 @@ public sealed class Scalar : NodeEvent
/// <param name="isQuotedImplicit">.</param>
/// <param name="start">The start position of the event.</param>
/// <param name="end">The end position of the event.</param>
public Scalar(AnchorName anchor, TagName tag, string value, ScalarStyle style, bool isPlainImplicit, bool isQuotedImplicit, Mark start, Mark end)
/// <param name="isKey">Whether or not this scalar event is for a key</param>
public Scalar(AnchorName anchor, TagName tag, string value, ScalarStyle style, bool isPlainImplicit, bool isQuotedImplicit, Mark start, Mark end, bool isKey = false)
: base(anchor, tag, start, end)
{
this.Value = value;
this.Style = style;
this.IsPlainImplicit = isPlainImplicit;
this.IsQuotedImplicit = isQuotedImplicit;
this.IsKey = isKey;
}

/// <summary>
Expand Down
2 changes: 1 addition & 1 deletion YamlDotNet/Core/Parser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -559,7 +559,7 @@ private ParsingEvent ParseNode(bool isBlock, bool isIndentlessSequence)
state = states.Pop();
Skip();

ParsingEvent evt = new Events.Scalar(anchorName, tagName, scalar.Value, scalar.Style, isPlainImplicit, isQuotedImplicit, start, scalar.End);
ParsingEvent evt = new Events.Scalar(anchorName, tagName, scalar.Value, scalar.Style, isPlainImplicit, isQuotedImplicit, start, scalar.End, scalar.IsKey);

// Read next token to ensure the error case spec test 'CXX2':
// "Mapping with anchor on document start line".
Expand Down
Loading

0 comments on commit 38b6ff5

Please sign in to comment.