From 9e4258f411b7470fda5c35e31d769f798020fa14 Mon Sep 17 00:00:00 2001 From: konstantin Date: Thu, 5 Sep 2024 13:51:26 +0200 Subject: [PATCH] Drop Verwendungszweck `MEHRMINDERMBENGENABRECHNUNG` to avoid further undefined behaviour; Add Lenient Converters for Legacy Data/Consistency (#518) --------- Co-authored-by: Konstantin --- BO4E/ENUM/Verwendungszweck.cs | 7 - .../LenientJsonSerializerOptionsGenerator.cs | 1 + .../LenientJsonSerializerSettingsGenerator.cs | 3 +- .../VerwendungszweckStringEnumConverter.cs | 121 ++++++++++++++++ BO4ETestProject/TestEnumMembers.cs | 8 +- BO4ETestProject/TestEnums.cs | 29 ++++ BO4ETestProject/TestStringEnumConverter.cs | 137 ++++++++++++++++++ 7 files changed, 294 insertions(+), 12 deletions(-) create mode 100644 BO4E/meta/LenientConverters/VerwendungszweckStringEnumConverter.cs create mode 100644 BO4ETestProject/TestEnums.cs diff --git a/BO4E/ENUM/Verwendungszweck.cs b/BO4E/ENUM/Verwendungszweck.cs index f7c5ce5b..d5e471fe 100644 --- a/BO4E/ENUM/Verwendungszweck.cs +++ b/BO4E/ENUM/Verwendungszweck.cs @@ -39,11 +39,4 @@ public enum Verwendungszweck /// ZB5 [EnumMember(Value = "ERMITTLUNG_AUSGEGLICHENHEIT_BILANZKREIS")] ERMITTLUNG_AUSGEGLICHENHEIT_BILANZKREIS, - - /// - /// - /// - [Obsolete("This is only to keep the library backwards compatible")] - [EnumMember(Value = "MEHRMINDERMBENGENABRECHNUNG")] - MEHRMINDERMBENGENABRECHNUNG = MEHRMINDERMENGENABRECHNUNG, } \ No newline at end of file diff --git a/BO4E/meta/LenientConverters/LenientJsonSerializerOptionsGenerator.cs b/BO4E/meta/LenientConverters/LenientJsonSerializerOptionsGenerator.cs index 6b00a25c..0ca46b2d 100644 --- a/BO4E/meta/LenientConverters/LenientJsonSerializerOptionsGenerator.cs +++ b/BO4E/meta/LenientConverters/LenientJsonSerializerOptionsGenerator.cs @@ -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()); diff --git a/BO4E/meta/LenientConverters/LenientJsonSerializerSettingsGenerator.cs b/BO4E/meta/LenientConverters/LenientJsonSerializerSettingsGenerator.cs index 1e223b0c..8f32957f 100644 --- a/BO4E/meta/LenientConverters/LenientJsonSerializerSettingsGenerator.cs +++ b/BO4E/meta/LenientConverters/LenientJsonSerializerSettingsGenerator.cs @@ -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 { diff --git a/BO4E/meta/LenientConverters/VerwendungszweckStringEnumConverter.cs b/BO4E/meta/LenientConverters/VerwendungszweckStringEnumConverter.cs new file mode 100644 index 00000000..296dca3f --- /dev/null +++ b/BO4E/meta/LenientConverters/VerwendungszweckStringEnumConverter.cs @@ -0,0 +1,121 @@ +using System; +using BO4E.ENUM; + +namespace BO4E.meta.LenientConverters; + +/// +/// Converts 'MEHRMINDERMBENGENABRECHNUNG' (with typo!) to Enum Member . +/// +public class SystemTextVerwendungszweckStringEnumConverter : System.Text.Json.Serialization.JsonConverter +{ + /// + 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(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}."); + } + + /// + 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()); + } + } +} + +/// +/// converts 'MEHRMINDERMBENGENABRECHNUNG' (with typo!) to Enum Member +/// +/// +/// It is intended, that you use this converter TOGETHER with the . +/// In this case you need to add this converter BEFORE the in the list of converters. +/// +/// var settings = new Newtonsoft.Json.JsonSerializerSettings() +/// { +/// Converters = { new NewtonsoftVerwendungszweckStringEnumConverter(), new StringEnumConverter() } +/// }; +/// +/// +/// +public class NewtonsoftVerwendungszweckStringEnumConverter : Newtonsoft.Json.JsonConverter +{ + /// > + 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."); + } + + /// + 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()); + } + } + + /// + public override bool CanWrite => true; +} \ No newline at end of file diff --git a/BO4ETestProject/TestEnumMembers.cs b/BO4ETestProject/TestEnumMembers.cs index 1921dc88..b3da735d 100644 --- a/BO4ETestProject/TestEnumMembers.cs +++ b/BO4ETestProject/TestEnumMembers.cs @@ -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(myLegacyJson, options); myNewInstance.Verwendungszweck.Should().Be(Verwendungszweck.MEHRMINDERMENGENABRECHNUNG); } @@ -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(myLegacyJson, options); myNewInstance.Verwendungszweck.Should().Be(Verwendungszweck.MEHRMINDERMENGENABRECHNUNG); } diff --git a/BO4ETestProject/TestEnums.cs b/BO4ETestProject/TestEnums.cs new file mode 100644 index 00000000..9f1bf94c --- /dev/null +++ b/BO4ETestProject/TestEnums.cs @@ -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(); + 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 + } +} \ No newline at end of file diff --git a/BO4ETestProject/TestStringEnumConverter.cs b/BO4ETestProject/TestStringEnumConverter.cs index 2ed196ff..fd6485b1 100644 --- a/BO4ETestProject/TestStringEnumConverter.cs +++ b/BO4ETestProject/TestStringEnumConverter.cs @@ -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(jsonString, settings); + actual.Should().NotBeNull(); + actual.Foo.Should().Be(expectedVerwendungszweck); + + 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(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("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(jsonString, settings); + actual.Should().NotBeNull(); + actual.Foo.Should().Be(expectedVerwendungszweck); + + 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(jsonString, settings); + actual.Should().NotBeNull(); + actual.Foo.Should().Be(expectedVerwendungszweck); + + 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(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(jsonString, settings); + actual.Should().NotBeNull(); + actual.Foo.Should().Be(expectedVerwendungszweck); + + var reSerializedJsonString = Newtonsoft.Json.JsonConvert.SerializeObject(actual, settings); + reSerializedJsonString.Should().Contain(expectedVerwendungszweck.ToString()); + } + } }