Skip to content

Commit

Permalink
Drop Verwendungszweck MEHRMINDERMBENGENABRECHNUNG to avoid further …
Browse files Browse the repository at this point in the history
…undefined behaviour; Add Lenient Converters for Legacy Data/Consistency (#518)



---------

Co-authored-by: Konstantin <[email protected]>
  • Loading branch information
hf-kklein and Konstantin authored Sep 5, 2024
1 parent fb1fc4e commit 9e4258f
Show file tree
Hide file tree
Showing 7 changed files with 294 additions and 12 deletions.
7 changes: 0 additions & 7 deletions BO4E/ENUM/Verwendungszweck.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,4 @@ public enum Verwendungszweck
/// <remarks>ZB5</remarks>
[EnumMember(Value = "ERMITTLUNG_AUSGEGLICHENHEIT_BILANZKREIS")]
ERMITTLUNG_AUSGEGLICHENHEIT_BILANZKREIS,

/// <summary>
/// <inheritdoc cref="MEHRMINDERMENGENABRECHNUNG"/>
/// </summary>
[Obsolete("This is only to keep the library backwards compatible")]
[EnumMember(Value = "MEHRMINDERMBENGENABRECHNUNG")]
MEHRMINDERMBENGENABRECHNUNG = MEHRMINDERMENGENABRECHNUNG,
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public static JsonSerializerOptions GetJsonSerializerOptions(this LenientParsing
settings.Converters.Add(new AutoNumberToStringConverter());
settings.Converters.Add(new VertragsConverter());
settings.Converters.Add(new EnergiemengeConverter());
settings.Converters.Add(new SystemTextVerwendungszweckStringEnumConverter());
settings.Converters.Add(new SystemTextGasqualitaetStringEnumConverter());
settings.Converters.Add(new LenientSystemTextJsonStringToBoolConverter());
settings.Converters.Add(new StringNullableEnumConverter());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ public static JsonSerializerSettings GetJsonSerializerSettings(this LenientParsi
if (lenient.HasFlag(LenientParsing.MOST_LENIENT))
{
converters.Insert(index: 0, item: new NewtonsoftGasqualitaetStringEnumConverter());
// needs to be placed BEFORE the regular StringEnumConverter, see its documentation
converters.Insert(index: 0, item: new NewtonsoftVerwendungszweckStringEnumConverter());
// need to be placed BEFORE the regular StringEnumConverter, see their documentation
}
var settings = new JsonSerializerSettings
{
Expand Down
121 changes: 121 additions & 0 deletions BO4E/meta/LenientConverters/VerwendungszweckStringEnumConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
using System;
using BO4E.ENUM;

namespace BO4E.meta.LenientConverters;

/// <summary>
/// Converts 'MEHRMINDERMBENGENABRECHNUNG' (with typo!) to Enum Member <see cref="Verwendungszweck.MEHRMINDERMENGENABRECHNUNG"/>.
/// </summary>
public class SystemTextVerwendungszweckStringEnumConverter : System.Text.Json.Serialization.JsonConverter<Verwendungszweck?>
{
/// <inheritdoc />
public override Verwendungszweck? Read(ref System.Text.Json.Utf8JsonReader reader, Type typeToConvert,
System.Text.Json.JsonSerializerOptions options)
{
if (reader.TokenType == System.Text.Json.JsonTokenType.Null)
{
return null;
}

if (reader.TokenType == System.Text.Json.JsonTokenType.Number)
{
var integerValue = reader.GetInt64();
return (Verwendungszweck)Enum.ToObject(typeof(Verwendungszweck), integerValue);
}

if (reader.TokenType == System.Text.Json.JsonTokenType.String)
{
string enumString = reader.GetString();

return enumString switch
{
"MEHRMINDERMBENGENABRECHNUNG" => Verwendungszweck.MEHRMINDERMENGENABRECHNUNG,
_ => Enum.TryParse<Verwendungszweck>(enumString, true, out var result)
? result
: throw new System.Text.Json.JsonException($"Invalid value for {typeToConvert.Name}: {enumString}")
};
}

throw new System.Text.Json.JsonException($"Unexpected token parsing {typeToConvert.Name}. Expected String or Number, got {reader.TokenType}.");
}

/// <inheritdoc />
public override void Write(System.Text.Json.Utf8JsonWriter writer, Verwendungszweck? value,
System.Text.Json.JsonSerializerOptions options)
{
if (value is null)
{
writer.WriteNullValue();
}
else
{
writer.WriteStringValue(value.ToString());
}
}
}

/// <summary>
/// converts 'MEHRMINDERMBENGENABRECHNUNG' (with typo!) to Enum Member <see cref="Verwendungszweck.MEHRMINDERMENGENABRECHNUNG"/>
/// </summary>
/// <remarks>
/// It is intended, that you use this converter TOGETHER with the <see cref="Newtonsoft.Json.Converters.StringEnumConverter"/>.
/// In this case you need to add this converter BEFORE the <see cref="Newtonsoft.Json.Converters.StringEnumConverter"/> in the list of converters.
/// <code>
/// var settings = new Newtonsoft.Json.JsonSerializerSettings()
/// {
/// Converters = { new NewtonsoftVerwendungszweckStringEnumConverter(), new StringEnumConverter() }
/// };
/// </code>
/// </remarks>
/// <remarks><seealso cref="SystemTextVerwendungszweckStringEnumConverter"/></remarks>
public class NewtonsoftVerwendungszweckStringEnumConverter : Newtonsoft.Json.JsonConverter<Verwendungszweck?>
{
/// <inheritdoc />>
public override Verwendungszweck? ReadJson(Newtonsoft.Json.JsonReader reader, Type objectType,
Verwendungszweck? existingValue, bool hasExistingValue, Newtonsoft.Json.JsonSerializer serializer)
{
if (reader.TokenType == Newtonsoft.Json.JsonToken.Null)
{
return null;
}

if (reader.TokenType == Newtonsoft.Json.JsonToken.Integer)
{
var integerValue = Convert.ToInt64(reader.Value);
return (Verwendungszweck)Enum.ToObject(typeof(Verwendungszweck), integerValue);
}

if (reader.TokenType == Newtonsoft.Json.JsonToken.String)
{
string enumString = reader.Value.ToString();

return enumString switch
{
"MEHRMINDERMBENGENABRECHNUNG" => Verwendungszweck.MEHRMINDERMENGENABRECHNUNG,
_ => Enum.TryParse(enumString, out Verwendungszweck result)
? result
: throw new Newtonsoft.Json.JsonSerializationException(
$"Invalid value for {objectType}: {enumString}")
};
}

throw new Newtonsoft.Json.JsonSerializationException("Expected string value.");
}

/// <inheritdoc />
public override void WriteJson(Newtonsoft.Json.JsonWriter writer, Verwendungszweck? value,
Newtonsoft.Json.JsonSerializer serializer)
{
if (value is null)
{
writer.WriteNull();
}
else
{
writer.WriteValue(value.ToString());
}
}

/// <inheritdoc />
public override bool CanWrite => true;
}
8 changes: 4 additions & 4 deletions BO4ETestProject/TestEnumMembers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@ internal class MyClass
public void Test_Mehrmindermengenabrechnung_System_Text()
{
var options = LenientParsing.MOST_LENIENT.GetJsonSerializerOptions();
var myLegacyInstance = new MyClass() { Verwendungszweck = Verwendungszweck.MEHRMINDERMBENGENABRECHNUNG };
var myLegacyInstance = new MyClass() { Verwendungszweck = Verwendungszweck.MEHRMINDERMENGENABRECHNUNG };
var myLegacyJson = System.Text.Json.JsonSerializer.Serialize(myLegacyInstance, options);
myLegacyJson.Should().Contain("MEHRMINDERMBENGENABRECHNUNG").And.Contain("B"); // note the "B"
myLegacyJson.Should().Contain("MEHRMINDERMENGENABRECHNUNG");
var myNewInstance = System.Text.Json.JsonSerializer.Deserialize<MyClass>(myLegacyJson, options);
myNewInstance.Verwendungszweck.Should().Be(Verwendungszweck.MEHRMINDERMENGENABRECHNUNG);
}
Expand All @@ -62,9 +62,9 @@ public void Test_Mehrmindermengenabrechnung_Newtonsoft()
{
var options = LenientParsing.MOST_LENIENT.GetJsonSerializerSettings();
options.Converters.Add(new StringEnumConverter());
var myLegacyInstance = new MyClass() { Verwendungszweck = Verwendungszweck.MEHRMINDERMBENGENABRECHNUNG };
var myLegacyInstance = new MyClass() { Verwendungszweck = Verwendungszweck.MEHRMINDERMENGENABRECHNUNG };
var myLegacyJson = Newtonsoft.Json.JsonConvert.SerializeObject(myLegacyInstance, options);
myLegacyJson.Should().Contain("MEHRMINDERMBENGENABRECHNUNG").And.Contain("B"); // note the "B"
myLegacyJson.Should().Contain("MEHRMINDERMENGENABRECHNUNG");
var myNewInstance = Newtonsoft.Json.JsonConvert.DeserializeObject<MyClass>(myLegacyJson, options);
myNewInstance.Verwendungszweck.Should().Be(Verwendungszweck.MEHRMINDERMENGENABRECHNUNG);
}
Expand Down
29 changes: 29 additions & 0 deletions BO4ETestProject/TestEnums.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System;
using System.Linq;
using System.Reflection;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace TestBO4E;
[TestClass]
public class TestEnums
{
[TestMethod]
public void No_Two_Enum_Members_Share_The_Same_Integer_Value()
{
var arbitraryEnumType = typeof(BO4E.ENUM.Abweichungsgrund);
var enumAssembly = Assembly.GetAssembly(arbitraryEnumType);
enumAssembly.Should().NotBeNull();
var enumTypesWithDuplicateValues = enumAssembly!.GetTypes()
.Where(t => t.IsEnum)
.Where(t =>
{
var values = Enum.GetValues(t).Cast<int>();
var valueGroups = values.GroupBy(v => v).Where(g => g.Count() > 1).ToList();
return valueGroups.Any();
})
.ToList();
enumTypesWithDuplicateValues.Should().BeEmpty("this may cause undefined behaviour");
// https://github.com/dotnet/runtime/issues/107296#issuecomment-2327881647
}
}
137 changes: 137 additions & 0 deletions BO4ETestProject/TestStringEnumConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -253,5 +253,142 @@ public void Test_Newtonsoft_Gasqualitaet_Legacy_Converter_With_Non_Nullable_Gasq
var reSerializedJsonString = Newtonsoft.Json.JsonConvert.SerializeObject(actual, settings);
reSerializedJsonString.Should().Contain(expectedGasqualitaet.ToString());
}

public class ClassWithNullableVerwendungszweck
{
public Verwendungszweck? Foo { get; set; }
}

public class ClassWithNonNullableVerwendungszweck
{
public Verwendungszweck Foo { get; set; }
}

[TestMethod]
[DataRow("MEHRMINDERMENGENABRECHNUNG", Verwendungszweck.MEHRMINDERMENGENABRECHNUNG)]
[DataRow("MEHRMINDERMBENGENABRECHNUNG", Verwendungszweck.MEHRMINDERMENGENABRECHNUNG)]
[DataRow(null, null)]
public void Test_System_Text_Verwendungszweck_Legacy_Converter_With_Nullable_Verwendungszweck(string? jsonValue, Verwendungszweck? expectedVerwendungszweck)
{
string jsonString;
if (jsonValue != null)
{
jsonString = "{\"Foo\": \"" + jsonValue + "\"}";
}
else
{
jsonString = "{\"Foo\": null}";
}

var settings = new JsonSerializerOptions { Converters = { new SystemTextVerwendungszweckStringEnumConverter() } };
var actual = System.Text.Json.JsonSerializer.Deserialize<ClassWithNullableVerwendungszweck>(jsonString, settings);
actual.Should().NotBeNull();
actual.Foo.Should().Be(expectedVerwendungszweck);

Check warning on line 286 in BO4ETestProject/TestStringEnumConverter.cs

View workflow job for this annotation

GitHub Actions / unittest (7.0.100)

Dereference of a possibly null reference.

Check warning on line 286 in BO4ETestProject/TestStringEnumConverter.cs

View workflow job for this annotation

GitHub Actions / unittest (6.0.201)

Dereference of a possibly null reference.

Check warning on line 286 in BO4ETestProject/TestStringEnumConverter.cs

View workflow job for this annotation

GitHub Actions / unittest (8.0.100)

Dereference of a possibly null reference.

Check warning on line 286 in BO4ETestProject/TestStringEnumConverter.cs

View workflow job for this annotation

GitHub Actions / unittest (6.0.201)

Dereference of a possibly null reference.

Check warning on line 286 in BO4ETestProject/TestStringEnumConverter.cs

View workflow job for this annotation

GitHub Actions / unittest (7.0.100)

Dereference of a possibly null reference.

Check warning on line 286 in BO4ETestProject/TestStringEnumConverter.cs

View workflow job for this annotation

GitHub Actions / unittest (8.0.100)

Dereference of a possibly null reference.

Check warning on line 286 in BO4ETestProject/TestStringEnumConverter.cs

View workflow job for this annotation

GitHub Actions / pushrelease

Dereference of a possibly null reference.

var reSerializedJsonString = System.Text.Json.JsonSerializer.Serialize(actual, settings);
reSerializedJsonString.Should().Contain(expectedVerwendungszweck?.ToString() ?? "null");
}

[TestMethod]
[DataRow("MEHRMINDERMENGENABRECHNUNG", Verwendungszweck.MEHRMINDERMENGENABRECHNUNG)]
[DataRow("MEHRMINDERMBENGENABRECHNUNG", Verwendungszweck.MEHRMINDERMENGENABRECHNUNG)]
public void Test_System_Text_Gasqualitaet_Legacy_Converter_With_Non_Nullable_Gasqualitaet(string? jsonValue, Verwendungszweck expectedVerwendungszweck)
{
string jsonString;
if (jsonValue != null)
{
jsonString = "{\"Foo\": \"" + jsonValue + "\"}";
}
else
{
jsonString = "{\"Foo\": null}";
}
var settings = new JsonSerializerOptions { Converters = { new SystemTextVerwendungszweckStringEnumConverter() } };
var actual = System.Text.Json.JsonSerializer.Deserialize<ClassWithNonNullableVerwendungszweck>(jsonString, settings);
actual.Should().NotBeNull();
actual.Foo.Should().Be(expectedVerwendungszweck);

Check warning on line 309 in BO4ETestProject/TestStringEnumConverter.cs

View workflow job for this annotation

GitHub Actions / unittest (7.0.100)

Dereference of a possibly null reference.

Check warning on line 309 in BO4ETestProject/TestStringEnumConverter.cs

View workflow job for this annotation

GitHub Actions / unittest (6.0.201)

Dereference of a possibly null reference.

var reSerializedJsonString = System.Text.Json.JsonSerializer.Serialize(actual, settings);
reSerializedJsonString.Should().Contain(expectedVerwendungszweck.ToString());
}


[TestMethod]
[DataRow("MEHRMINDERMENGENABRECHNUNG", Verwendungszweck.MEHRMINDERMENGENABRECHNUNG)]
[DataRow("MEHRMINDERMBENGENABRECHNUNG", Verwendungszweck.MEHRMINDERMENGENABRECHNUNG)]
[DataRow(null, null)]
public void Test_Newtonsoft_Verwendungszweck_Legacy_Converter_With_Nullable_Verwendungszweck(string? jsonValue, Verwendungszweck? expectedVerwendungszweck)
{
string jsonString;
if (jsonValue != null)
{
jsonString = "{\"Foo\": \"" + jsonValue + "\"}";
}
else
{
jsonString = "{\"Foo\": null}";
}

var settings = new Newtonsoft.Json.JsonSerializerSettings()
{
Converters = { new NewtonsoftVerwendungszweckStringEnumConverter(), new StringEnumConverter() }
};
var actual = Newtonsoft.Json.JsonConvert.DeserializeObject<ClassWithNullableVerwendungszweck>(jsonString, settings);
actual.Should().NotBeNull();
actual.Foo.Should().Be(expectedVerwendungszweck);

Check warning on line 338 in BO4ETestProject/TestStringEnumConverter.cs

View workflow job for this annotation

GitHub Actions / unittest (7.0.100)

Dereference of a possibly null reference.

Check warning on line 338 in BO4ETestProject/TestStringEnumConverter.cs

View workflow job for this annotation

GitHub Actions / unittest (6.0.201)

Dereference of a possibly null reference.

var reSerializedJsonString = Newtonsoft.Json.JsonConvert.SerializeObject(actual, settings);
reSerializedJsonString.Should().Contain(expectedVerwendungszweck?.ToString() ?? "null");
}

[TestMethod]
[DataRow("MEHRMINDERMENGENABRECHNUNG", Verwendungszweck.MEHRMINDERMENGENABRECHNUNG)]
[DataRow("MEHRMINDERMBENGENABRECHNUNG", Verwendungszweck.MEHRMINDERMENGENABRECHNUNG)]
public void Test_Newtonsoft_Verwendungszweck_Legacy_Converter_With_Non_Nullable_Verwendungszweck(string? jsonValue, Verwendungszweck expectedVerwendungszweck)
{
string jsonString = "{\"Foo\": \"" + jsonValue + "\"}";
var settings = new Newtonsoft.Json.JsonSerializerSettings()
{
Converters = { new NewtonsoftVerwendungszweckStringEnumConverter(), new StringEnumConverter() }
};
var actual = Newtonsoft.Json.JsonConvert.DeserializeObject<ClassWithNonNullableVerwendungszweck>(jsonString, settings);
actual.Should().NotBeNull();
actual.Foo.Should().Be(expectedVerwendungszweck);

Check warning on line 356 in BO4ETestProject/TestStringEnumConverter.cs

View workflow job for this annotation

GitHub Actions / unittest (7.0.100)

Dereference of a possibly null reference.

Check warning on line 356 in BO4ETestProject/TestStringEnumConverter.cs

View workflow job for this annotation

GitHub Actions / unittest (6.0.201)

Dereference of a possibly null reference.

var reSerializedJsonString = Newtonsoft.Json.JsonConvert.SerializeObject(actual, settings);
reSerializedJsonString.Should().Contain(expectedVerwendungszweck.ToString());
}

[TestMethod]
[DataRow((int)Verwendungszweck.MEHRMINDERMENGENABRECHNUNG, Verwendungszweck.MEHRMINDERMENGENABRECHNUNG)]
public void Test_SystemText_Verwendungszweck_Legacy_Converter_With_Non_Nullable_Verwendungszweck_Integer(int jsonValue, Verwendungszweck expectedVerwendungszweck)
{
string jsonString = "{\"Foo\": " + jsonValue + "}";
var settings = new JsonSerializerOptions { Converters = { new SystemTextVerwendungszweckStringEnumConverter(), new JsonStringEnumConverter() } };
var actual = System.Text.Json.JsonSerializer.Deserialize<ClassWithNonNullableVerwendungszweck>(jsonString, settings);
actual.Should().NotBeNull();
actual.Foo.Should().Be(expectedVerwendungszweck);

var reSerializedJsonString = System.Text.Json.JsonSerializer.Serialize(actual, settings);
reSerializedJsonString.Should().Contain(expectedVerwendungszweck.ToString());
}

[TestMethod]
[DataRow((int)Verwendungszweck.MEHRMINDERMENGENABRECHNUNG, Verwendungszweck.MEHRMINDERMENGENABRECHNUNG)]
public void Test_Newtonsoft_Verwendungszweck_Legacy_Converter_With_Non_Nullable_Verwendungszweck_Integer(int jsonValue, Verwendungszweck expectedVerwendungszweck)
{
string jsonString = "{\"Foo\": " + jsonValue + "}";
var settings = new Newtonsoft.Json.JsonSerializerSettings()
{
Converters = { new NewtonsoftVerwendungszweckStringEnumConverter(), new StringEnumConverter() }
};
var actual = Newtonsoft.Json.JsonConvert.DeserializeObject<ClassWithNonNullableVerwendungszweck>(jsonString, settings);
actual.Should().NotBeNull();
actual.Foo.Should().Be(expectedVerwendungszweck);

var reSerializedJsonString = Newtonsoft.Json.JsonConvert.SerializeObject(actual, settings);
reSerializedJsonString.Should().Contain(expectedVerwendungszweck.ToString());
}

}
}

0 comments on commit 9e4258f

Please sign in to comment.