Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow quoting of specialized strings like null/true/false/numbers etc #699

Merged
merged 4 commits into from
Jul 15, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
34 changes: 34 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,39 @@ 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);
}

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
18 changes: 17 additions & 1 deletion YamlDotNet/Core/Scanner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ public class Scanner : IScanner
private bool tokenAvailable;
private Token? previous;
private Anchor? previousAnchor;
private Scalar? lastScalar = null;

private bool IsDocumentStart() =>
!analyzer.EndOfInput &&
Expand Down Expand Up @@ -313,13 +314,15 @@ private void FetchNextToken()

if (analyzer.Buffer.EndOfInput)
{
lastScalar = null;
FetchStreamEnd();
}

// Is it a directive?

if (cursor.LineOffset == 0 && analyzer.Check('%'))
{
lastScalar = null;
FetchDirective();
return;
}
Expand All @@ -328,6 +331,7 @@ private void FetchNextToken()

if (IsDocumentStart())
{
lastScalar = null;
FetchDocumentIndicator(true);
return;
}
Expand All @@ -336,6 +340,7 @@ private void FetchNextToken()

if (IsDocumentEnd())
{
lastScalar = null;
FetchDocumentIndicator(false);
return;
}
Expand All @@ -344,6 +349,7 @@ private void FetchNextToken()

if (analyzer.Check('['))
{
lastScalar = null;
FetchFlowCollectionStart(true);
return;
}
Expand All @@ -352,6 +358,7 @@ private void FetchNextToken()

if (analyzer.Check('{'))
{
lastScalar = null;
FetchFlowCollectionStart(false);
return;
}
Expand All @@ -360,6 +367,7 @@ private void FetchNextToken()

if (analyzer.Check(']'))
{
lastScalar = null;
FetchFlowCollectionEnd(true);
return;
}
Expand All @@ -368,6 +376,7 @@ private void FetchNextToken()

if (analyzer.Check('}'))
{
lastScalar = null;
FetchFlowCollectionEnd(false);
return;
}
Expand All @@ -376,6 +385,7 @@ private void FetchNextToken()

if (analyzer.Check(','))
{
lastScalar = null;
FetchFlowEntry();
return;
}
Expand Down Expand Up @@ -416,6 +426,12 @@ private void FetchNextToken()
{
if (analyzer.IsWhiteBreakOrZero(1) || analyzer.Check(',', 1) || flowScalarFetched || flowCollectionFetched || startFlowCollectionFetched)
{
if (lastScalar != null)
{
lastScalar.IsKey = true;
lastScalar = null;
}

FetchValue();
return;
}
Expand Down Expand Up @@ -2042,7 +2058,7 @@ private void FetchPlainScalar()
// Create the SCALAR token and append it to the queue.
var isMultiline = false;
var scalar = ScanPlainScalar(ref isMultiline);

lastScalar = scalar;
if (isMultiline && analyzer.Check(':') && flowLevel == 0 && indent < cursor.LineOffset)
{
tokens.Enqueue(new Error("While scanning a multiline plain scalar, found invalid mapping.", cursor.Mark(), cursor.Mark()));
Expand Down
5 changes: 5 additions & 0 deletions YamlDotNet/Core/Tokens/Scalar.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ namespace YamlDotNet.Core.Tokens
/// </summary>
public sealed class Scalar : Token
{
/// <summary>
/// Gets or sets whether this scalar is a key
/// </summary>
public bool IsKey { get; set; }

/// <summary>
/// Gets the value.
/// </summary>
Expand Down