Skip to content

Commit

Permalink
Move over to ISerdeInfo interface (#183)
Browse files Browse the repository at this point in the history
Allows more customization for different info implementations,
and sets the stage for handling unions, which will have different
capabilities than other types.
  • Loading branch information
agocke authored Jul 28, 2024
1 parent c386bb7 commit 0236318
Show file tree
Hide file tree
Showing 122 changed files with 805 additions and 633 deletions.
24 changes: 12 additions & 12 deletions perf/bench/SampleTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,19 +58,19 @@ public partial record Location

public partial record LocationWrap : IDeserialize<Location>
{
public static SerdeInfo SerdeInfo { get; } = Serde.SerdeInfo.Create(
public static ISerdeInfo SerdeInfo { get; } = Serde.SerdeInfo.MakeCustom(
"Location",
SerdeInfo.TypeKind.CustomType, [
("id", StringWrap.SerdeInfo, typeof(Location).GetProperty("Id")!),
("address1", StringWrap.SerdeInfo, typeof(Location).GetProperty("Address1")!),
("address2", StringWrap.SerdeInfo, typeof(Location).GetProperty("Address2")!),
("city", StringWrap.SerdeInfo, typeof(Location).GetProperty("City")!),
("state", StringWrap.SerdeInfo, typeof(Location).GetProperty("State")!),
("postalCode", StringWrap.SerdeInfo, typeof(Location).GetProperty("PostalCode")!),
("name", StringWrap.SerdeInfo, typeof(Location).GetProperty("Name")!),
("phoneNumber", StringWrap.SerdeInfo, typeof(Location).GetProperty("PhoneNumber")!),
("country", StringWrap.SerdeInfo, typeof(Location).GetProperty("Country")!)
]);
[
("id", StringWrap.SerdeInfo, typeof(Location).GetProperty("Id")!),
("address1", StringWrap.SerdeInfo, typeof(Location).GetProperty("Address1")!),
("address2", StringWrap.SerdeInfo, typeof(Location).GetProperty("Address2")!),
("city", StringWrap.SerdeInfo, typeof(Location).GetProperty("City")!),
("state", StringWrap.SerdeInfo, typeof(Location).GetProperty("State")!),
("postalCode", StringWrap.SerdeInfo, typeof(Location).GetProperty("PostalCode")!),
("name", StringWrap.SerdeInfo, typeof(Location).GetProperty("Name")!),
("phoneNumber", StringWrap.SerdeInfo, typeof(Location).GetProperty("PhoneNumber")!),
("country", StringWrap.SerdeInfo, typeof(Location).GetProperty("Country")!)
]);

static Benchmarks.Location Serde.IDeserialize<Benchmarks.Location>.Deserialize(IDeserializer deserializer)
{
Expand Down
120 changes: 89 additions & 31 deletions src/generator/SerdeInfoGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,24 +31,62 @@ public static void GenerateSerdeInfo(
SerdeUsage usage,
ImmutableList<ITypeSymbol> inProgress)
{
var statements = new List<StatementSyntax>();
var fieldsAndProps = SymbolUtilities.GetDataMembers(receiverType, SerdeUsage.Both);
var typeDeclContext = new TypeDeclContext(typeDecl);
var typeName = receiverType.Name;
bool isEnum;
string makeFuncSuffix;
string fieldArrayType;
if (receiverType.TypeKind == TypeKind.Enum)
{
isEnum = true;
makeFuncSuffix = "Enum";
fieldArrayType = "(string, System.Reflection.MemberInfo)[]";
}
else
{
isEnum = false;
makeFuncSuffix = "Custom";
fieldArrayType = "(string, global::Serde.ISerdeInfo, System.Reflection.MemberInfo)[]";
}

var typeString = receiverType.ToDisplayString();
if (typeString.IndexOf('<') is var index && index != -1)
{
typeString = typeString[..index];
typeString = typeString + "<" + new string(',', receiverType.TypeParameters.Length - 1) + ">";
}

var membersString = string.Join("," + Environment.NewLine,
SymbolUtilities.GetDataMembers(receiverType, SerdeUsage.Both).SelectNotNull(GetMemberEntry));

List<string> makeArgs = [ $"\"{receiverType.Name}\"" ];

if (isEnum)
{
string underlyingInfo;
if (Wrappers.TryGetImplicitWrapper(receiverType.EnumUnderlyingType!, context, usage, inProgress) is { Wrapper: { } wrap })
{
underlyingInfo = wrap.ToString();
}
else
{
// This should never happen. Produce a bogus string
underlyingInfo = "<underlying info not found, this is a bug>";
}
makeArgs.Add($"global::Serde.SerdeInfoProvider.GetInfo<{underlyingInfo}>()");
}

makeArgs.Add($$"""
new {{fieldArrayType}} {
{{membersString}}
}
""");

var argsString = string.Join("," + Environment.NewLine + " ", makeArgs);

var body = $$"""
static global::Serde.SerdeInfo global::Serde.ISerdeInfoProvider.SerdeInfo { get; } = Serde.SerdeInfo.Create(
"{{typeName}}",
Serde.SerdeInfo.TypeKind.{{(receiverType.TypeKind == TypeKind.Enum ? "Enum" : "CustomType")}},
new (string, global::Serde.SerdeInfo, System.Reflection.MemberInfo)[] {
{{string.Join("," + Environment.NewLine, fieldsAndProps.SelectNotNull(GetMemberEntry))}}
});
static global::Serde.ISerdeInfo global::Serde.ISerdeInfoProvider.SerdeInfo { get; } = Serde.SerdeInfo.Make{{makeFuncSuffix}}(
{{argsString}});
""";
var typeDeclContext = new TypeDeclContext(typeDecl);
var (fileName, newType) = SerdeImplRoslynGenerator.MakePartialDecl(
typeDeclContext,
baseList: null,
Expand All @@ -60,30 +98,50 @@ public static void GenerateSerdeInfo(
// Returns a string that represents a single member in the custom SerdeInfo member list.
string? GetMemberEntry(DataMemberSymbol m)
{
var fieldName = m.GetFormattedName();
var getAccessor = m.Symbol.Kind == SymbolKind.Field ? "GetField" : "GetProperty";
string wrapperName;
if (Wrappers.TryGetExplicitWrapper(m, context, usage, inProgress) is { } explicitWrap)
{
wrapperName = explicitWrap.ToString();
}
else if (SerdeImplRoslynGenerator.ImplementsSerde(m.Type, m.Type, context, usage))
{
wrapperName = m.Type.WithNullableAnnotation(m.NullableAnnotation).ToDisplayString();
}
else if (Wrappers.TryGetImplicitWrapper(m.Type, context, usage, inProgress) is { Wrapper: { } wrap })
{
wrapperName = wrap.ToString();
}
else
List<string> elements = [ $"\"{m.GetFormattedName()}\"" ];

if (!isEnum)
{
// This is an error, but it should have already been produced by the serialization
// or deserialization generator
return null;
string? wrapperName = GetWrapperName(m, context, usage, inProgress);
if (wrapperName is null)
{
// This is an error, but it should have already been produced by the serialization
// or deserialization generator
return null;
}
elements.Add($"global::Serde.SerdeInfoProvider.GetInfo<{wrapperName}>()");
}
var fieldInfo = $"global::Serde.SerdeInfoProvider.GetInfo<{wrapperName}>()";

return $"""("{fieldName}", {fieldInfo}, typeof({typeString}).{getAccessor}("{m.Name}")!)""";
var getAccessor = m.Symbol.Kind == SymbolKind.Field ? "GetField" : "GetProperty";
elements.Add($"typeof({typeString}).{getAccessor}(\"{m.Name}\")!");

return $"""({string.Join(", ", elements)})""";
}
}

private static string? GetWrapperName(
DataMemberSymbol m,
GeneratorExecutionContext context,
SerdeUsage usage,
ImmutableList<ITypeSymbol> inProgress)
{
string? wrapperName;
if (Wrappers.TryGetExplicitWrapper(m, context, usage, inProgress) is { } explicitWrap)
{
wrapperName = explicitWrap.ToString();
}
else if (SerdeImplRoslynGenerator.ImplementsSerde(m.Type, m.Type, context, usage))
{
wrapperName = m.Type.WithNullableAnnotation(m.NullableAnnotation).ToDisplayString();
}
else if (Wrappers.TryGetImplicitWrapper(m.Type, context, usage, inProgress) is { Wrapper: { } wrap })
{
wrapperName = wrap.ToString();
}
else
{
wrapperName = null;
}
return wrapperName;
}
}
16 changes: 8 additions & 8 deletions src/serde-xml/XmlSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,9 @@ public void SerializeDouble(double d)
_writer.WriteValue(d);
}

void ISerializer.SerializeEnumValue<T, U>(SerdeInfo typeInfo, int index, T value, U serialize)
void ISerializer.SerializeEnumValue<T, U>(ISerdeInfo serdeInfo, int index, T value, U serialize)
{
var name = typeInfo.GetStringSerializeName(index);
var name = serdeInfo.GetStringSerializeName(index);
SerializeString(name);
}

Expand Down Expand Up @@ -156,13 +156,13 @@ private static string FormatTypeName(string name)
return formattingListener.ToString();
}

public ISerializeCollection SerializeCollection(SerdeInfo typeInfo, int? length)
public ISerializeCollection SerializeCollection(ISerdeInfo typeInfo, int? length)
{
if (typeInfo.Kind == SerdeInfo.TypeKind.Dictionary)
if (typeInfo.Kind == ISerdeInfo.TypeKind.Dictionary)
{
throw new NotSupportedException("Serde.XmlSerializer doesn't currently support serializing dictionaries");
}
else if (typeInfo.Kind != SerdeInfo.TypeKind.Enumerable)
else if (typeInfo.Kind != ISerdeInfo.TypeKind.Enumerable)
{
throw new ArgumentException("typeInfo must be a collection type", nameof(typeInfo));
}
Expand All @@ -188,7 +188,7 @@ public SerializeCollectionImpl(XmlSerializer serializer, State savedState)
void ISerializeCollection.SerializeElement<T, U>(T value, U serialize)
=> serialize.Serialize(value, _serializer);

void ISerializeCollection.End(SerdeInfo typeInfo)
void ISerializeCollection.End(ISerdeInfo typeInfo)
{
if (_savedState == State.Enumerable)
{
Expand All @@ -198,7 +198,7 @@ void ISerializeCollection.End(SerdeInfo typeInfo)
}
}

public ISerializeType SerializeType(SerdeInfo typeInfo)
public ISerializeType SerializeType(ISerdeInfo typeInfo)
{
var saved = _state;
bool writeEnd;
Expand Down Expand Up @@ -228,7 +228,7 @@ public XmlTypeSerializer(bool writeEnd, XmlSerializer parent, State savedState)
_savedState = savedState;
}

public void SerializeField<T, U>(SerdeInfo typeInfo, int fieldIndex, T value, U impl)
public void SerializeField<T, U>(ISerdeInfo typeInfo, int fieldIndex, T value, U impl)
where U : ISerialize<T>
{
var name = typeInfo.GetStringSerializeName(fieldIndex);
Expand Down
8 changes: 4 additions & 4 deletions src/serde/IDeserialize.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public interface IDeserializeCollection
{
int? SizeOpt { get; }

bool TryReadValue<T, D>(SerdeInfo typeInfo, [MaybeNullWhen(false)] out T next)
bool TryReadValue<T, D>(ISerdeInfo typeInfo, [MaybeNullWhen(false)] out T next)
where D : IDeserialize<T>;
}

Expand Down Expand Up @@ -96,7 +96,7 @@ public interface IDeserializeType
/// cref="IndexNotFound" /> and set <paramref name="errorName" /> to the name of the missing
/// field, or the best-possible user-facing name.
/// </summary>
int TryReadIndex(SerdeInfo map, out string? errorName);
int TryReadIndex(ISerdeInfo map, out string? errorName);

V ReadValue<V, D>(int index) where D : IDeserialize<V>;
}
Expand All @@ -120,7 +120,7 @@ public interface IDeserializer
T DeserializeString<T>(IDeserializeVisitor<T> v);
T DeserializeIdentifier<T>(IDeserializeVisitor<T> v);
T DeserializeNullableRef<T>(IDeserializeVisitor<T> v);
IDeserializeCollection DeserializeCollection(SerdeInfo typeInfo);
IDeserializeType DeserializeType(SerdeInfo typeInfo);
IDeserializeCollection DeserializeCollection(ISerdeInfo typeInfo);
IDeserializeType DeserializeType(ISerdeInfo typeInfo);
}
}
67 changes: 67 additions & 0 deletions src/serde/ISerdeInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@

using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Metadata;

namespace Serde;

/// <summary>
/// Provides info for an arbitrary type that can be serialized or deserialized.
/// </summary>
public interface ISerdeInfo
{
string TypeName { get; }
TypeKind Kind { get; }

/// <summary>
/// The number of serializable or deserializable fields or properties on the type.
/// </summary>
int FieldCount { get; }

/// <summary>
/// Get the field name as a string for the field at the given index. The index must be valid.
/// </summary>
string GetStringSerializeName(int index);

/// <summary>
/// Get the field name as a UTF8 string for the field at the given index. The index must be valid.
/// </summary>
Utf8Span GetSerializeName(int index);

/// <summary>
/// Get the attributes for the field at the given index. The index must be valid. This list may be
/// modified from the original set of attributes in source code or metadata to reflect only the
/// attributes that are relevant to serialization or deserialization.
/// </summary>
IList<CustomAttributeData> GetCustomAttributeData(int index);

/// <summary>
/// Search the fields for one with the given name and return its index. Returns
/// <see cref="IDeserializeType.IndexNotFound"/> if not found.
/// </summary>
int TryGetIndex(Utf8Span fieldName);

public enum TypeKind
{
Primitive,
CustomType,
Enumerable,
Dictionary,
Enum,
/// <summary>
/// Represents a closed union of types. Any type that returns this value from <see
/// cref="Kind"/> must also implement <see cref="IUnionSerdeInfo"/>.
/// </summary>
Union
}
}

/// <summary>
/// Provides info for a "closed union" of types that can be serialized or deserialized.
/// </summary>
public interface IUnionSerdeInfo : ISerdeInfo
{
TypeKind ISerdeInfo.Kind => TypeKind.Union;

IEnumerable<ISerdeInfo> CaseInfos { get; }
}
18 changes: 9 additions & 9 deletions src/serde/ISerialize.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@ public interface ISerialize<T> : ISerdeInfoProvider

public interface ISerializeType
{
void SerializeField<T, U>(SerdeInfo typeInfo, int index, T value, U serialize) where U : ISerialize<T>;
void SkipField(SerdeInfo typeInfo, int index) { }
void SerializeField<T, U>(ISerdeInfo typeInfo, int index, T value, U serialize) where U : ISerialize<T>;
void SkipField(ISerdeInfo typeInfo, int index) { }
void End();
}

public static class ISerializeTypeExt
{
public static void SerializeField<T, U>(
this ISerializeType serializeType,
SerdeInfo typeInfo,
ISerdeInfo typeInfo,
int index,
T value) where U : struct, ISerialize<T>
{
Expand All @@ -25,7 +25,7 @@ public static void SerializeField<T, U>(

public static void SerializeFieldIfNotNull<T, U>(
this ISerializeType serializeType,
SerdeInfo typeInfo,
ISerdeInfo typeInfo,
int index,
T value,
U serialize) where U : ISerialize<T>
Expand All @@ -42,7 +42,7 @@ public static void SerializeFieldIfNotNull<T, U>(

public static void SerializeFieldIfNotNull<T, U>(
this ISerializeType serializeType,
SerdeInfo typeInfo,
ISerdeInfo typeInfo,
int index,
T value) where U : struct, ISerialize<T>
{
Expand All @@ -53,7 +53,7 @@ public static void SerializeFieldIfNotNull<T, U>(
public interface ISerializeCollection
{
void SerializeElement<T, U>(T value, U serialize) where U : ISerialize<T>;
void End(SerdeInfo typeInfo);
void End(ISerdeInfo typeInfo);
}

public interface ISerializer
Expand All @@ -73,10 +73,10 @@ public interface ISerializer
void SerializeDecimal(decimal d);
void SerializeString(string s);
void SerializeNull();
void SerializeEnumValue<T, U>(SerdeInfo typeInfo, int index, T value, U serialize)
void SerializeEnumValue<T, U>(ISerdeInfo typeInfo, int index, T value, U serialize)
where T : unmanaged
where U : ISerialize<T>;

ISerializeType SerializeType(SerdeInfo typeInfo);
ISerializeCollection SerializeCollection(SerdeInfo typeInfo, int? length);
ISerializeType SerializeType(ISerdeInfo typeInfo);
ISerializeCollection SerializeCollection(ISerdeInfo typeInfo, int? length);
}
Loading

0 comments on commit 0236318

Please sign in to comment.