From 11f9fa6668e4df054130e64395fa22c030e8ceff Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Thu, 25 Jan 2024 20:56:35 -0800 Subject: [PATCH] Implement a better union example and allow SerdeTypeOptions on enum (#149) Provides an example on how to write 'externally tagged unions' in serde.net and allows placing SerdeTypeOptions on enum declarations. --- samples/unions/Program.cs | 171 +++++++----------- samples/unions/Unions.csproj | 7 + src/serde/Attributes.cs | 2 +- src/serde/ISerialize.cs | 4 +- .../MemberFormatTests.cs | 18 ++ .../ColorEnumWrap.IDeserialize.verified.cs | 32 ++++ .../ColorEnumWrap.ISerialize.verified.cs | 20 ++ .../ColorEnumWrap.ISerializeWrap.verified.cs | 6 + .../ColorEnumWrap.ISerialize`1.verified.cs | 20 ++ .../ColorEnumWrap.verified.cs | 3 + 10 files changed, 176 insertions(+), 107 deletions(-) create mode 100644 test/Serde.Generation.Test/test_output/MemberFormatTests.EnumFormat/ColorEnumWrap.IDeserialize.verified.cs create mode 100644 test/Serde.Generation.Test/test_output/MemberFormatTests.EnumFormat/ColorEnumWrap.ISerialize.verified.cs create mode 100644 test/Serde.Generation.Test/test_output/MemberFormatTests.EnumFormat/ColorEnumWrap.ISerializeWrap.verified.cs create mode 100644 test/Serde.Generation.Test/test_output/MemberFormatTests.EnumFormat/ColorEnumWrap.ISerialize`1.verified.cs create mode 100644 test/Serde.Generation.Test/test_output/MemberFormatTests.EnumFormat/ColorEnumWrap.verified.cs diff --git a/samples/unions/Program.cs b/samples/unions/Program.cs index 45041197..560d5700 100644 --- a/samples/unions/Program.cs +++ b/samples/unions/Program.cs @@ -1,129 +1,90 @@ -using System.Runtime.Serialization; +using System.Diagnostics; using Serde; using Serde.Json; +using StaticCs; -var json = """ -{ "bar": { "_t": "Bar1", "bar": 1 } } -"""; -var bar = JsonSerializer.Deserialize(json); -Console.WriteLine(bar); +var a = new BaseType.DerivedA { A = 1 }; +var b = new BaseType.DerivedB { B = "foo" }; +var aSerialized = JsonSerializer.Serialize(a); +var bSerialized = JsonSerializer.Serialize(b); -[GenerateDeserialize] -partial class Foo { - public required AbstractBar bar; -} - -[GenerateDeserialize] -partial class Bar1 : AbstractBar { - public int bar; -} +Console.WriteLine($"a: {aSerialized}, " + (aSerialized == """{"DerivedA":{"a":1}}""")); +Console.WriteLine($"b: {bSerialized}, " + (bSerialized == """{"DerivedB":{"b":"foo"}}""")); -[GenerateSerde] -partial class Bar2 : AbstractBar { - public double bar; -} +Console.WriteLine("a: " + (JsonSerializer.Deserialize(aSerialized) == a)); +Console.WriteLine("b: " + (JsonSerializer.Deserialize(bSerialized) == b)); -abstract partial class AbstractBar : IDeserialize +[Closed] +abstract partial record BaseType { - static AbstractBar IDeserialize.Deserialize(ref D deserializer) + private BaseType() { } + + public sealed partial record DerivedA : BaseType { - var visitor = new SerdeVisitor(deserializer); - var fieldNames = new[] - { - "bar" - }; - return deserializer.DeserializeType("AbstractBar", fieldNames, visitor); + public required int A { get; init; } } + public sealed partial record DerivedB : BaseType + { + public required string B { get; init; } + } +} - private sealed class SerdeVisitor : IDeserializeVisitor +partial record BaseType : ISerialize +{ + public void Serialize(BaseType value, ISerializer serializer) { - private readonly IDeserializer _deserializer; - public SerdeVisitor(IDeserializer deserializer) + var serializeType = serializer.SerializeType("BaseType", 2); + switch (value) { - _deserializer = deserializer; + case DerivedA derivedA: + serializeType.SerializeField(nameof(DerivedA), derivedA); + break; + case DerivedB derivedB: + serializeType.SerializeField(nameof(DerivedB), derivedB); + break; } + serializeType.End(); + } - public string ExpectedTypeName => "AbstractBar"; + [GenerateSerde(Through = nameof(Value))] + private readonly partial record struct DerivedAWrap(DerivedA Value); - AbstractBar IDeserializeVisitor.VisitDictionary(ref D d) - { - var result = d.TryGetNextKey(out string? key); - if (!result || key != "_t") - { - throw new InvalidDeserializeValueException("Expected a _t field"); - } - var value = d.GetNextValue(); - var inline = new InlineDeserializer(_deserializer, d); - switch (value) - { - case "Bar1": - var bar1 = InlineDeserialize(inline); - return bar1; - case "Bar2": - var bar2 = InlineDeserialize(inline); - return bar2; - default: - throw new InvalidDeserializeValueException($"Unexpected value {value}"); - } - } + [GenerateSerde(Through = nameof(Value))] + private readonly partial record struct DerivedBWrap(DerivedB Value); +} - private static T InlineDeserialize(IDeserializer deserializer) where T : IDeserialize - { - return T.Deserialize(ref deserializer); - } +partial record BaseType : IDeserialize +{ + public static BaseType Deserialize(ref D deserializer) where D : IDeserializer + { + return deserializer.DeserializeDictionary(new DeserializeVisitor()); } - private class InlineDeserializer : IDeserializer + [Closed] + [GenerateDeserialize] + [SerdeTypeOptions(MemberFormat = MemberFormat.None)] + private enum KeyNames { - private readonly IDeserializer _deserializer; - private IDeserializeDictionary _deserializeDictionary; + DerivedA, + DerivedB, + } - public InlineDeserializer(IDeserializer deserializer, IDeserializeDictionary deserializeDictionary) + private sealed class DeserializeVisitor : IDeserializeVisitor + { + public string ExpectedTypeName => nameof(BaseType); + + BaseType IDeserializeVisitor.VisitDictionary(ref D deserializer) { - _deserializer = deserializer; - _deserializeDictionary = deserializeDictionary; + deserializer.TryGetNextKey(out var type); + switch (type) + { + case KeyNames.DerivedA: + return deserializer.GetNextValue(); + case KeyNames.DerivedB: + return deserializer.GetNextValue(); + default: + throw new InvalidOperationException(); + } } - - public T DeserializeAny(V v) where V : IDeserializeVisitor => throw new NotImplementedException(); - - public T DeserializeBool(V v) where V : IDeserializeVisitor => throw new NotImplementedException(); - - public T DeserializeByte(V v) where V : IDeserializeVisitor => throw new NotImplementedException(); - - public T DeserializeChar(V v) where V : IDeserializeVisitor => throw new NotImplementedException(); - - public T DeserializeDecimal(V v) where V : IDeserializeVisitor => throw new NotImplementedException(); - - public T DeserializeDictionary(V v) where V : IDeserializeVisitor - => v.VisitDictionary(ref _deserializeDictionary); - - public T DeserializeDouble(V v) where V : IDeserializeVisitor => throw new NotImplementedException(); - - public T DeserializeEnumerable(V v) where V : IDeserializeVisitor => throw new NotImplementedException(); - - public T DeserializeFloat(V v) where V : IDeserializeVisitor => throw new NotImplementedException(); - - public T DeserializeI16(V v) where V : IDeserializeVisitor => throw new NotImplementedException(); - - public T DeserializeI32(V v) where V : IDeserializeVisitor => throw new NotImplementedException(); - - public T DeserializeI64(V v) where V : IDeserializeVisitor => throw new NotImplementedException(); - - public T DeserializeIdentifier(V v) where V : IDeserializeVisitor => throw new NotImplementedException(); - - public T DeserializeNullableRef(V v) where V : IDeserializeVisitor => throw new NotImplementedException(); - - public T DeserializeSByte(V v) where V : IDeserializeVisitor => throw new NotImplementedException(); - - public T DeserializeString(V v) where V : IDeserializeVisitor => throw new NotImplementedException(); - - public T DeserializeType(string typeName, ReadOnlySpan fieldNames, V v) where V : IDeserializeVisitor - => DeserializeDictionary(v); - - public T DeserializeU16(V v) where V : IDeserializeVisitor => throw new NotImplementedException(); - - public T DeserializeU32(V v) where V : IDeserializeVisitor => throw new NotImplementedException(); - - public T DeserializeU64(V v) where V : IDeserializeVisitor => throw new NotImplementedException(); } } \ No newline at end of file diff --git a/samples/unions/Unions.csproj b/samples/unions/Unions.csproj index 6240f9ae..a3f15738 100644 --- a/samples/unions/Unions.csproj +++ b/samples/unions/Unions.csproj @@ -11,4 +11,11 @@ + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + diff --git a/src/serde/Attributes.cs b/src/serde/Attributes.cs index 56ad0c8b..cf99b0ca 100644 --- a/src/serde/Attributes.cs +++ b/src/serde/Attributes.cs @@ -116,7 +116,7 @@ public SerdeWrapAttribute(Type wrapper) /// /// Set options for the Serde source generator for the current type. /// -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false, Inherited = false)] +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum, AllowMultiple = false, Inherited = false)] #if !SRCGEN public #else diff --git a/src/serde/ISerialize.cs b/src/serde/ISerialize.cs index 6153bfe3..59819fdc 100644 --- a/src/serde/ISerialize.cs +++ b/src/serde/ISerialize.cs @@ -8,9 +8,11 @@ public interface ISerialize void Serialize(ISerializer serializer); } -public interface ISerialize +public interface ISerialize : ISerialize { void Serialize(T value, ISerializer serializer); + + void ISerialize.Serialize(ISerializer serializer) => Serialize((T)this, serializer); } public interface ISerializeType diff --git a/test/Serde.Generation.Test/MemberFormatTests.cs b/test/Serde.Generation.Test/MemberFormatTests.cs index bc51d2f4..6e125ed9 100644 --- a/test/Serde.Generation.Test/MemberFormatTests.cs +++ b/test/Serde.Generation.Test/MemberFormatTests.cs @@ -108,5 +108,23 @@ partial struct S }"; return VerifyMultiFile(src); } + + [Fact] + public Task EnumFormat() + { + var src = """ + +using Serde; +[GenerateSerde] +[SerdeTypeOptions(MemberFormat = MemberFormat.None)] +public enum ColorEnum +{ + Red, + Green, + Blue +} +"""; + return VerifyMultiFile(src); + } } } \ No newline at end of file diff --git a/test/Serde.Generation.Test/test_output/MemberFormatTests.EnumFormat/ColorEnumWrap.IDeserialize.verified.cs b/test/Serde.Generation.Test/test_output/MemberFormatTests.EnumFormat/ColorEnumWrap.IDeserialize.verified.cs new file mode 100644 index 00000000..6fcfc576 --- /dev/null +++ b/test/Serde.Generation.Test/test_output/MemberFormatTests.EnumFormat/ColorEnumWrap.IDeserialize.verified.cs @@ -0,0 +1,32 @@ +//HintName: ColorEnumWrap.IDeserialize.cs + +#nullable enable +using System; +using Serde; + +partial record struct ColorEnumWrap : Serde.IDeserialize +{ + static ColorEnum Serde.IDeserialize.Deserialize(ref D deserializer) + { + var visitor = new SerdeVisitor(); + return deserializer.DeserializeString(visitor); + } + + private sealed class SerdeVisitor : Serde.IDeserializeVisitor + { + public string ExpectedTypeName => "ColorEnum"; + + ColorEnum Serde.IDeserializeVisitor.VisitString(string s) => s switch + { + "Red" => ColorEnum.Red, + "Green" => ColorEnum.Green, + "Blue" => ColorEnum.Blue, + _ => throw new InvalidDeserializeValueException("Unexpected enum field name: " + s)}; + ColorEnum Serde.IDeserializeVisitor.VisitUtf8Span(System.ReadOnlySpan s) => s switch + { + _ when System.MemoryExtensions.SequenceEqual(s, "Red"u8) => ColorEnum.Red, + _ when System.MemoryExtensions.SequenceEqual(s, "Green"u8) => ColorEnum.Green, + _ when System.MemoryExtensions.SequenceEqual(s, "Blue"u8) => ColorEnum.Blue, + _ => throw new InvalidDeserializeValueException("Unexpected enum field name: " + System.Text.Encoding.UTF8.GetString(s))}; + } +} \ No newline at end of file diff --git a/test/Serde.Generation.Test/test_output/MemberFormatTests.EnumFormat/ColorEnumWrap.ISerialize.verified.cs b/test/Serde.Generation.Test/test_output/MemberFormatTests.EnumFormat/ColorEnumWrap.ISerialize.verified.cs new file mode 100644 index 00000000..6f285c64 --- /dev/null +++ b/test/Serde.Generation.Test/test_output/MemberFormatTests.EnumFormat/ColorEnumWrap.ISerialize.verified.cs @@ -0,0 +1,20 @@ +//HintName: ColorEnumWrap.ISerialize.cs + +#nullable enable +using System; +using Serde; + +partial record struct ColorEnumWrap : Serde.ISerialize +{ + void Serde.ISerialize.Serialize(ISerializer serializer) + { + var name = Value switch + { + ColorEnum.Red => "Red", + ColorEnum.Green => "Green", + ColorEnum.Blue => "Blue", + _ => null + }; + serializer.SerializeEnumValue("ColorEnum", name, new Int32Wrap((int)Value)); + } +} \ No newline at end of file diff --git a/test/Serde.Generation.Test/test_output/MemberFormatTests.EnumFormat/ColorEnumWrap.ISerializeWrap.verified.cs b/test/Serde.Generation.Test/test_output/MemberFormatTests.EnumFormat/ColorEnumWrap.ISerializeWrap.verified.cs new file mode 100644 index 00000000..6b01f663 --- /dev/null +++ b/test/Serde.Generation.Test/test_output/MemberFormatTests.EnumFormat/ColorEnumWrap.ISerializeWrap.verified.cs @@ -0,0 +1,6 @@ +//HintName: ColorEnumWrap.ISerializeWrap.cs + +partial record struct ColorEnumWrap : Serde.ISerializeWrap +{ + static ColorEnumWrap Serde.ISerializeWrap.Create(ColorEnum value) => new(value); +} \ No newline at end of file diff --git a/test/Serde.Generation.Test/test_output/MemberFormatTests.EnumFormat/ColorEnumWrap.ISerialize`1.verified.cs b/test/Serde.Generation.Test/test_output/MemberFormatTests.EnumFormat/ColorEnumWrap.ISerialize`1.verified.cs new file mode 100644 index 00000000..87b75286 --- /dev/null +++ b/test/Serde.Generation.Test/test_output/MemberFormatTests.EnumFormat/ColorEnumWrap.ISerialize`1.verified.cs @@ -0,0 +1,20 @@ +//HintName: ColorEnumWrap.ISerialize`1.cs + +#nullable enable +using System; +using Serde; + +partial record struct ColorEnumWrap : Serde.ISerialize +{ + void ISerialize.Serialize(ColorEnum value, ISerializer serializer) + { + var name = value switch + { + ColorEnum.Red => "Red", + ColorEnum.Green => "Green", + ColorEnum.Blue => "Blue", + _ => null + }; + serializer.SerializeEnumValue("ColorEnum", name, new Int32Wrap((int)value)); + } +} \ No newline at end of file diff --git a/test/Serde.Generation.Test/test_output/MemberFormatTests.EnumFormat/ColorEnumWrap.verified.cs b/test/Serde.Generation.Test/test_output/MemberFormatTests.EnumFormat/ColorEnumWrap.verified.cs new file mode 100644 index 00000000..9d53fdad --- /dev/null +++ b/test/Serde.Generation.Test/test_output/MemberFormatTests.EnumFormat/ColorEnumWrap.verified.cs @@ -0,0 +1,3 @@ +//HintName: ColorEnumWrap.cs + +readonly partial record struct ColorEnumWrap(ColorEnum Value); \ No newline at end of file