Skip to content

Commit

Permalink
Support enum as property name (#27)
Browse files Browse the repository at this point in the history
* Add SerializeEnumDictionaryWorks

* Implement ReadAsPropertyName, WriteAsPropertyName

* Add IReadOnlyDictionary into MyJsonSerializerContext

* Fix ReadAsPropertyName

* Fix TryToEnum

* Add DeserializeEnumDictionaryWorks

* Update fixture

* Apply ReplaceLineEndings
  • Loading branch information
trejjam authored Oct 23, 2023
1 parent 1b4e5c4 commit 3f70a14
Show file tree
Hide file tree
Showing 24 changed files with 1,457 additions and 181 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Aviationexam.GeneratedJsonConverters.SourceGenerator.Target.Contracts;
using System;
using System.Collections.Generic;
using System.Text.Json;
using Xunit;

Expand Down Expand Up @@ -80,4 +81,62 @@ string sourceJson

Assert.Equal(expectedValue, serializedValue);
}

[Theory]
[MemberData(nameof(EnumDictionaryData))]
public void SerializeEnumDictionaryWorks(Type type, object dictionary, string json)
{
var serializedValue = JsonSerializer.Serialize(
dictionary,
type,
MyJsonSerializerContext.Default.Options
);

Assert.Equal(json, serializedValue.ReplaceLineEndings("\n"));
}

[Theory]
[MemberData(nameof(EnumDictionaryData))]
public void DeserializeEnumDictionaryWorks(Type type, object dictionary, string json)
{
var deserializedDictionary = JsonSerializer.Deserialize(
json,
type,
MyJsonSerializerContext.Default.Options
);

Assert.Equal(dictionary, deserializedDictionary);
}

public static IEnumerable<object[]> EnumDictionaryData()
{
yield return new object[]
{
typeof(IReadOnlyDictionary<EBackingEnum, int>),
new Dictionary<EBackingEnum, int>
{
[EBackingEnum.B] = 2,
},
// language=json
"""
{
"1": 2
}
""".ReplaceLineEndings("\n"),
};
yield return new object[]
{
typeof(IReadOnlyDictionary<EConfiguredPropertyEnum, int>),
new Dictionary<EConfiguredPropertyEnum, int>
{
[EConfiguredPropertyEnum.B] = 2,
},
// language=json
"""
{
"D": 2
}
""".ReplaceLineEndings("\n"),
};
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Aviationexam.GeneratedJsonConverters.SourceGenerator.Target.Contracts;
using Aviationexam.GeneratedJsonConverters.SourceGenerator.Target.ContractWithCustomDelimiter;
using System.Collections.Generic;
using System.Text.Json.Serialization;

namespace Aviationexam.GeneratedJsonConverters.SourceGenerator.Target;
Expand All @@ -21,6 +22,8 @@ namespace Aviationexam.GeneratedJsonConverters.SourceGenerator.Target;
[JsonSerializable(typeof(EMyEnum))]
[JsonSerializable(typeof(EPropertyEnum))]
[JsonSerializable(typeof(EPropertyWithBackingEnum))]
[JsonSerializable(typeof(IReadOnlyDictionary<EBackingEnum, int>))]
[JsonSerializable(typeof(IReadOnlyDictionary<EConfiguredPropertyEnum, int>))]
public partial class MyJsonSerializerContext : JsonSerializerContext
{
static MyJsonSerializerContext()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ internal abstract class EnumJsonConvertor<T, TBackingType> : JsonConverter<T>

protected abstract EnumSerializationStrategy SerializationStrategy { get; }

protected abstract T ToEnum(ReadOnlySpan<byte> enumName);
protected abstract bool TryToEnum(ReadOnlySpan<byte> enumName, out T value);

protected abstract T ToEnum(TBackingType numericValue);
protected abstract bool TryToEnum(TBackingType numericValue, out T value);

protected abstract TBackingType ToBackingType(T value);

Expand All @@ -41,7 +41,14 @@ reader.TokenType is JsonTokenType.String
{
var enumName = reader.ValueSpan;

return ToEnum(enumName);
if (TryToEnum(enumName, out var enumValue))
{
return enumValue;
}

var stringValue = Encoding.UTF8.GetString(enumName.ToArray());

throw new JsonException($"Undefined mapping of '{stringValue}' to enum '{typeof(T).FullName}'");
}

if (reader.TokenType is JsonTokenType.Number)
Expand All @@ -50,7 +57,12 @@ reader.TokenType is JsonTokenType.String

if (numericValue.HasValue)
{
return ToEnum(numericValue.Value);
if (TryToEnum(numericValue.Value, out var enumValue))
{
return enumValue;
}

throw new JsonException($"Undefined mapping of '{numericValue}' to enum '{{enumFullName}}'");
}
}

Expand All @@ -59,6 +71,44 @@ reader.TokenType is JsonTokenType.String
throw new JsonException($"Unable to deserialize {value}('{reader.TokenType}') into {typeof(T).Name}");
}

