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

Support null values in YamlScalarNode for YamlStream #834

Merged
merged 2 commits into from
Aug 22, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
90 changes: 90 additions & 0 deletions YamlDotNet.Test/RepresentationModel/YamlStreamTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@
using FluentAssertions;
using Xunit;
using YamlDotNet.Core;
using YamlDotNet.Core.Events;
using YamlDotNet.RepresentationModel;
using YamlDotNet.Serialization.Schemas;

namespace YamlDotNet.Test.RepresentationModel
{
Expand Down Expand Up @@ -103,6 +105,63 @@ public void ForwardAliasReferenceWorks()
Assert.Same(sequence.Children[0], sequence.Children[2]);
}

[Theory]
[InlineData("B: !!null ", "", ScalarStyle.Plain, false)]
[InlineData("B: ", "", ScalarStyle.Plain, true)]
[InlineData("B: abc", "abc", ScalarStyle.Plain, true)]
[InlineData("B: ~", "~", ScalarStyle.Plain, true)]
[InlineData("B: Null", "Null", ScalarStyle.Plain, true)]
[InlineData("B: ''", "", ScalarStyle.SingleQuoted, true)]
[InlineData("B: 'Null'", "Null", ScalarStyle.SingleQuoted, true)]
public void ImplicitNullRoundtrips(string yaml, string value, ScalarStyle style, bool implicitPlain)
{
//load
var stream = new YamlStream();
stream.Load(new StringReader(yaml));
var mapping = (YamlMappingNode)stream.Documents[0].RootNode;
var map = mapping.Children[0];

var yamlValue = (YamlScalarNode)map.Value;
Assert.Equal(value, yamlValue.Value);

var emitter = new RoundTripNullTestEmitter(implicitPlain, style);
yamlValue.Emit(emitter, null);

var stringWriter = new StringWriter();
new YamlStream(stream.Documents).Save(stringWriter);
Assert.Equal($@"{yaml}
...".NormalizeNewLines(),
stringWriter.ToString().NormalizeNewLines().TrimNewLines());
}

[Fact]
public void EmptyScalarsAreEmptySingleQuoted()
{
var stringWriter = new StringWriter();
var rootNode = new YamlMappingNode();
rootNode.Children.Add(new YamlScalarNode("test"), new YamlScalarNode(""));
var document = new YamlDocument(rootNode);
var yamlStream = new YamlStream(document);
yamlStream.Save(stringWriter);
var actual = stringWriter.ToString().NormalizeNewLines();
var expected = "test: ''\r\n...\r\n".NormalizeNewLines();
Assert.Equal(expected, actual);
}

[Fact]
public void NullScalarsAreEmptyPlain()
{
var stringWriter = new StringWriter();
var rootNode = new YamlMappingNode();
rootNode.Children.Add(new YamlScalarNode("test"), new YamlScalarNode(null));
var document = new YamlDocument(rootNode);
var yamlStream = new YamlStream(document);
yamlStream.Save(stringWriter);
var actual = stringWriter.ToString().NormalizeNewLines();
var expected = "test: \r\n...\r\n".NormalizeNewLines();
Assert.Equal(expected, actual);
}

[Fact]
public void RoundtripExample1()
{
Expand Down Expand Up @@ -313,5 +372,36 @@ private enum YamlNodeEventType
MappingEnd,
Scalar,
}

