Skip to content

Commit

Permalink
Merge pull request #834 from EdwardCooke/ec-810-nullyamlstreamscalars
Browse files Browse the repository at this point in the history
Support null values in YamlScalarNode for YamlStream

+semver:fix
  • Loading branch information
EdwardCooke authored Aug 22, 2023
2 parents bfba244 + 02836c5 commit 8a7cb19
Show file tree
Hide file tree
Showing 8 changed files with 159 additions and 14 deletions.
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

0 comments on commit 8a7cb19

Please sign in to comment.