public override T ReadAsPropertyName(
ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options
)
{
if (
reader.TokenType is JsonTokenType.PropertyName
&& DeserializationStrategy.HasFlag(EnumDeserializationStrategy.UseEnumName)
)
{
var enumName = reader.ValueSpan;

if (TryToEnum(enumName, out var enumValue))
{
return enumValue;
}
}

var value = Encoding.UTF8.GetString(reader.ValueSpan.ToArray());

if (
reader.TokenType is JsonTokenType.PropertyName
&& DeserializationStrategy.HasFlag(EnumDeserializationStrategy.UseBackingType)
)
{
var numericValue = ParseAsNumber(value);

if (numericValue.HasValue)
{
if (TryToEnum(numericValue.Value, out var enumValue))
{
return enumValue;
}
}
}

throw new JsonException($"Unable to deserialize {value}('{reader.TokenType}') into {typeof(T).Name}");
}

private TBackingType? ReadAsNumber(ref Utf8JsonReader reader) => BackingTypeTypeCode switch
{
TypeCode.SByte => reader.GetSByte() is var numericValue ? Unsafe.As<sbyte, TBackingType>(ref numericValue) : null,
Expand All @@ -72,6 +122,21 @@ reader.TokenType is JsonTokenType.String
_ => throw new ArgumentOutOfRangeException(nameof(BackingTypeTypeCode), BackingTypeTypeCode, $"Unexpected TypeCode {BackingTypeTypeCode}")
};

private TBackingType? ParseAsNumber(
string value
) => BackingTypeTypeCode switch
{
TypeCode.SByte => sbyte.TryParse(value, out var numericValue) ? Unsafe.As<sbyte, TBackingType>(ref numericValue) : null,
TypeCode.Byte => byte.TryParse(value, out var numericValue) ? Unsafe.As<byte, TBackingType>(ref numericValue) : null,
TypeCode.Int16 => short.TryParse(value, out var numericValue) ? Unsafe.As<short, TBackingType>(ref numericValue) : null,
TypeCode.UInt16 => ushort.TryParse(value, out var numericValue) ? Unsafe.As<ushort, TBackingType>(ref numericValue) : null,
TypeCode.Int32 => int.TryParse(value, out var numericValue) ? Unsafe.As<int, TBackingType>(ref numericValue) : null,
TypeCode.UInt32 => uint.TryParse(value, out var numericValue) ? Unsafe.As<uint, TBackingType>(ref numericValue) : null,
TypeCode.Int64 => long.TryParse(value, out var numericValue) ? Unsafe.As<long, TBackingType>(ref numericValue) : null,
TypeCode.UInt64 => ulong.TryParse(value, out var numericValue) ? Unsafe.As<ulong, TBackingType>(ref numericValue) : null,
_ => throw new ArgumentOutOfRangeException(nameof(BackingTypeTypeCode), BackingTypeTypeCode, $"Unexpected TypeCode {BackingTypeTypeCode}")
};

