diff --git a/src/Aviationexam.GeneratedJsonConverters.SourceGenerator.Target.Tests/EnumSerializationTests.cs b/src/Aviationexam.GeneratedJsonConverters.SourceGenerator.Target.Tests/EnumSerializationTests.cs index b190823..76238aa 100644 --- a/src/Aviationexam.GeneratedJsonConverters.SourceGenerator.Target.Tests/EnumSerializationTests.cs +++ b/src/Aviationexam.GeneratedJsonConverters.SourceGenerator.Target.Tests/EnumSerializationTests.cs @@ -1,4 +1,5 @@ using Aviationexam.GeneratedJsonConverters.SourceGenerator.Target.Contracts; +using System; using System.Text.Json; using Xunit; @@ -7,21 +8,35 @@ namespace Aviationexam.GeneratedJsonConverters.SourceGenerator.Target.Tests; public class EnumSerializationTests { [Theory] - [InlineData(EBackingEnum.A, "0")] - [InlineData(EBackingEnum.B, "1")] - [InlineData(EConfiguredPropertyEnum.A, "\"C\"")] - [InlineData(EConfiguredPropertyEnum.B, "\"D\"")] - [InlineData(EMyEnum.A, "\"A\"")] - [InlineData(EMyEnum.B, "\"B\"")] - [InlineData(EPropertyEnum.C, "\"C\"")] - [InlineData(EPropertyEnum.D, "\"D\"")] - [InlineData(EPropertyWithBackingEnum.E, "\"E\"")] - [InlineData(EPropertyWithBackingEnum.F, "\"F\"")] - public void SerializeEnumWorks(object enumValue, string expectedValue) + [InlineData(typeof(EBackingEnum), EBackingEnum.A, "0")] + [InlineData(typeof(EBackingEnum), EBackingEnum.B, "1")] + [InlineData(typeof(EConfiguredPropertyEnum), EConfiguredPropertyEnum.A, "\"C\"")] + [InlineData(typeof(EConfiguredPropertyEnum), EConfiguredPropertyEnum.B, "\"D\"")] + [InlineData(typeof(EMyEnum), EMyEnum.A, "\"A\"")] + [InlineData(typeof(EMyEnum), EMyEnum.B, "\"B\"")] + [InlineData(typeof(EPropertyEnum), EPropertyEnum.C, "\"C\"")] + [InlineData(typeof(EPropertyEnum), EPropertyEnum.D, "\"D\"")] + [InlineData(typeof(EPropertyWithBackingEnum), EPropertyWithBackingEnum.E, "\"E\"")] + [InlineData(typeof(EPropertyWithBackingEnum), EPropertyWithBackingEnum.F, "\"F\"")] + [InlineData(typeof(EDuplicatedValueUsingBackingTypeEnum), EDuplicatedValueUsingBackingTypeEnum.A, "0")] + [InlineData(typeof(EDuplicatedValueUsingBackingTypeEnum), EDuplicatedValueUsingBackingTypeEnum.B, "1")] +#pragma warning disable xUnit1025 + [InlineData(typeof(EDuplicatedValueUsingBackingTypeEnum), EDuplicatedValueUsingBackingTypeEnum.C, "1")] +#pragma warning restore xUnit1025 + [InlineData(typeof(EDuplicatedValueUsingFirstEnumNameEnum), EDuplicatedValueUsingFirstEnumNameEnum.A, "\"C\"")] + [InlineData(typeof(EDuplicatedValueUsingFirstEnumNameEnum), EDuplicatedValueUsingFirstEnumNameEnum.B, "\"D\"")] +#pragma warning disable xUnit1025 + [InlineData(typeof(EDuplicatedValueUsingFirstEnumNameEnum), EDuplicatedValueUsingFirstEnumNameEnum.C, "\"D\"")] +#pragma warning restore xUnit1025 + public void SerializeEnumWorks( + Type type, + object enumValue, + string expectedValue + ) { var serializedValue = JsonSerializer.Serialize( enumValue, - enumValue.GetType(), + type, MyJsonSerializerContext.Default.Options ); @@ -29,23 +44,37 @@ public void SerializeEnumWorks(object enumValue, string expectedValue) } [Theory] - [InlineData(EBackingEnum.A, "0")] - [InlineData(EBackingEnum.B, "1")] - [InlineData(EConfiguredPropertyEnum.A, "\"C\"")] - [InlineData(EConfiguredPropertyEnum.B, "\"D\"")] - [InlineData(EMyEnum.A, "\"A\"")] - [InlineData(EMyEnum.B, "\"B\"")] - [InlineData(EPropertyEnum.C, "\"C\"")] - [InlineData(EPropertyEnum.D, "\"D\"")] - [InlineData(EPropertyWithBackingEnum.E, "\"E\"")] - [InlineData(EPropertyWithBackingEnum.F, "\"F\"")] - [InlineData(EPropertyWithBackingEnum.E, "0")] - [InlineData(EPropertyWithBackingEnum.F, "1")] - public void DeserializeEnumWorks(object expectedValue, string sourceJson) + [InlineData(typeof(EBackingEnum), EBackingEnum.A, "0")] + [InlineData(typeof(EBackingEnum), EBackingEnum.B, "1")] + [InlineData(typeof(EConfiguredPropertyEnum), EConfiguredPropertyEnum.A, "\"C\"")] + [InlineData(typeof(EConfiguredPropertyEnum), EConfiguredPropertyEnum.B, "\"D\"")] + [InlineData(typeof(EMyEnum), EMyEnum.A, "\"A\"")] + [InlineData(typeof(EMyEnum), EMyEnum.B, "\"B\"")] + [InlineData(typeof(EPropertyEnum), EPropertyEnum.C, "\"C\"")] + [InlineData(typeof(EPropertyEnum), EPropertyEnum.D, "\"D\"")] + [InlineData(typeof(EPropertyWithBackingEnum), EPropertyWithBackingEnum.E, "\"E\"")] + [InlineData(typeof(EPropertyWithBackingEnum), EPropertyWithBackingEnum.F, "\"F\"")] + [InlineData(typeof(EPropertyWithBackingEnum), EPropertyWithBackingEnum.E, "0")] + [InlineData(typeof(EPropertyWithBackingEnum), EPropertyWithBackingEnum.F, "1")] + [InlineData(typeof(EDuplicatedValueUsingBackingTypeEnum), EDuplicatedValueUsingBackingTypeEnum.A, "0")] + [InlineData(typeof(EDuplicatedValueUsingBackingTypeEnum), EDuplicatedValueUsingBackingTypeEnum.B, "1")] +#pragma warning disable xUnit1025 + [InlineData(typeof(EDuplicatedValueUsingBackingTypeEnum), EDuplicatedValueUsingBackingTypeEnum.C, "1")] +#pragma warning restore xUnit1025 + [InlineData(typeof(EDuplicatedValueUsingFirstEnumNameEnum), EDuplicatedValueUsingFirstEnumNameEnum.A, "\"C\"")] + [InlineData(typeof(EDuplicatedValueUsingFirstEnumNameEnum), EDuplicatedValueUsingFirstEnumNameEnum.B, "\"D\"")] +#pragma warning disable xUnit1025 + [InlineData(typeof(EDuplicatedValueUsingFirstEnumNameEnum), EDuplicatedValueUsingFirstEnumNameEnum.C, "\"E\"")] +#pragma warning restore xUnit1025 + public void DeserializeEnumWorks( + Type type, + object expectedValue, + string sourceJson + ) { var serializedValue = JsonSerializer.Deserialize( sourceJson, - expectedValue.GetType(), + type, MyJsonSerializerContext.Default.Options ); diff --git a/src/Aviationexam.GeneratedJsonConverters.SourceGenerator.Target/Contracts/EConfiguredPropertyEnum.cs b/src/Aviationexam.GeneratedJsonConverters.SourceGenerator.Target/Contracts/EConfiguredPropertyEnum.cs index bc4fce6..c5ca53f 100644 --- a/src/Aviationexam.GeneratedJsonConverters.SourceGenerator.Target/Contracts/EConfiguredPropertyEnum.cs +++ b/src/Aviationexam.GeneratedJsonConverters.SourceGenerator.Target/Contracts/EConfiguredPropertyEnum.cs @@ -11,6 +11,7 @@ public enum EConfiguredPropertyEnum { [EnumMember(Value = "C")] A, + [EnumMember(Value = "D")] B, } diff --git a/src/Aviationexam.GeneratedJsonConverters.SourceGenerator.Target/Contracts/EDuplicatedValueUsingBackingTypeEnum.cs b/src/Aviationexam.GeneratedJsonConverters.SourceGenerator.Target/Contracts/EDuplicatedValueUsingBackingTypeEnum.cs new file mode 100644 index 0000000..7c2ab02 --- /dev/null +++ b/src/Aviationexam.GeneratedJsonConverters.SourceGenerator.Target/Contracts/EDuplicatedValueUsingBackingTypeEnum.cs @@ -0,0 +1,17 @@ +using Aviationexam.GeneratedJsonConverters.Attributes; +using System.Runtime.Serialization; + +namespace Aviationexam.GeneratedJsonConverters.SourceGenerator.Target.Contracts; + +[EnumJsonConverter(SerializationStrategy = EnumSerializationStrategy.BackingType, DeserializationStrategy = EnumDeserializationStrategy.UseBackingType)] +public enum EDuplicatedValueUsingBackingTypeEnum +{ + [EnumMember(Value = "C")] + A = 0, + + [EnumMember(Value = "D")] + B = 1, + + [EnumMember(Value = "E")] + C = 1, +} diff --git a/src/Aviationexam.GeneratedJsonConverters.SourceGenerator.Target/Contracts/EDuplicatedValueUsingFirstEnumNameEnum.cs b/src/Aviationexam.GeneratedJsonConverters.SourceGenerator.Target/Contracts/EDuplicatedValueUsingFirstEnumNameEnum.cs new file mode 100644 index 0000000..a811f79 --- /dev/null +++ b/src/Aviationexam.GeneratedJsonConverters.SourceGenerator.Target/Contracts/EDuplicatedValueUsingFirstEnumNameEnum.cs @@ -0,0 +1,17 @@ +using Aviationexam.GeneratedJsonConverters.Attributes; +using System.Runtime.Serialization; + +namespace Aviationexam.GeneratedJsonConverters.SourceGenerator.Target.Contracts; + +[EnumJsonConverter(SerializationStrategy = EnumSerializationStrategy.FirstEnumName, DeserializationStrategy = EnumDeserializationStrategy.UseEnumName)] +public enum EDuplicatedValueUsingFirstEnumNameEnum +{ + [EnumMember(Value = "C")] + A = 0, + + [EnumMember(Value = "D")] + B = 1, + + [EnumMember(Value = "E")] + C = 1, +} diff --git a/src/Aviationexam.GeneratedJsonConverters.SourceGenerator.Target/MyJsonSerializerContext.cs b/src/Aviationexam.GeneratedJsonConverters.SourceGenerator.Target/MyJsonSerializerContext.cs index 899c9f7..0b0d1ca 100644 --- a/src/Aviationexam.GeneratedJsonConverters.SourceGenerator.Target/MyJsonSerializerContext.cs +++ b/src/Aviationexam.GeneratedJsonConverters.SourceGenerator.Target/MyJsonSerializerContext.cs @@ -16,6 +16,8 @@ namespace Aviationexam.GeneratedJsonConverters.SourceGenerator.Target; [JsonSerializable(typeof(LeafContractWithCustomDelimiter))] [JsonSerializable(typeof(EBackingEnum))] [JsonSerializable(typeof(EConfiguredPropertyEnum))] +[JsonSerializable(typeof(EDuplicatedValueUsingBackingTypeEnum))] +[JsonSerializable(typeof(EDuplicatedValueUsingFirstEnumNameEnum))] [JsonSerializable(typeof(EMyEnum))] [JsonSerializable(typeof(EPropertyEnum))] [JsonSerializable(typeof(EPropertyWithBackingEnum))] diff --git a/src/Aviationexam.GeneratedJsonConverters.SourceGenerator.Tests/EnumJsonConverterIncrementalGeneratorSnapshotTests.EnumWithDuplicatedFieldWorks_BackingType#Attributes.DisableEnumJsonConverterAttribute.g.verified.cs b/src/Aviationexam.GeneratedJsonConverters.SourceGenerator.Tests/EnumJsonConverterIncrementalGeneratorSnapshotTests.EnumWithDuplicatedFieldWorks_BackingType#Attributes.DisableEnumJsonConverterAttribute.g.verified.cs new file mode 100644 index 0000000..ca5374d --- /dev/null +++ b/src/Aviationexam.GeneratedJsonConverters.SourceGenerator.Tests/EnumJsonConverterIncrementalGeneratorSnapshotTests.EnumWithDuplicatedFieldWorks_BackingType#Attributes.DisableEnumJsonConverterAttribute.g.verified.cs @@ -0,0 +1,11 @@ +//HintName: Attributes.DisableEnumJsonConverterAttribute.g.cs +// ReSharper disable once CheckNamespace +namespace Aviationexam.GeneratedJsonConverters.Attributes; + +/// +/// When placed on an enum, indicates that generator should not report missing +/// +[System.AttributeUsage(System.AttributeTargets.Enum, AllowMultiple = false, Inherited = false)] +internal sealed class DisableEnumJsonConverterAttribute : System.Text.Json.Serialization.JsonAttribute +{ +} diff --git a/src/Aviationexam.GeneratedJsonConverters.SourceGenerator.Tests/EnumJsonConverterIncrementalGeneratorSnapshotTests.EnumWithDuplicatedFieldWorks_BackingType#EMyEnumEnumJsonConverter.g.verified.cs b/src/Aviationexam.GeneratedJsonConverters.SourceGenerator.Tests/EnumJsonConverterIncrementalGeneratorSnapshotTests.EnumWithDuplicatedFieldWorks_BackingType#EMyEnumEnumJsonConverter.g.verified.cs new file mode 100644 index 0000000..0f8fd3d --- /dev/null +++ b/src/Aviationexam.GeneratedJsonConverters.SourceGenerator.Tests/EnumJsonConverterIncrementalGeneratorSnapshotTests.EnumWithDuplicatedFieldWorks_BackingType#EMyEnumEnumJsonConverter.g.verified.cs @@ -0,0 +1,57 @@ +//HintName: EMyEnumEnumJsonConverter.g.cs +#nullable enable + +namespace ApplicationNamespace.Contracts; + +internal class EMyEnumEnumJsonConverter : Aviationexam.GeneratedJsonConverters.EnumJsonConvertor +{ + protected override System.TypeCode BackingTypeTypeCode => System.TypeCode.Int32; + + protected override Aviationexam.GeneratedJsonConverters.EnumDeserializationStrategy DeserializationStrategy => Aviationexam.GeneratedJsonConverters.EnumDeserializationStrategy.UseBackingType | Aviationexam.GeneratedJsonConverters.EnumDeserializationStrategy.UseEnumName; + + protected override Aviationexam.GeneratedJsonConverters.EnumSerializationStrategy SerializationStrategy => Aviationexam.GeneratedJsonConverters.EnumSerializationStrategy.BackingType; + + protected override ApplicationNamespace.Contracts.EMyEnum ToEnum( + System.ReadOnlySpan enumName + ) + { + if (System.MemoryExtensions.SequenceEqual(enumName, "C"u8)) + { + return ApplicationNamespace.Contracts.EMyEnum.A; + } + if (System.MemoryExtensions.SequenceEqual(enumName, "D"u8)) + { + return ApplicationNamespace.Contracts.EMyEnum.B; + } + if (System.MemoryExtensions.SequenceEqual(enumName, "E"u8)) + { + return ApplicationNamespace.Contracts.EMyEnum.C; + } + + var stringValue = System.Text.Encoding.UTF8.GetString(enumName.ToArray()); + + throw new System.Text.Json.JsonException($"Undefined mapping of '{stringValue}' to enum 'ApplicationNamespace.Contracts.EMyEnum'"); + } + + protected override ApplicationNamespace.Contracts.EMyEnum ToEnum( + System.Int32 numericValue + ) => numericValue switch + { + 0 => ApplicationNamespace.Contracts.EMyEnum.A, + 1 => ApplicationNamespace.Contracts.EMyEnum.B, + _ => throw new System.Text.Json.JsonException($"Undefined mapping of '{numericValue}' to enum 'ApplicationNamespace.Contracts.EMyEnum'"), + }; + + protected override System.Int32 ToBackingType( + ApplicationNamespace.Contracts.EMyEnum value + ) => value switch + { + ApplicationNamespace.Contracts.EMyEnum.A => 0, + ApplicationNamespace.Contracts.EMyEnum.B => 1, + _ => throw new System.Text.Json.JsonException($"Undefined mapping of '{value}' from enum 'ApplicationNamespace.Contracts.EMyEnum'"), + }; + + protected override System.ReadOnlySpan ToFirstEnumName( + ApplicationNamespace.Contracts.EMyEnum value + ) => throw new System.Text.Json.JsonException("Enum is not configured to support serialization to enum type"); +} diff --git a/src/Aviationexam.GeneratedJsonConverters.SourceGenerator.Tests/EnumJsonConverterIncrementalGeneratorSnapshotTests.EnumWithDuplicatedFieldWorks_BackingType#EnumDeserializationStrategy.g.verified.cs b/src/Aviationexam.GeneratedJsonConverters.SourceGenerator.Tests/EnumJsonConverterIncrementalGeneratorSnapshotTests.EnumWithDuplicatedFieldWorks_BackingType#EnumDeserializationStrategy.g.verified.cs new file mode 100644 index 0000000..4416069 --- /dev/null +++ b/src/Aviationexam.GeneratedJsonConverters.SourceGenerator.Tests/EnumJsonConverterIncrementalGeneratorSnapshotTests.EnumWithDuplicatedFieldWorks_BackingType#EnumDeserializationStrategy.g.verified.cs @@ -0,0 +1,18 @@ +//HintName: EnumDeserializationStrategy.g.cs +// ReSharper disable once RedundantNullableDirective + +#nullable enable + +using Aviationexam.GeneratedJsonConverters.Attributes; +using System; + +namespace Aviationexam.GeneratedJsonConverters; + +[Flags] +[DisableEnumJsonConverter] +internal enum EnumDeserializationStrategy : byte +{ + ProjectDefault = 0, + UseBackingType = 1 << 0, + UseEnumName = 1 << 1, +} diff --git a/src/Aviationexam.GeneratedJsonConverters.SourceGenerator.Tests/EnumJsonConverterIncrementalGeneratorSnapshotTests.EnumWithDuplicatedFieldWorks_BackingType#EnumJsonConverterAttribute.g.verified.cs b/src/Aviationexam.GeneratedJsonConverters.SourceGenerator.Tests/EnumJsonConverterIncrementalGeneratorSnapshotTests.EnumWithDuplicatedFieldWorks_BackingType#EnumJsonConverterAttribute.g.verified.cs new file mode 100644 index 0000000..eebdd39 --- /dev/null +++ b/src/Aviationexam.GeneratedJsonConverters.SourceGenerator.Tests/EnumJsonConverterIncrementalGeneratorSnapshotTests.EnumWithDuplicatedFieldWorks_BackingType#EnumJsonConverterAttribute.g.verified.cs @@ -0,0 +1,21 @@ +//HintName: EnumJsonConverterAttribute.g.cs +#nullable enable + +namespace Aviationexam.GeneratedJsonConverters.Attributes; + +/// +/// When placed on an enum, indicates that the type should be serialized using generated enum convertor. +/// +[System.AttributeUsage(System.AttributeTargets.Enum, AllowMultiple = false, Inherited = false)] +internal sealed class EnumJsonConverterAttribute : System.Text.Json.Serialization.JsonAttribute +{ + /// + /// Configure serialization strategy + /// + public EnumSerializationStrategy SerializationStrategy { get; set; } = EnumSerializationStrategy.ProjectDefault; + + /// + /// Configure deserialization strategy + /// + public EnumDeserializationStrategy DeserializationStrategy { get; set; } = EnumDeserializationStrategy.ProjectDefault; +} diff --git a/src/Aviationexam.GeneratedJsonConverters.SourceGenerator.Tests/EnumJsonConverterIncrementalGeneratorSnapshotTests.EnumWithDuplicatedFieldWorks_BackingType#EnumJsonConvertor.g.verified.cs b/src/Aviationexam.GeneratedJsonConverters.SourceGenerator.Tests/EnumJsonConverterIncrementalGeneratorSnapshotTests.EnumWithDuplicatedFieldWorks_BackingType#EnumJsonConvertor.g.verified.cs new file mode 100644 index 0000000..1fc6eaa --- /dev/null +++ b/src/Aviationexam.GeneratedJsonConverters.SourceGenerator.Tests/EnumJsonConverterIncrementalGeneratorSnapshotTests.EnumWithDuplicatedFieldWorks_BackingType#EnumJsonConvertor.g.verified.cs @@ -0,0 +1,142 @@ +//HintName: EnumJsonConvertor.g.cs +// ReSharper disable once RedundantNullableDirective + +#nullable enable + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Aviationexam.GeneratedJsonConverters; + +internal abstract class EnumJsonConvertor : JsonConverter + where T : struct, Enum + where TBackingType : struct +{ + protected abstract TypeCode BackingTypeTypeCode { get; } + + protected abstract EnumDeserializationStrategy DeserializationStrategy { get; } + + protected abstract EnumSerializationStrategy SerializationStrategy { get; } + + protected abstract T ToEnum(ReadOnlySpan enumName); + + protected abstract T ToEnum(TBackingType numericValue); + + protected abstract TBackingType ToBackingType(T value); + + protected abstract ReadOnlySpan ToFirstEnumName(T value); + + public override T Read( + ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options + ) + { + if ( + reader.TokenType is JsonTokenType.String + && DeserializationStrategy.HasFlag(EnumDeserializationStrategy.UseEnumName) + ) + { + var enumName = reader.ValueSpan; + + return ToEnum(enumName); + } + + if (reader.TokenType is JsonTokenType.Number) + { + var numericValue = ReadAsNumber(ref reader); + + if (numericValue.HasValue) + { + return ToEnum(numericValue.Value); + } + } + + var value = Encoding.UTF8.GetString(reader.ValueSpan.ToArray()); + + 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(ref numericValue) : null, + TypeCode.Byte => reader.GetByte() is var numericValue ? Unsafe.As(ref numericValue) : null, + TypeCode.Int16 => reader.GetInt16() is var numericValue ? Unsafe.As(ref numericValue) : null, + TypeCode.UInt16 => reader.GetUInt16() is var numericValue ? Unsafe.As(ref numericValue) : null, + TypeCode.Int32 => reader.GetInt32() is var numericValue ? Unsafe.As(ref numericValue) : null, + TypeCode.UInt32 => reader.GetUInt32() is var numericValue ? Unsafe.As(ref numericValue) : null, + TypeCode.Int64 => reader.GetInt64() is var numericValue ? Unsafe.As(ref numericValue) : null, + TypeCode.UInt64 => reader.GetUInt64() is var numericValue ? Unsafe.As(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) + { + WriteAsBackingType(writer, value, options); + } + else if (SerializationStrategy is EnumSerializationStrategy.FirstEnumName) + { + WriteAsFirstEnumName(writer, value, options); + } + else + { + throw new ArgumentOutOfRangeException(nameof(SerializationStrategy), SerializationStrategy, "Unknown serialization strategy"); + } + } + + private void WriteAsBackingType( + Utf8JsonWriter writer, + T value, + [SuppressMessage("ReSharper", "UnusedParameter.Local")] + JsonSerializerOptions options + ) + { + var numericValue = ToBackingType(value); + + switch (BackingTypeTypeCode) + { + case TypeCode.SByte: + writer.WriteNumberValue(Unsafe.As(ref numericValue)); + break; + case TypeCode.Byte: + writer.WriteNumberValue(Unsafe.As(ref numericValue)); + break; + case TypeCode.Int16: + writer.WriteNumberValue(Unsafe.As(ref numericValue)); + break; + case TypeCode.UInt16: + writer.WriteNumberValue(Unsafe.As(ref numericValue)); + break; + case TypeCode.Int32: + writer.WriteNumberValue(Unsafe.As(ref numericValue)); + break; + case TypeCode.UInt32: + writer.WriteNumberValue(Unsafe.As(ref numericValue)); + break; + case TypeCode.Int64: + writer.WriteNumberValue(Unsafe.As(ref numericValue)); + break; + case TypeCode.UInt64: + writer.WriteNumberValue(Unsafe.As(ref numericValue)); + break; + default: + throw new ArgumentOutOfRangeException(nameof(BackingTypeTypeCode), BackingTypeTypeCode, $"Unexpected TypeCode {BackingTypeTypeCode}"); + } + } + + private void WriteAsFirstEnumName( + Utf8JsonWriter writer, + T value, + [SuppressMessage("ReSharper", "UnusedParameter.Local")] + JsonSerializerOptions options + ) + { + var enumValue = ToFirstEnumName(value); + + writer.WriteStringValue(enumValue); + } +} diff --git a/src/Aviationexam.GeneratedJsonConverters.SourceGenerator.Tests/EnumJsonConverterIncrementalGeneratorSnapshotTests.EnumWithDuplicatedFieldWorks_BackingType#EnumSerializationStrategy.g.verified.cs b/src/Aviationexam.GeneratedJsonConverters.SourceGenerator.Tests/EnumJsonConverterIncrementalGeneratorSnapshotTests.EnumWithDuplicatedFieldWorks_BackingType#EnumSerializationStrategy.g.verified.cs new file mode 100644 index 0000000..1e3e7f8 --- /dev/null +++ b/src/Aviationexam.GeneratedJsonConverters.SourceGenerator.Tests/EnumJsonConverterIncrementalGeneratorSnapshotTests.EnumWithDuplicatedFieldWorks_BackingType#EnumSerializationStrategy.g.verified.cs @@ -0,0 +1,16 @@ +//HintName: EnumSerializationStrategy.g.cs +// ReSharper disable once RedundantNullableDirective + +#nullable enable + +using Aviationexam.GeneratedJsonConverters.Attributes; + +namespace Aviationexam.GeneratedJsonConverters; + +[DisableEnumJsonConverter] +internal enum EnumSerializationStrategy : byte +{ + ProjectDefault, + BackingType, + FirstEnumName, +} diff --git a/src/Aviationexam.GeneratedJsonConverters.SourceGenerator.Tests/EnumJsonConverterIncrementalGeneratorSnapshotTests.EnumWithDuplicatedFieldWorks_FirstEnumName#Attributes.DisableEnumJsonConverterAttribute.g.verified.cs b/src/Aviationexam.GeneratedJsonConverters.SourceGenerator.Tests/EnumJsonConverterIncrementalGeneratorSnapshotTests.EnumWithDuplicatedFieldWorks_FirstEnumName#Attributes.DisableEnumJsonConverterAttribute.g.verified.cs new file mode 100644 index 0000000..ca5374d --- /dev/null +++ b/src/Aviationexam.GeneratedJsonConverters.SourceGenerator.Tests/EnumJsonConverterIncrementalGeneratorSnapshotTests.EnumWithDuplicatedFieldWorks_FirstEnumName#Attributes.DisableEnumJsonConverterAttribute.g.verified.cs @@ -0,0 +1,11 @@ +//HintName: Attributes.DisableEnumJsonConverterAttribute.g.cs +// ReSharper disable once CheckNamespace +namespace Aviationexam.GeneratedJsonConverters.Attributes; + +/// +/// When placed on an enum, indicates that generator should not report missing +/// +[System.AttributeUsage(System.AttributeTargets.Enum, AllowMultiple = false, Inherited = false)] +internal sealed class DisableEnumJsonConverterAttribute : System.Text.Json.Serialization.JsonAttribute +{ +} diff --git a/src/Aviationexam.GeneratedJsonConverters.SourceGenerator.Tests/EnumJsonConverterIncrementalGeneratorSnapshotTests.EnumWithDuplicatedFieldWorks_FirstEnumName#EMyEnumEnumJsonConverter.g.verified.cs b/src/Aviationexam.GeneratedJsonConverters.SourceGenerator.Tests/EnumJsonConverterIncrementalGeneratorSnapshotTests.EnumWithDuplicatedFieldWorks_FirstEnumName#EMyEnumEnumJsonConverter.g.verified.cs new file mode 100644 index 0000000..0f8fd3d --- /dev/null +++ b/src/Aviationexam.GeneratedJsonConverters.SourceGenerator.Tests/EnumJsonConverterIncrementalGeneratorSnapshotTests.EnumWithDuplicatedFieldWorks_FirstEnumName#EMyEnumEnumJsonConverter.g.verified.cs @@ -0,0 +1,57 @@ +//HintName: EMyEnumEnumJsonConverter.g.cs +#nullable enable + +namespace ApplicationNamespace.Contracts; + +internal class EMyEnumEnumJsonConverter : Aviationexam.GeneratedJsonConverters.EnumJsonConvertor +{ + protected override System.TypeCode BackingTypeTypeCode => System.TypeCode.Int32; + + protected override Aviationexam.GeneratedJsonConverters.EnumDeserializationStrategy DeserializationStrategy => Aviationexam.GeneratedJsonConverters.EnumDeserializationStrategy.UseBackingType | Aviationexam.GeneratedJsonConverters.EnumDeserializationStrategy.UseEnumName; + + protected override Aviationexam.GeneratedJsonConverters.EnumSerializationStrategy SerializationStrategy => Aviationexam.GeneratedJsonConverters.EnumSerializationStrategy.BackingType; + + protected override ApplicationNamespace.Contracts.EMyEnum ToEnum( + System.ReadOnlySpan enumName + ) + { + if (System.MemoryExtensions.SequenceEqual(enumName, "C"u8)) + { + return ApplicationNamespace.Contracts.EMyEnum.A; + } + if (System.MemoryExtensions.SequenceEqual(enumName, "D"u8)) + { + return ApplicationNamespace.Contracts.EMyEnum.B; + } + if (System.MemoryExtensions.SequenceEqual(enumName, "E"u8)) + { + return ApplicationNamespace.Contracts.EMyEnum.C; + } + + var stringValue = System.Text.Encoding.UTF8.GetString(enumName.ToArray()); + + throw new System.Text.Json.JsonException($"Undefined mapping of '{stringValue}' to enum 'ApplicationNamespace.Contracts.EMyEnum'"); + } + + protected override ApplicationNamespace.Contracts.EMyEnum ToEnum( + System.Int32 numericValue + ) => numericValue switch + { + 0 => ApplicationNamespace.Contracts.EMyEnum.A, + 1 => ApplicationNamespace.Contracts.EMyEnum.B, + _ => throw new System.Text.Json.JsonException($"Undefined mapping of '{numericValue}' to enum 'ApplicationNamespace.Contracts.EMyEnum'"), + }; + + protected override System.Int32 ToBackingType( + ApplicationNamespace.Contracts.EMyEnum value + ) => value switch + { + ApplicationNamespace.Contracts.EMyEnum.A => 0, + ApplicationNamespace.Contracts.EMyEnum.B => 1, + _ => throw new System.Text.Json.JsonException($"Undefined mapping of '{value}' from enum 'ApplicationNamespace.Contracts.EMyEnum'"), + }; + + protected override System.ReadOnlySpan ToFirstEnumName( + ApplicationNamespace.Contracts.EMyEnum value + ) => throw new System.Text.Json.JsonException("Enum is not configured to support serialization to enum type"); +} diff --git a/src/Aviationexam.GeneratedJsonConverters.SourceGenerator.Tests/EnumJsonConverterIncrementalGeneratorSnapshotTests.EnumWithDuplicatedFieldWorks_FirstEnumName#EnumDeserializationStrategy.g.verified.cs b/src/Aviationexam.GeneratedJsonConverters.SourceGenerator.Tests/EnumJsonConverterIncrementalGeneratorSnapshotTests.EnumWithDuplicatedFieldWorks_FirstEnumName#EnumDeserializationStrategy.g.verified.cs new file mode 100644 index 0000000..4416069 --- /dev/null +++ b/src/Aviationexam.GeneratedJsonConverters.SourceGenerator.Tests/EnumJsonConverterIncrementalGeneratorSnapshotTests.EnumWithDuplicatedFieldWorks_FirstEnumName#EnumDeserializationStrategy.g.verified.cs @@ -0,0 +1,18 @@ +//HintName: EnumDeserializationStrategy.g.cs +// ReSharper disable once RedundantNullableDirective + +#nullable enable + +using Aviationexam.GeneratedJsonConverters.Attributes; +using System; + +namespace Aviationexam.GeneratedJsonConverters; + +[Flags] +[DisableEnumJsonConverter] +internal enum EnumDeserializationStrategy : byte +{ + ProjectDefault = 0, + UseBackingType = 1 << 0, + UseEnumName = 1 << 1, +} diff --git a/src/Aviationexam.GeneratedJsonConverters.SourceGenerator.Tests/EnumJsonConverterIncrementalGeneratorSnapshotTests.EnumWithDuplicatedFieldWorks_FirstEnumName#EnumJsonConverterAttribute.g.verified.cs b/src/Aviationexam.GeneratedJsonConverters.SourceGenerator.Tests/EnumJsonConverterIncrementalGeneratorSnapshotTests.EnumWithDuplicatedFieldWorks_FirstEnumName#EnumJsonConverterAttribute.g.verified.cs new file mode 100644 index 0000000..eebdd39 --- /dev/null +++ b/src/Aviationexam.GeneratedJsonConverters.SourceGenerator.Tests/EnumJsonConverterIncrementalGeneratorSnapshotTests.EnumWithDuplicatedFieldWorks_FirstEnumName#EnumJsonConverterAttribute.g.verified.cs @@ -0,0 +1,21 @@ +//HintName: EnumJsonConverterAttribute.g.cs +#nullable enable + +namespace Aviationexam.GeneratedJsonConverters.Attributes; + +/// +/// When placed on an enum, indicates that the type should be serialized using generated enum convertor. +/// +[System.AttributeUsage(System.AttributeTargets.Enum, AllowMultiple = false, Inherited = false)] +internal sealed class EnumJsonConverterAttribute : System.Text.Json.Serialization.JsonAttribute +{ + /// + /// Configure serialization strategy + /// + public EnumSerializationStrategy SerializationStrategy { get; set; } = EnumSerializationStrategy.ProjectDefault; + + /// + /// Configure deserialization strategy + /// + public EnumDeserializationStrategy DeserializationStrategy { get; set; } = EnumDeserializationStrategy.ProjectDefault; +} diff --git a/src/Aviationexam.GeneratedJsonConverters.SourceGenerator.Tests/EnumJsonConverterIncrementalGeneratorSnapshotTests.EnumWithDuplicatedFieldWorks_FirstEnumName#EnumJsonConvertor.g.verified.cs b/src/Aviationexam.GeneratedJsonConverters.SourceGenerator.Tests/EnumJsonConverterIncrementalGeneratorSnapshotTests.EnumWithDuplicatedFieldWorks_FirstEnumName#EnumJsonConvertor.g.verified.cs new file mode 100644 index 0000000..1fc6eaa --- /dev/null +++ b/src/Aviationexam.GeneratedJsonConverters.SourceGenerator.Tests/EnumJsonConverterIncrementalGeneratorSnapshotTests.EnumWithDuplicatedFieldWorks_FirstEnumName#EnumJsonConvertor.g.verified.cs @@ -0,0 +1,142 @@ +//HintName: EnumJsonConvertor.g.cs +// ReSharper disable once RedundantNullableDirective + +#nullable enable + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Aviationexam.GeneratedJsonConverters; + +internal abstract class EnumJsonConvertor : JsonConverter + where T : struct, Enum + where TBackingType : struct +{ + protected abstract TypeCode BackingTypeTypeCode { get; } + + protected abstract EnumDeserializationStrategy DeserializationStrategy { get; } + + protected abstract EnumSerializationStrategy SerializationStrategy { get; } + + protected abstract T ToEnum(ReadOnlySpan enumName); + + protected abstract T ToEnum(TBackingType numericValue); + + protected abstract TBackingType ToBackingType(T value); + + protected abstract ReadOnlySpan ToFirstEnumName(T value); + + public override T Read( + ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options + ) + { + if ( + reader.TokenType is JsonTokenType.String + && DeserializationStrategy.HasFlag(EnumDeserializationStrategy.UseEnumName) + ) + { + var enumName = reader.ValueSpan; + + return ToEnum(enumName); + } + + if (reader.TokenType is JsonTokenType.Number) + { + var numericValue = ReadAsNumber(ref reader); + + if (numericValue.HasValue) + { + return ToEnum(numericValue.Value); + } + } + + var value = Encoding.UTF8.GetString(reader.ValueSpan.ToArray()); + + 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(ref numericValue) : null, + TypeCode.Byte => reader.GetByte() is var numericValue ? Unsafe.As(ref numericValue) : null, + TypeCode.Int16 => reader.GetInt16() is var numericValue ? Unsafe.As(ref numericValue) : null, + TypeCode.UInt16 => reader.GetUInt16() is var numericValue ? Unsafe.As(ref numericValue) : null, + TypeCode.Int32 => reader.GetInt32() is var numericValue ? Unsafe.As(ref numericValue) : null, + TypeCode.UInt32 => reader.GetUInt32() is var numericValue ? Unsafe.As(ref numericValue) : null, + TypeCode.Int64 => reader.GetInt64() is var numericValue ? Unsafe.As(ref numericValue) : null, + TypeCode.UInt64 => reader.GetUInt64() is var numericValue ? Unsafe.As(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) + { + WriteAsBackingType(writer, value, options); + } + else if (SerializationStrategy is EnumSerializationStrategy.FirstEnumName) + { + WriteAsFirstEnumName(writer, value, options); + } + else + { + throw new ArgumentOutOfRangeException(nameof(SerializationStrategy), SerializationStrategy, "Unknown serialization strategy"); + } + } + + private void WriteAsBackingType( + Utf8JsonWriter writer, + T value, + [SuppressMessage("ReSharper", "UnusedParameter.Local")] + JsonSerializerOptions options + ) + { + var numericValue = ToBackingType(value); + + switch (BackingTypeTypeCode) + { + case TypeCode.SByte: + writer.WriteNumberValue(Unsafe.As(ref numericValue)); + break; + case TypeCode.Byte: + writer.WriteNumberValue(Unsafe.As(ref numericValue)); + break; + case TypeCode.Int16: + writer.WriteNumberValue(Unsafe.As(ref numericValue)); + break; + case TypeCode.UInt16: + writer.WriteNumberValue(Unsafe.As(ref numericValue)); + break; + case TypeCode.Int32: + writer.WriteNumberValue(Unsafe.As(ref numericValue)); + break; + case TypeCode.UInt32: + writer.WriteNumberValue(Unsafe.As(ref numericValue)); + break; + case TypeCode.Int64: + writer.WriteNumberValue(Unsafe.As(ref numericValue)); + break; + case TypeCode.UInt64: + writer.WriteNumberValue(Unsafe.As(ref numericValue)); + break; + default: + throw new ArgumentOutOfRangeException(nameof(BackingTypeTypeCode), BackingTypeTypeCode, $"Unexpected TypeCode {BackingTypeTypeCode}"); + } + } + + private void WriteAsFirstEnumName( + Utf8JsonWriter writer, + T value, + [SuppressMessage("ReSharper", "UnusedParameter.Local")] + JsonSerializerOptions options + ) + { + var enumValue = ToFirstEnumName(value); + + writer.WriteStringValue(enumValue); + } +} diff --git a/src/Aviationexam.GeneratedJsonConverters.SourceGenerator.Tests/EnumJsonConverterIncrementalGeneratorSnapshotTests.EnumWithDuplicatedFieldWorks_FirstEnumName#EnumSerializationStrategy.g.verified.cs b/src/Aviationexam.GeneratedJsonConverters.SourceGenerator.Tests/EnumJsonConverterIncrementalGeneratorSnapshotTests.EnumWithDuplicatedFieldWorks_FirstEnumName#EnumSerializationStrategy.g.verified.cs new file mode 100644 index 0000000..1e3e7f8 --- /dev/null +++ b/src/Aviationexam.GeneratedJsonConverters.SourceGenerator.Tests/EnumJsonConverterIncrementalGeneratorSnapshotTests.EnumWithDuplicatedFieldWorks_FirstEnumName#EnumSerializationStrategy.g.verified.cs @@ -0,0 +1,16 @@ +//HintName: EnumSerializationStrategy.g.cs +// ReSharper disable once RedundantNullableDirective + +#nullable enable + +using Aviationexam.GeneratedJsonConverters.Attributes; + +namespace Aviationexam.GeneratedJsonConverters; + +[DisableEnumJsonConverter] +internal enum EnumSerializationStrategy : byte +{ + ProjectDefault, + BackingType, + FirstEnumName, +} diff --git a/src/Aviationexam.GeneratedJsonConverters.SourceGenerator.Tests/EnumJsonConverterIncrementalGeneratorSnapshotTests.cs b/src/Aviationexam.GeneratedJsonConverters.SourceGenerator.Tests/EnumJsonConverterIncrementalGeneratorSnapshotTests.cs index 5b845b6..a4a6610 100644 --- a/src/Aviationexam.GeneratedJsonConverters.SourceGenerator.Tests/EnumJsonConverterIncrementalGeneratorSnapshotTests.cs +++ b/src/Aviationexam.GeneratedJsonConverters.SourceGenerator.Tests/EnumJsonConverterIncrementalGeneratorSnapshotTests.cs @@ -146,6 +146,62 @@ public partial class MyJsonSerializerContext : JsonSerializerContext """ ); + [Fact] + public Task EnumWithDuplicatedFieldWorks_FirstEnumName() => TestHelper.Verify( + new DictionaryAnalyzerConfigOptionsProvider(globalOptions: new Dictionary + { + ["build_property.AVI_EJC_DefaultEnumSerializationStrategy"] = "BackingType", + ["build_property.AVI_EJC_DefaultEnumDeserializationStrategy"] = "UseBackingType|UseEnumName", + }), + // ReSharper disable once HeapView.ObjectAllocation + """ + using Aviationexam.GeneratedJsonConverters; + using Aviationexam.GeneratedJsonConverters.Attributes; + using System.Runtime.Serialization; + + namespace ApplicationNamespace.Contracts; + + [EnumJsonConverter] + public enum EMyEnum + { + [EnumMember(Value = "C")] + A = 0, + [EnumMember(Value = "D")] + B = 1, + [EnumMember(Value = "E")] + C = 1, + } + """ + ); + + [Fact] + public Task EnumWithDuplicatedFieldWorks_BackingType() => TestHelper.Verify( + new DictionaryAnalyzerConfigOptionsProvider(globalOptions: new Dictionary + { + ["build_property.AVI_EJC_DefaultEnumSerializationStrategy"] = "BackingType", + ["build_property.AVI_EJC_DefaultEnumDeserializationStrategy"] = "UseBackingType|UseEnumName", + }), + // ReSharper disable once HeapView.ObjectAllocation + """ + using Aviationexam.GeneratedJsonConverters; + using Aviationexam.GeneratedJsonConverters.Attributes; + using System.Runtime.Serialization; + + namespace ApplicationNamespace.Contracts; + + [EnumJsonConverter] + public enum EMyEnum + { + [EnumMember(Value = "C")] + A = 0, + [EnumMember(Value = "D")] + B = 1, + [EnumMember(Value = "E")] + C = 1, + } + """ + ); + [Fact] public Task ProjectConfigurationWorks_UseEnumName() => TestHelper.Verify( new DictionaryAnalyzerConfigOptionsProvider(globalOptions: new Dictionary diff --git a/src/Aviationexam.GeneratedJsonConverters.SourceGenerator/Generators/EnumJsonConverterGenerator.cs b/src/Aviationexam.GeneratedJsonConverters.SourceGenerator/Generators/EnumJsonConverterGenerator.cs index 98e9b99..967934e 100644 --- a/src/Aviationexam.GeneratedJsonConverters.SourceGenerator/Generators/EnumJsonConverterGenerator.cs +++ b/src/Aviationexam.GeneratedJsonConverters.SourceGenerator/Generators/EnumJsonConverterGenerator.cs @@ -44,16 +44,6 @@ out string converterName var constantValue = typeMember.ConstantValue ?? throw new NullReferenceException(nameof(typeMember.ConstantValue)); backingType = constantValue.GetType(); - if (!backingTypeDeserialization.ContainsKey(constantValue)) - { - backingTypeDeserialization.Add(constantValue, typeMember.Name); - } - - if (!backingTypeSerialization.ContainsKey(typeMember.Name)) - { - backingTypeSerialization.Add(typeMember.Name, constantValue); - } - var fieldName = typeMember.Name; var enumMember = typeMember.GetAttributes().Where( x => SymbolEqualityComparer.Default.Equals(x.AttributeClass, enumMemberAttributeSymbol) @@ -71,10 +61,27 @@ out string converterName fieldNameDeserialization.Add(fieldName, typeMember.Name); } - if (!fieldNameSerialization.ContainsKey(typeMember.Name)) + if ( + !fieldNameSerialization.ContainsKey(typeMember.Name) + && !backingTypeDeserialization.ContainsKey(constantValue) + ) { fieldNameSerialization.Add(typeMember.Name, fieldName); } + + + if ( + !backingTypeSerialization.ContainsKey(typeMember.Name) + && !backingTypeDeserialization.ContainsKey(constantValue) + ) + { + backingTypeSerialization.Add(typeMember.Name, constantValue); + } + + if (!backingTypeDeserialization.ContainsKey(constantValue)) + { + backingTypeDeserialization.Add(constantValue, typeMember.Name); + } } if (backingType is null)