private class RoundTripNullTestEmitter : IEmitter
{
private readonly bool enforceImplicit;
private readonly ScalarStyle style;

public RoundTripNullTestEmitter(bool enforceImplicit, ScalarStyle style)
{
this.enforceImplicit = enforceImplicit;
this.style = style;
}

public void Emit(ParsingEvent @event)
{
Assert.Equal(EventType.Scalar, @event.Type);
Assert.IsType<Scalar>(@event);
var scalar = (Scalar)@event;

Assert.Equal(style, scalar.Style);
if (enforceImplicit)
{
Assert.True(scalar.IsPlainImplicit);
}
else
{
Assert.False(scalar.IsPlainImplicit);
}

Assert.False(scalar.IsQuotedImplicit);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public Lazy(Func<T> valueFactory)
this.valueFactory = valueFactory;
this.isThreadSafe = false;
valueState = ValueState.NotCreated;
value = default!;
}

public Lazy(Func<T> valueFactory, bool isThreadSafe)
Expand Down
61 changes: 58 additions & 3 deletions YamlDotNet/RepresentationModel/YamlScalarNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
using YamlDotNet.Core;
using YamlDotNet.Core.Events;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.Schemas;
using static YamlDotNet.Core.HashCode;

namespace YamlDotNet.RepresentationModel
Expand All @@ -35,11 +36,30 @@ namespace YamlDotNet.RepresentationModel
[DebuggerDisplay("{Value}")]
public sealed class YamlScalarNode : YamlNode, IYamlConvertible
{
private bool _forceImplicitPlain = false;
private string? _value;

/// <summary>
/// Gets or sets the value of the node.
/// </summary>
/// <value>The value.</value>
public string? Value { get; set; }
public string? Value
{
get => _value;
set
{
if (value == null)
{
_forceImplicitPlain = true;
}
else
{
_forceImplicitPlain = false;
}

_value = value;
}
}

/// <summary>
/// Gets or sets the style of the node.
Expand All @@ -58,8 +78,25 @@ internal YamlScalarNode(IParser parser, DocumentLoadingState state)
private void Load(IParser parser, DocumentLoadingState state)
{
var scalar = parser.Consume<Scalar>();

Load(scalar, state);
Value = scalar.Value;

if (scalar.Style == ScalarStyle.Plain &&
Tag.IsEmpty &&
(scalar.Value == string.Empty ||
scalar.Value.Equals("null", StringComparison.InvariantCulture) ||
scalar.Value.Equals("Null", StringComparison.InvariantCulture) ||
scalar.Value.Equals("NULL", StringComparison.InvariantCulture) ||
scalar.Value == "~"))
{
// we have an implicit null value without a tag stating it, fake it out
_forceImplicitPlain = true;

// for backwards compatability we won't be setting the Value property
// to null
}

_value = scalar.Value;
Style = scalar.Style;
}

Expand Down Expand Up @@ -95,7 +132,24 @@ internal override void ResolveAliases(DocumentLoadingState state)
/// <param name="state">The state.</param>
internal override void Emit(IEmitter emitter, EmitterState state)
{
emitter.Emit(new Scalar(Anchor, Tag, Value ?? string.Empty, Style, Tag.IsEmpty, false));
var tag = Tag;
var implicitPlain = tag.IsEmpty;

if (_forceImplicitPlain &&
Style == ScalarStyle.Plain &&
(Value == null || Value == ""))
{
tag = JsonSchema.Tags.Null;
implicitPlain = true;
}
else if (tag.IsEmpty && Value == null &&
(Style == ScalarStyle.Plain || Style == ScalarStyle.Any))
{
tag = JsonSchema.Tags.Null;
implicitPlain = true;
}

emitter.Emit(new Scalar(Anchor, tag, Value ?? string.Empty, Style, implicitPlain, false));
}

/// <summary>
Expand Down Expand Up @@ -176,5 +230,6 @@ void IYamlConvertible.Write(IEmitter emitter, ObjectSerializer nestedObjectSeria
{
Emit(emitter, new EmitterState());
}

}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// This file is part of YamlDotNet - A .NET library for YAML.
// This file is part of YamlDotNet - A .NET library for YAML.
// Copyright (c) Antoine Aubry and contributors
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
Expand Down Expand Up @@ -74,8 +74,8 @@ public bool TryDiscriminate(IParser parser, out Type? suggestedType)
{
if (parser.TryFindMappingEntry(
scalar => this.typeMapping.ContainsKey(scalar.Value),
out Scalar key,
out ParsingEvent _))
out var key,
out var _))
{
suggestedType = this.typeMapping[key.Value];
return true;
Expand All @@ -85,4 +85,4 @@ public bool TryDiscriminate(IParser parser, out Type? suggestedType)
return false;
}
}
}
}
6 changes: 3 additions & 3 deletions YamlDotNet/Serialization/Deserializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,12 @@ public T Deserialize<T>(IParser parser)
{
return (T)Deserialize(parser, typeof(T))!; // We really want an exception if we are trying to deserialize null into a non-nullable type
}

public object? Deserialize(string input)
{
return Deserialize(input, typeof(object));
}

public object? Deserialize(TextReader input)
{
return Deserialize(input, typeof(object));
Expand All @@ -96,7 +96,7 @@ public T Deserialize<T>(IParser parser)
{
return Deserialize(parser, typeof(object));
}

public object? Deserialize(string input, Type type)
{
using var reader = new StringReader(input);
Expand Down
3 changes: 1 addition & 2 deletions YamlDotNet/Serialization/IDeserializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,9 @@ public interface IDeserializer
object? Deserialize(string input);
object? Deserialize(TextReader input);
object? Deserialize(IParser parser);

object? Deserialize(string input, Type type);
object? Deserialize(TextReader input, Type type);

/// <summary>
/// Deserializes an object of the specified type.
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion YamlDotNet/Serialization/ISerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public interface ISerializer
/// <param name="graph">The object to serialize.</param>
/// <param name="type">The static type of the object to serialize.</param>
string Serialize(object? graph, Type type);

/// <summary>
/// Serializes the specified object.
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion YamlDotNet/Serialization/Serializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ public string Serialize(object? graph, Type type)
Serialize(buffer, graph, type);
return buffer.ToString();
}

/// <summary>
/// Serializes the specified object.
/// </summary>
Expand Down