Skip to content

Commit

Permalink
feat: Add JsonStringEnumMemberConverter
Browse files Browse the repository at this point in the history
  • Loading branch information
davidkallesen committed Jul 14, 2024
1 parent d00d39b commit a7c5945
Show file tree
Hide file tree
Showing 10 changed files with 211 additions and 4 deletions.
19 changes: 19 additions & 0 deletions docs/CodeDoc/Atc/Atc.Serialization.JsonConverters.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,25 @@
<br />
## JsonStringEnumMemberConverter&lt;T&gt;
>```csharp
>public class JsonStringEnumMemberConverter&lt;T&gt; : JsonConverter<T>
>```
### Methods
#### Read
>```csharp
>T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
>```
#### Write
>```csharp
>void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
>```
<br />
## JsonTimeSpanConverter
>```csharp
Expand Down
1 change: 1 addition & 0 deletions docs/CodeDoc/Atc/Index.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@
- [JsonElementObjectConverter](Atc.Serialization.JsonConverters.md#jsonelementobjectconverter)
- [JsonFileInfoToFullNameConverter](Atc.Serialization.JsonConverters.md#jsonfileinfotofullnameconverter)
- [JsonNumberToStringConverter](Atc.Serialization.JsonConverters.md#jsonnumbertostringconverter)
- [JsonStringEnumMemberConverter&lt;T&gt;](Atc.Serialization.JsonConverters.md#jsonstringenummemberconverter&lt;t&gt;)
- [JsonTimeSpanConverter](Atc.Serialization.JsonConverters.md#jsontimespanconverter)
- [JsonTypeDiscriminatorConverter&lt;T&gt;](Atc.Serialization.JsonConverters.md#jsontypediscriminatorconverter&lt;t&gt;)
- [JsonUnixDateTimeOffsetConverter](Atc.Serialization.JsonConverters.md#jsonunixdatetimeoffsetconverter)
Expand Down
4 changes: 4 additions & 0 deletions docs/CodeDoc/Atc/IndexExtended.md
Original file line number Diff line number Diff line change
Expand Up @@ -4861,6 +4861,10 @@
- CanConvert(Type typeToConvert)
- Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
- Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
- [JsonStringEnumMemberConverter&lt;T&gt;](Atc.Serialization.JsonConverters.md#jsonstringenummemberconverter&lt;t&gt;)
- Methods
- Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
- Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
- [JsonTimeSpanConverter](Atc.Serialization.JsonConverters.md#jsontimespanconverter)
- Methods
- Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
namespace Atc.Serialization.JsonConverters;

public sealed class JsonStringEnumMemberConverter<T> : JsonConverter<T>
where T : Enum
{
public override T Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options)
{
if (typeToConvert is null)
{
throw new ArgumentNullException(nameof(typeToConvert));
}

var enumValue = reader.GetString();
foreach (var field in typeToConvert.GetFields())
{
var enumMemberAttribute = field.GetCustomAttribute<EnumMemberAttribute>();

switch (enumMemberAttribute)
{
case null when
field.Name.Equals(enumValue, StringComparison.OrdinalIgnoreCase):
return (T)field.GetValue(null);
case null:
continue;
}

if (enumMemberAttribute.Value.Equals(enumValue, StringComparison.OrdinalIgnoreCase))
{
return (T)field.GetValue(null);
}
}

throw new JsonException($"Unable to convert \"{enumValue}\" to Enum \"{typeToConvert}\".");
}

public override void Write(
Utf8JsonWriter writer,
T value,
JsonSerializerOptions options)
{
if (writer is null)
{
throw new ArgumentNullException(nameof(writer));
}

if (options is null)
{
throw new ArgumentNullException(nameof(options));
}

var enumMemberAttribute = value
.GetType()
.GetField(value.ToString())
.GetCustomAttribute<EnumMemberAttribute>();

var enumValue = enumMemberAttribute?.Value ?? value.ToString();

writer.WriteStringValue(options.PropertyNamingPolicy == JsonNamingPolicy.CamelCase
? enumValue.EnsureFirstCharacterToLower()
: enumValue.EnsureFirstCharacterToUpper());
}
}
1 change: 1 addition & 0 deletions test/Atc.Tests/CodeComplianceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ public class CodeComplianceTests
typeof(System.TaskExtensions),
typeof(ThreadExtensions),
typeof(VersionExtensions),
typeof(JsonStringEnumMemberConverter<>),
};

public CodeComplianceTests(ITestOutputHelper testOutputHelper)
Expand Down
1 change: 1 addition & 0 deletions test/Atc.Tests/GlobalUsings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
global using System.Globalization;
global using System.Net;
global using System.Reflection;
global using System.Runtime.Serialization;
global using System.Runtime.Versioning;
global using System.Security.Claims;
global using System.Security.Cryptography.X509Certificates;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,20 @@ public class JsonComplexDataTests
public void ToJson()
{
// Arrange
var jsonSerializerOptions = JsonSerializerOptionsFactory.Create();
var jsonSerializerFactorySettings = new JsonSerializerFactorySettings
{
UseConverterEnumAsString = false,
};

var jsonSerializerOptions = JsonSerializerOptionsFactory.Create(jsonSerializerFactorySettings);
jsonSerializerOptions.Converters.Add(new JsonCultureInfoToNameConverter());
jsonSerializerOptions.Converters.Add(new JsonDirectoryInfoToFullNameConverter());
jsonSerializerOptions.Converters.Add(new JsonFileInfoToFullNameConverter());
jsonSerializerOptions.Converters.Add(new JsonTimeSpanConverter());
jsonSerializerOptions.Converters.Add(new JsonDateTimeOffsetMinToNullConverter());
jsonSerializerOptions.Converters.Add(new JsonUriToAbsoluteUriConverter());
jsonSerializerOptions.Converters.Add(new JsonVersionConverter());
jsonSerializerOptions.Converters.Add(new JsonStringEnumMemberConverter<ChargePointState>());

var data = new ComplexData
{
Expand All @@ -24,6 +30,7 @@ public void ToJson()
MyDateTimeOffset = new DateTimeOffset(2023, 11, 29, 20, 39, 22, 123, TimeSpan.Zero),
MyUri = new Uri("http://dr.dk/"),
MyVersion = new Version(1, 2, 3, 4),
State = ChargePointState.BusyNonReleased,
};

var expected = "{\r\n" +
Expand All @@ -33,7 +40,8 @@ public void ToJson()
" \"myTimeSpan\": \"-10675199.02:48:05.4775808\",\r\n" +
" \"myDateTimeOffset\": \"2023-11-29T20:39:22.123+00:00\",\r\n" +
" \"myUri\": \"http://dr.dk/\",\r\n" +
" \"myVersion\": \"1.2.3.4\"\r\n" +
" \"myVersion\": \"1.2.3.4\",\r\n" +
" \"state\": \"busy-non-released\"\r\n" +
"}".EnsureEnvironmentNewLines();

// Atc
Expand All @@ -51,14 +59,20 @@ public void ToJson()
public void FromJson()
{
// Arrange
var jsonSerializerOptions = JsonSerializerOptionsFactory.Create();
var jsonSerializerFactorySettings = new JsonSerializerFactorySettings
{
UseConverterEnumAsString = false,
};

var jsonSerializerOptions = JsonSerializerOptionsFactory.Create(jsonSerializerFactorySettings);
jsonSerializerOptions.Converters.Add(new JsonCultureInfoToNameConverter());
jsonSerializerOptions.Converters.Add(new JsonDirectoryInfoToFullNameConverter());
jsonSerializerOptions.Converters.Add(new JsonFileInfoToFullNameConverter());
jsonSerializerOptions.Converters.Add(new JsonTimeSpanConverter());
jsonSerializerOptions.Converters.Add(new JsonDateTimeOffsetMinToNullConverter());
jsonSerializerOptions.Converters.Add(new JsonUriToAbsoluteUriConverter());
jsonSerializerOptions.Converters.Add(new JsonVersionConverter());
jsonSerializerOptions.Converters.Add(new JsonStringEnumMemberConverter<ChargePointState>());

const string data = "{\r\n" +
" \"myCulture\": \"en-US\",\r\n" +
Expand All @@ -67,7 +81,8 @@ public void FromJson()
" \"myTimeSpan\": \"-10675199.02:48:05.4775808\",\r\n" +
" \"myDateTimeOffset\": \"2023-11-29T20:39:22.123+00:00\",\r\n" +
" \"myUri\": \"http://dr.dk/\",\r\n" +
" \"myVersion\": \"1.2.3.4\"\r\n" +
" \"myVersion\": \"1.2.3.4\",\r\n" +
" \"state\": \"busy-non-released\"\r\n" +
"}";

var expected = new ComplexData
Expand All @@ -79,6 +94,7 @@ public void FromJson()
MyDateTimeOffset = new DateTimeOffset(2023, 11, 29, 20, 39, 22, 123, TimeSpan.Zero),
MyUri = new Uri("http://dr.dk/"),
MyVersion = new Version(1, 2, 3, 4),
State = ChargePointState.BusyNonReleased,
};

// Atc
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
namespace Atc.Tests.Serialization.JsonConverters;

public class JsonStringEnumMemberConverterTests
{
[Theory]
[InlineData(ChargePointState.BusyNonCharging, "busy-non-charging")]
[InlineData(ChargePointState.TestWithoutEnumMember, "testWithoutEnumMember")]
public void Read_ShouldReturnExpectedChargePointState(ChargePointState expected, string enumValue)
{
// Arrange
var jsonSerializerOptions = JsonSerializerOptionsFactory.Create();
var jsonConverter = new JsonStringEnumMemberConverter<ChargePointState>();
var json = $"\"{enumValue.Replace("\\", "\\\\", StringComparison.Ordinal)}\"";
var utf8JsonReader = new Utf8JsonReader(Encoding.UTF8.GetBytes(json));

utf8JsonReader.Read();

// Act
var result = jsonConverter.Read(ref utf8JsonReader, typeof(ChargePointState), jsonSerializerOptions);

// Assert
if (OperatingSystem.IsWindows())
{
Assert.Equal(expected, result);
}
}

[Theory]
[InlineData("busy-non-charging", ChargePointState.BusyNonCharging)]
[InlineData("testWithoutEnumMember", ChargePointState.TestWithoutEnumMember)]
public void Write_ShouldWriteChargePointStateToUtf8JsonWriter(string expected, ChargePointState chargePointState)
{
// Arrange
var jsonSerializerOptions = JsonSerializerOptionsFactory.Create();
var jsonConverter = new JsonStringEnumMemberConverter<ChargePointState>();
var memoryStream = new MemoryStream();
using var utf8JsonWriter = new Utf8JsonWriter(memoryStream);

// Act
jsonConverter.Write(utf8JsonWriter, chargePointState, jsonSerializerOptions);

// Assert
utf8JsonWriter.Flush();
var result = Encoding.UTF8.GetString(memoryStream.ToArray());

if (OperatingSystem.IsWindows())
{
Assert.Equal($"\"{expected}\"", result.Replace("\\\\", "\\", StringComparison.Ordinal));
}
}
}
47 changes: 47 additions & 0 deletions test/Atc.Tests/Serialization/XUnitTestTypes/ChargePointState.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
namespace Atc.Tests.Serialization.XUnitTestTypes;

[SuppressMessage("Naming", "CA1700:Do not name enum values 'Reserved'", Justification = "OK - For testing.")]
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum ChargePointState
{
[EnumMember(Value = "available")]
Available,

[EnumMember(Value = "busy")]
Busy,

[EnumMember(Value = "busy-blocked")]
BusyBlocked,

[EnumMember(Value = "busy-charging")]
BusyCharging,

[EnumMember(Value = "busy-non-charging")]
BusyNonCharging,

[EnumMember(Value = "busy-non-released")]
BusyNonReleased,

[EnumMember(Value = "busy-reserved")]
BusyReserved,

[EnumMember(Value = "busy-scheduled")]
BusyScheduled,

[EnumMember(Value = "error")]
Error,

[EnumMember(Value = "disconnected")]
Disconnected,

[EnumMember(Value = "passive")]
Passive,

[EnumMember(Value = "maintenance")]
Maintenance,

[EnumMember(Value = "other")]
Other,

TestWithoutEnumMember,
}
2 changes: 2 additions & 0 deletions test/Atc.Tests/Serialization/XUnitTestTypes/ComplexData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ public class ComplexData

public Version? MyVersion { get; set; }

public ChargePointState? State { get; set; }

public override string ToString()
=> $"{nameof(MyCulture)}: {MyCulture}, {nameof(MyDirectory)}: {MyDirectory}, {nameof(MyFile)}: {MyFile}, {nameof(MyTimeSpan)}: {MyTimeSpan}, {nameof(MyDateTimeOffset)}: {MyDateTimeOffset}, {nameof(MyUri)}: {MyUri}, {nameof(MyVersion)}: {MyVersion}";
}

0 comments on commit a7c5945

Please sign in to comment.