public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
if (SerializationStrategy is EnumSerializationStrategy.BackingType)
Expand All @@ -88,6 +153,22 @@ public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions
}
}

public override void WriteAsPropertyName(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
if (SerializationStrategy is EnumSerializationStrategy.BackingType)
{
WriteAsPropertyNameAsBackingType(writer, value, options);
}
else if (SerializationStrategy is EnumSerializationStrategy.FirstEnumName)
{
WriteAsPropertyNameAsFirstEnumName(writer, value, options);
}
else
{
throw new ArgumentOutOfRangeException(nameof(SerializationStrategy), SerializationStrategy, "Unknown serialization strategy");
}
}

private void WriteAsBackingType(
Utf8JsonWriter writer,
T value,
Expand Down Expand Up @@ -128,6 +209,18 @@ JsonSerializerOptions options
}
}

private void WriteAsPropertyNameAsBackingType(
Utf8JsonWriter writer,
T value,
[SuppressMessage("ReSharper", "UnusedParameter.Local")]
JsonSerializerOptions options
)
{
var numericValue = ToBackingType(value);

writer.WritePropertyName($"{numericValue}");
}

private void WriteAsFirstEnumName(
Utf8JsonWriter writer,
T value,
Expand All @@ -139,4 +232,16 @@ JsonSerializerOptions options

writer.WriteStringValue(enumValue);
}

