Skip to content

Commit

Permalink
Implement a better union example and allow SerdeTypeOptions on enum (#…
Browse files Browse the repository at this point in the history
…149)

Provides an example on how to write 'externally tagged unions' in serde.net and allows
placing SerdeTypeOptions on enum declarations.
  • Loading branch information
agocke authored Jan 26, 2024
1 parent 060025a commit 11f9fa6
Show file tree
Hide file tree
Showing 10 changed files with 176 additions and 107 deletions.
171 changes: 66 additions & 105 deletions samples/unions/Program.cs
Original file line number Diff line number Diff line change
@@ -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<Foo>(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<BaseType>(aSerialized) == a));
Console.WriteLine("b: " + (JsonSerializer.Deserialize<BaseType>(bSerialized) == b));

abstract partial class AbstractBar : IDeserialize<AbstractBar>
[Closed]
abstract partial record BaseType
{
static AbstractBar IDeserialize<AbstractBar>.Deserialize<D>(ref D deserializer)
private BaseType() { }

public sealed partial record DerivedA : BaseType
{
var visitor = new SerdeVisitor(deserializer);
var fieldNames = new[]
{
"bar"
};
return deserializer.DeserializeType<AbstractBar, SerdeVisitor>("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<AbstractBar>
partial record BaseType : ISerialize<BaseType>
{
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<DerivedA, DerivedAWrap>(nameof(DerivedA), derivedA);
break;
case DerivedB derivedB:
serializeType.SerializeField<DerivedB, DerivedBWrap>(nameof(DerivedB), derivedB);
break;
}
serializeType.End();
}

public string ExpectedTypeName => "AbstractBar";
[GenerateSerde(Through = nameof(Value))]
private readonly partial record struct DerivedAWrap(DerivedA Value);

AbstractBar IDeserializeVisitor<AbstractBar>.VisitDictionary<D>(ref D d)
{
var result = d.TryGetNextKey<string, StringWrap>(out string? key);
if (!result || key != "_t")
{
throw new InvalidDeserializeValueException("Expected a _t field");
}
var value = d.GetNextValue<string, StringWrap>();
var inline = new InlineDeserializer(_deserializer, d);
switch (value)
{
case "Bar1":
var bar1 = InlineDeserialize<Bar1>(inline);
return bar1;
case "Bar2":
var bar2 = InlineDeserialize<Bar2>(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<T>(IDeserializer deserializer) where T : IDeserialize<T>
{
return T.Deserialize(ref deserializer);
}
partial record BaseType : IDeserialize<BaseType>
{
public static BaseType Deserialize<D>(ref D deserializer) where D : IDeserializer
{
return deserializer.DeserializeDictionary<BaseType, DeserializeVisitor>(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<BaseType>
{
public string ExpectedTypeName => nameof(BaseType);

BaseType IDeserializeVisitor<BaseType>.VisitDictionary<D>(ref D deserializer)
{
_deserializer = deserializer;
_deserializeDictionary = deserializeDictionary;
deserializer.TryGetNextKey<KeyNames, KeyNamesWrap>(out var type);
switch (type)
{
case KeyNames.DerivedA:
return deserializer.GetNextValue<DerivedA, DerivedAWrap>();
case KeyNames.DerivedB:
return deserializer.GetNextValue<DerivedB, DerivedBWrap>();
default:
throw new InvalidOperationException();
}
}

public T DeserializeAny<T, V>(V v) where V : IDeserializeVisitor<T> => throw new NotImplementedException();

public T DeserializeBool<T, V>(V v) where V : IDeserializeVisitor<T> => throw new NotImplementedException();

public T DeserializeByte<T, V>(V v) where V : IDeserializeVisitor<T> => throw new NotImplementedException();

public T DeserializeChar<T, V>(V v) where V : IDeserializeVisitor<T> => throw new NotImplementedException();

public T DeserializeDecimal<T, V>(V v) where V : IDeserializeVisitor<T> => throw new NotImplementedException();

public T DeserializeDictionary<T, V>(V v) where V : IDeserializeVisitor<T>
=> v.VisitDictionary(ref _deserializeDictionary);

public T DeserializeDouble<T, V>(V v) where V : IDeserializeVisitor<T> => throw new NotImplementedException();

public T DeserializeEnumerable<T, V>(V v) where V : IDeserializeVisitor<T> => throw new NotImplementedException();

public T DeserializeFloat<T, V>(V v) where V : IDeserializeVisitor<T> => throw new NotImplementedException();

public T DeserializeI16<T, V>(V v) where V : IDeserializeVisitor<T> => throw new NotImplementedException();

public T DeserializeI32<T, V>(V v) where V : IDeserializeVisitor<T> => throw new NotImplementedException();

public T DeserializeI64<T, V>(V v) where V : IDeserializeVisitor<T> => throw new NotImplementedException();

public T DeserializeIdentifier<T, V>(V v) where V : IDeserializeVisitor<T> => throw new NotImplementedException();

public T DeserializeNullableRef<T, V>(V v) where V : IDeserializeVisitor<T> => throw new NotImplementedException();

public T DeserializeSByte<T, V>(V v) where V : IDeserializeVisitor<T> => throw new NotImplementedException();

public T DeserializeString<T, V>(V v) where V : IDeserializeVisitor<T> => throw new NotImplementedException();

public T DeserializeType<T, V>(string typeName, ReadOnlySpan<string> fieldNames, V v) where V : IDeserializeVisitor<T>
=> DeserializeDictionary<T, V>(v);

public T DeserializeU16<T, V>(V v) where V : IDeserializeVisitor<T> => throw new NotImplementedException();

public T DeserializeU32<T, V>(V v) where V : IDeserializeVisitor<T> => throw new NotImplementedException();

public T DeserializeU64<T, V>(V v) where V : IDeserializeVisitor<T> => throw new NotImplementedException();
}
}
7 changes: 7 additions & 0 deletions samples/unions/Unions.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,11 @@
<ProjectReference Include="../../src/serde/Serde.csproj" />
<ProjectReference Include="../../src/generator/SerdeGenerator.csproj" OutputItemType="Analyzer" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="StaticCs" Version="0.3.1">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
</Project>
2 changes: 1 addition & 1 deletion src/serde/Attributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ public SerdeWrapAttribute(Type wrapper)
/// <summary>
/// Set options for the Serde source generator for the current type.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false, Inherited = false)]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum, AllowMultiple = false, Inherited = false)]
#if !SRCGEN
public
#else
Expand Down
4 changes: 3 additions & 1 deletion src/serde/ISerialize.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ public interface ISerialize
void Serialize(ISerializer serializer);
}

public interface ISerialize<in T>
public interface ISerialize<in T> : ISerialize
{
void Serialize(T value, ISerializer serializer);

void ISerialize.Serialize(ISerializer serializer) => Serialize((T)this, serializer);
}

public interface ISerializeType
Expand Down
18 changes: 18 additions & 0 deletions test/Serde.Generation.Test/MemberFormatTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//HintName: ColorEnumWrap.IDeserialize.cs

#nullable enable
using System;
using Serde;

partial record struct ColorEnumWrap : Serde.IDeserialize<ColorEnum>
{
static ColorEnum Serde.IDeserialize<ColorEnum>.Deserialize<D>(ref D deserializer)
{
var visitor = new SerdeVisitor();
return deserializer.DeserializeString<ColorEnum, SerdeVisitor>(visitor);
}

private sealed class SerdeVisitor : Serde.IDeserializeVisitor<ColorEnum>
{
public string ExpectedTypeName => "ColorEnum";

ColorEnum Serde.IDeserializeVisitor<ColorEnum>.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<ColorEnum>.VisitUtf8Span(System.ReadOnlySpan<byte> 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))};
}
}
Original file line number Diff line number Diff line change
@@ -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));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
//HintName: ColorEnumWrap.ISerializeWrap.cs

partial record struct ColorEnumWrap : Serde.ISerializeWrap<ColorEnum, ColorEnumWrap>
{
static ColorEnumWrap Serde.ISerializeWrap<ColorEnum, ColorEnumWrap>.Create(ColorEnum value) => new(value);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//HintName: ColorEnumWrap.ISerialize`1.cs

#nullable enable
using System;
using Serde;

partial record struct ColorEnumWrap : Serde.ISerialize<ColorEnum>
{
void ISerialize<ColorEnum>.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));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
//HintName: ColorEnumWrap.cs

readonly partial record struct ColorEnumWrap(ColorEnum Value);

0 comments on commit 11f9fa6

Please sign in to comment.