Skip to content

Commit

Permalink
Fix nullable enum parsing bug
Browse files Browse the repository at this point in the history
  • Loading branch information
atruskie committed Feb 12, 2021
1 parent 522a30e commit 2a0b304
Show file tree
Hide file tree
Showing 2 changed files with 108 additions and 1 deletion.
66 changes: 66 additions & 0 deletions src/Acoustics.Shared/Yaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ namespace Acoustics.Shared
using System.Linq;
using JetBrains.Annotations;
using YamlDotNet.Core;
using YamlDotNet.Core.Events;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NodeDeserializers;

public static class Yaml
{
Expand All @@ -33,6 +35,7 @@ static Yaml()
internal static IDeserializer Deserializer => new DeserializerBuilder()
.IgnoreUnmatchedProperties()
.WithTagMappings(TagMappings)
.WithTypeConverter(new YamlNullableEnumTypeConverter())
.Build();

public static T Deserialize<T>(FileInfo file)
Expand Down Expand Up @@ -98,5 +101,68 @@ private static T WithTagMappings<T>(
return mappings
.Aggregate(builder, (b, kvp) => b.WithTagMapping(kvp.Key, kvp.Value));
}

private class YamlNullableEnumTypeConverter : IYamlTypeConverter
{
// deals with nullable-enum parsing bug
// https://github.com/aaubry/YamlDotNet/issues/544#issuecomment-761711947
public bool Accepts(Type type)
{
return Nullable.GetUnderlyingType(type)?.IsEnum ?? false;
}

public object ReadYaml(IParser parser, Type type)
{
type = Nullable.GetUnderlyingType(type) ?? throw new ArgumentException("Expected nullable enum type for ReadYaml");

if (parser.Accept<NodeEvent>(out var @event))
{
if (NodeIsNull(@event))
{
parser.SkipThisAndNestedEvents();
return null;
}
}

var scalar = parser.Consume<Scalar>();
try
{
return Enum.Parse(type, scalar.Value, ignoreCase: true);
}
catch (Exception ex)
{
throw new Exception($"Invalid value: \"{scalar.Value}\" for {type.Name}", ex);
}
}

public void WriteYaml(IEmitter emitter, object value, Type type)
{
type = Nullable.GetUnderlyingType(type) ?? throw new ArgumentException("Expected nullable enum type for WriteYaml");

if (value != null)
{
var toWrite = Enum.GetName(type, value) ?? throw new InvalidOperationException($"Invalid value {value} for enum: {type}");
emitter.Emit(new Scalar(null, null, toWrite, ScalarStyle.Any, true, false));
}
}

private static bool NodeIsNull(NodeEvent nodeEvent)
{
// http://yaml.org/type/null.html

if (nodeEvent.Tag == "tag:yaml.org,2002:null")
{
return true;
}

if (nodeEvent is Scalar scalar && scalar.Style == ScalarStyle.Plain)
{
var value = scalar.Value;
return value is "" or "~" or "null" or "Null" or "NULL";
}

return false;
}
}
}
}
43 changes: 42 additions & 1 deletion tests/Acoustics.Test/Shared/YamlTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@
// --------------------------------------------------------------------------------------------------------------------
namespace Acoustics.Test.Shared
{
using System;
using System.IO;

using Acoustics.Shared;

using Microsoft.VisualStudio.TestTools.UnitTesting;
using YamlDotNet.Core;

[TestClass]
public class YamlTests
Expand Down Expand Up @@ -94,7 +96,7 @@ public class YamlTests
# Frame step. units=samples
# NOTE: The value for FrameStep is used only when calculating a standard spectrogram within the ZOOMING spectrogram function.
# FrameStep is NOT used when calculating Summary and Spectral indices.
# However the FrameStep entry must NOT be deleted from this file. Keep the value for when it is required.
# However the FrameStep entry must NOT be deleted from this file. Keep the value for when it is required.
# The value 441 should NOT be changed because it has been calculated specifically for current ZOOMING spectrogram set-up.
# TODO: this option should be refactored out into the spectrogram generation analyzer - currently confusing implementation
FrameStep: 441
Expand Down Expand Up @@ -183,6 +185,36 @@ public void SerializerCanDecodePrivateSetters()
Assert.AreEqual(123456, testObject.PrivateSetter.Value);
}

[TestMethod]
public void CanDeserializeNullableEnums()
{
// related to https://github.com/aaubry/YamlDotNet/issues/544
var testCase = @"
A: WAVEFORM
B: ~
C: Spectrogram
";

// bug in yamldotnet should fail unless our patch is added
// if not fail, then patch no longer needed
var exception = Assert.ThrowsException<YamlException>(
() =>
{
using var stream = new StringReader(testCase);
var @default = new YamlDotNet.Serialization.Deserializer();
@default.Deserialize<YamlEnumTestClass>(stream);
});
Assert.IsInstanceOfType(exception.InnerException, typeof(FormatException));
StringAssert.Contains( "Input string was not in a correct format.", exception.InnerException.Message);

using var stream = new StringReader(testCase);
var actual = Yaml.Deserialize<YamlEnumTestClass>(stream);

Assert.AreEqual(SpectrogramType.WaveForm, actual.A);
Assert.AreEqual(null, actual.B);
Assert.AreEqual(SpectrogramType.Spectrogram, actual.C);
}

[TestCleanup]
public void TestCleanup()
{
Expand All @@ -196,6 +228,15 @@ public void TestInitialize()
File.WriteAllText(this.testDocument.FullName, WrapperDocument);
}

public class YamlEnumTestClass
{
public SpectrogramType A { get; set; }

public SpectrogramType? B { get; set; }

public SpectrogramType? C { get; set; }
}

public class YamlTestDataClass
{
public string SomeProperty { get; set; }
Expand Down

0 comments on commit 2a0b304

Please sign in to comment.