private void WriteAsPropertyNameAsFirstEnumName(
Utf8JsonWriter writer,
T value,
[SuppressMessage("ReSharper", "UnusedParameter.Local")]
JsonSerializerOptions options
)
{
var enumValue = ToFirstEnumName(value);

writer.WritePropertyName(enumValue);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,23 @@ internal class EBackingEnumEnumJsonConverter : Aviationexam.GeneratedJsonConvert

protected override Aviationexam.GeneratedJsonConverters.EnumSerializationStrategy SerializationStrategy => Aviationexam.GeneratedJsonConverters.EnumSerializationStrategy.BackingType;

protected override ApplicationNamespace.Contracts.EBackingEnum ToEnum(
System.ReadOnlySpan<byte> enumName
protected override bool TryToEnum(
System.ReadOnlySpan<byte> enumName, out ApplicationNamespace.Contracts.EBackingEnum value
) => throw new System.Text.Json.JsonException("Enum is not configured to support deserialization from enum name");

protected override ApplicationNamespace.Contracts.EBackingEnum ToEnum(
System.Int32 numericValue
) => numericValue switch
protected override bool TryToEnum(
System.Int32 numericValue, out ApplicationNamespace.Contracts.EBackingEnum value
)
{
0 => ApplicationNamespace.Contracts.EBackingEnum.A,
1 => ApplicationNamespace.Contracts.EBackingEnum.B,
_ => throw new System.Text.Json.JsonException($"Undefined mapping of '{numericValue}' to enum 'ApplicationNamespace.Contracts.EBackingEnum'"),
};
(var tryValue, value) = numericValue switch
{
0 => (true, ApplicationNamespace.Contracts.EBackingEnum.A),
1 => (true, ApplicationNamespace.Contracts.EBackingEnum.B),
_ => (false, default(ApplicationNamespace.Contracts.EBackingEnum)),
};

return tryValue;
}

protected override System.Int32 ToBackingType(
ApplicationNamespace.Contracts.EBackingEnum value
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,27 @@ internal class EPropertyEnumEnumJsonConverter : Aviationexam.GeneratedJsonConver

protected override Aviationexam.GeneratedJsonConverters.EnumSerializationStrategy SerializationStrategy => Aviationexam.GeneratedJsonConverters.EnumSerializationStrategy.FirstEnumName;

protected override ApplicationNamespace.Contracts.EPropertyEnum ToEnum(
System.ReadOnlySpan<byte> enumName
protected override bool TryToEnum(
System.ReadOnlySpan<byte> enumName, out ApplicationNamespace.Contracts.EPropertyEnum value
)
{
if (System.MemoryExtensions.SequenceEqual(enumName, "C"u8))
{
return ApplicationNamespace.Contracts.EPropertyEnum.C;
value = ApplicationNamespace.Contracts.EPropertyEnum.C;
return true;
}
if (System.MemoryExtensions.SequenceEqual(enumName, "D"u8))
{
return ApplicationNamespace.Contracts.EPropertyEnum.D;
value = ApplicationNamespace.Contracts.EPropertyEnum.D;
return true;
}

var stringValue = System.Text.Encoding.UTF8.GetString(enumName.ToArray());

throw new System.Text.Json.JsonException($"Undefined mapping of '{stringValue}' to enum 'ApplicationNamespace.Contracts.EPropertyEnum'");
value = default(ApplicationNamespace.Contracts.EPropertyEnum);
return false;
}

protected override ApplicationNamespace.Contracts.EPropertyEnum ToEnum(
System.Byte numericValue
protected override bool TryToEnum(
System.Byte numericValue, out ApplicationNamespace.Contracts.EPropertyEnum value
) => throw new System.Text.Json.JsonException("Enum is not configured to support deserialization from backing type");

protected override System.Byte ToBackingType(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,32 +11,38 @@ internal class EPropertyWithBackingEnumEnumJsonConverter : Aviationexam.Generate

protected override Aviationexam.GeneratedJsonConverters.EnumSerializationStrategy SerializationStrategy => Aviationexam.GeneratedJsonConverters.EnumSerializationStrategy.FirstEnumName;

protected override ApplicationNamespace.Contracts.EPropertyWithBackingEnum ToEnum(
System.ReadOnlySpan<byte> enumName
protected override bool TryToEnum(
System.ReadOnlySpan<byte> enumName, out ApplicationNamespace.Contracts.EPropertyWithBackingEnum value
)
{
if (System.MemoryExtensions.SequenceEqual(enumName, "E"u8))
{
return ApplicationNamespace.Contracts.EPropertyWithBackingEnum.E;
value = ApplicationNamespace.Contracts.EPropertyWithBackingEnum.E;
return true;
}
if (System.MemoryExtensions.SequenceEqual(enumName, "F"u8))
{
return ApplicationNamespace.Contracts.EPropertyWithBackingEnum.F;
value = ApplicationNamespace.Contracts.EPropertyWithBackingEnum.F;
return true;
}

var stringValue = System.Text.Encoding.UTF8.GetString(enumName.ToArray());

throw new System.Text.Json.JsonException($"Undefined mapping of '{stringValue}' to enum 'ApplicationNamespace.Contracts.EPropertyWithBackingEnum'");
value = default(ApplicationNamespace.Contracts.EPropertyWithBackingEnum);
return false;
}

protected override ApplicationNamespace.Contracts.EPropertyWithBackingEnum ToEnum(
System.Int32 numericValue
) => numericValue switch
protected override bool TryToEnum(
System.Int32 numericValue, out ApplicationNamespace.Contracts.EPropertyWithBackingEnum value
)
{
0 => ApplicationNamespace.Contracts.EPropertyWithBackingEnum.E,
1 => ApplicationNamespace.Contracts.EPropertyWithBackingEnum.F,
_ => throw new System.Text.Json.JsonException($"Undefined mapping of '{numericValue}' to enum 'ApplicationNamespace.Contracts.EPropertyWithBackingEnum'"),
};
(var tryValue, value) = numericValue switch
{
0 => (true, ApplicationNamespace.Contracts.EPropertyWithBackingEnum.E),
1 => (true, ApplicationNamespace.Contracts.EPropertyWithBackingEnum.F),
_ => (false, default(ApplicationNamespace.Contracts.EPropertyWithBackingEnum)),
};

return tryValue;
}

protected override System.Int32 ToBackingType(
ApplicationNamespace.Contracts.EPropertyWithBackingEnum value
Expand Down
Loading

0 comments on commit 3f70a14

Please sign in to comment.