Generate explicit CultCache payload codecs
This commit is contained in:
parent
8606b74d65
commit
c26870b803
7 changed files with 135 additions and 15 deletions
|
|
@ -24,7 +24,7 @@ namespace GameCult.Caching.MessagePack.Generator
|
|||
var compilationAndShapes = context.CompilationProvider.Combine(documentShapes.Collect());
|
||||
context.RegisterSourceOutput(
|
||||
compilationAndShapes,
|
||||
static (spc, pair) => EmitProvider(spc, pair.Left.AssemblyName ?? "GameCult", pair.Right));
|
||||
static (spc, pair) => EmitProvider(spc, pair.Left, pair.Right));
|
||||
}
|
||||
|
||||
private static DocumentShape? BuildDocumentShape(GeneratorSyntaxContext context)
|
||||
|
|
@ -142,7 +142,7 @@ namespace GameCult.Caching.MessagePack.Generator
|
|||
: null;
|
||||
}
|
||||
|
||||
private static void EmitProvider(SourceProductionContext context, string assemblyName, ImmutableArray<DocumentShape> shapes)
|
||||
private static void EmitProvider(SourceProductionContext context, Compilation compilation, ImmutableArray<DocumentShape> shapes)
|
||||
{
|
||||
var documents = shapes
|
||||
.Where(shape => shape != null)
|
||||
|
|
@ -156,7 +156,11 @@ namespace GameCult.Caching.MessagePack.Generator
|
|||
return;
|
||||
}
|
||||
|
||||
var providerName = $"GeneratedCultDocumentMetadataProvider_{SanitizeIdentifier(assemblyName)}";
|
||||
var canEmitPayloadCodecs =
|
||||
compilation.GetTypeByMetadataName("MessagePack.MessagePackWriter") != null &&
|
||||
compilation.GetTypeByMetadataName("GameCult.Caching.MessagePack.CultDocumentMessagePackSerialization") != null;
|
||||
|
||||
var providerName = $"GeneratedCultDocumentMetadataProvider_{SanitizeIdentifier(compilation.AssemblyName ?? "GameCult")}";
|
||||
var builder = new StringBuilder();
|
||||
builder.AppendLine("// <auto-generated/>");
|
||||
builder.AppendLine("#nullable enable");
|
||||
|
|
@ -181,6 +185,12 @@ namespace GameCult.Caching.MessagePack.Generator
|
|||
builder.AppendLine(document.NameAccessorMethodName == null
|
||||
? " null,"
|
||||
: $" {document.NameAccessorMethodName},");
|
||||
builder.AppendLine(canEmitPayloadCodecs
|
||||
? $" {document.PayloadSerializerMethodName},"
|
||||
: " null,");
|
||||
builder.AppendLine(canEmitPayloadCodecs
|
||||
? $" {document.PayloadDeserializerMethodName},"
|
||||
: " null,");
|
||||
builder.AppendLine(" new global::GameCult.Caching.CultGeneratedDocumentIndexAccessor[]");
|
||||
builder.AppendLine(" {");
|
||||
foreach (var indexAccessor in document.IndexAccessors)
|
||||
|
|
@ -190,12 +200,12 @@ namespace GameCult.Caching.MessagePack.Generator
|
|||
builder.AppendLine(" },");
|
||||
builder.AppendLine(" new global::GameCult.Caching.CultGeneratedDocumentMemberDefinition[]");
|
||||
builder.AppendLine(" {");
|
||||
foreach (var member in document.Members)
|
||||
foreach (var member in document.Members.OrderBy(member => member.Slot))
|
||||
{
|
||||
builder.AppendLine(" new global::GameCult.Caching.CultGeneratedDocumentMemberDefinition(");
|
||||
builder.AppendLine($" \"{Escape(member.Name)}\",");
|
||||
builder.AppendLine($" {member.Slot},");
|
||||
builder.AppendLine($" \"{Escape(member.TypeName)}\",");
|
||||
builder.AppendLine($" \"{Escape(member.SchemaTypeName)}\",");
|
||||
builder.AppendLine($" {(member.IsReference ? "true" : "false")},");
|
||||
builder.AppendLine($" {(member.IsMany ? "true" : "false")},");
|
||||
builder.AppendLine(member.TargetSchemaName == null
|
||||
|
|
@ -219,6 +229,12 @@ namespace GameCult.Caching.MessagePack.Generator
|
|||
|
||||
foreach (var document in documents)
|
||||
{
|
||||
if (canEmitPayloadCodecs)
|
||||
{
|
||||
EmitPayloadSerializer(builder, document);
|
||||
EmitPayloadDeserializer(builder, document);
|
||||
}
|
||||
|
||||
foreach (var member in document.Members.Where(member => member.AccessorMethodName != null))
|
||||
{
|
||||
builder.AppendLine($" private static string? {member.AccessorMethodName}(object document)");
|
||||
|
|
@ -247,6 +263,49 @@ namespace GameCult.Caching.MessagePack.Generator
|
|||
context.AddSource($"{providerName}.g.cs", SourceText.From(builder.ToString(), Encoding.UTF8));
|
||||
}
|
||||
|
||||
private static void EmitPayloadSerializer(StringBuilder builder, DocumentShape document)
|
||||
{
|
||||
builder.AppendLine($" private static byte[] {document.PayloadSerializerMethodName}(object document)");
|
||||
builder.AppendLine(" {");
|
||||
builder.AppendLine($" var typed = ({document.DocumentTypeName})document;");
|
||||
builder.AppendLine(" var options = global::GameCult.Caching.MessagePack.CultDocumentMessagePackSerialization.Options;");
|
||||
builder.AppendLine(" var buffer = new global::System.Buffers.ArrayBufferWriter<byte>();");
|
||||
builder.AppendLine(" var writer = new global::MessagePack.MessagePackWriter(buffer);");
|
||||
builder.AppendLine($" writer.WriteArrayHeader({document.Members.Length});");
|
||||
foreach (var member in document.Members.OrderBy(member => member.Slot))
|
||||
{
|
||||
builder.AppendLine($" global::MessagePack.FormatterResolverExtensions.GetFormatterWithVerify<{member.TypeSyntaxName}>(options.Resolver).Serialize(ref writer, typed.{member.Name}, options);");
|
||||
}
|
||||
builder.AppendLine(" writer.Flush();");
|
||||
builder.AppendLine(" return buffer.WrittenSpan.ToArray();");
|
||||
builder.AppendLine(" }");
|
||||
builder.AppendLine();
|
||||
}
|
||||
|
||||
private static void EmitPayloadDeserializer(StringBuilder builder, DocumentShape document)
|
||||
{
|
||||
builder.AppendLine($" private static object {document.PayloadDeserializerMethodName}(byte[] payload)");
|
||||
builder.AppendLine(" {");
|
||||
builder.AppendLine(" var options = global::GameCult.Caching.MessagePack.CultDocumentMessagePackSerialization.Options;");
|
||||
builder.AppendLine(" var reader = new global::MessagePack.MessagePackReader(payload);");
|
||||
builder.AppendLine(" var count = reader.ReadArrayHeader();");
|
||||
builder.AppendLine($" var value = new {document.DocumentTypeName}();");
|
||||
foreach (var member in document.Members.OrderBy(member => member.Slot))
|
||||
{
|
||||
builder.AppendLine($" if (count > {member.Slot})");
|
||||
builder.AppendLine(" {");
|
||||
builder.AppendLine($" value.{member.Name} = global::MessagePack.FormatterResolverExtensions.GetFormatterWithVerify<{member.TypeSyntaxName}>(options.Resolver).Deserialize(ref reader, options);");
|
||||
builder.AppendLine(" }");
|
||||
}
|
||||
builder.AppendLine($" for (var index = {document.Members.Length}; index < count; index++)");
|
||||
builder.AppendLine(" {");
|
||||
builder.AppendLine(" reader.Skip();");
|
||||
builder.AppendLine(" }");
|
||||
builder.AppendLine(" return value;");
|
||||
builder.AppendLine(" }");
|
||||
builder.AppendLine();
|
||||
}
|
||||
|
||||
private static string Escape(string value)
|
||||
{
|
||||
return value
|
||||
|
|
@ -345,7 +404,8 @@ namespace GameCult.Caching.MessagePack.Generator
|
|||
{
|
||||
private MemberShapeSeed(
|
||||
string name,
|
||||
string typeName,
|
||||
string schemaTypeName,
|
||||
string typeSyntaxName,
|
||||
int? explicitSlot,
|
||||
string sortKey,
|
||||
bool isName,
|
||||
|
|
@ -356,7 +416,8 @@ namespace GameCult.Caching.MessagePack.Generator
|
|||
string? targetSchemaName)
|
||||
{
|
||||
Name = name;
|
||||
TypeName = typeName;
|
||||
SchemaTypeName = schemaTypeName;
|
||||
TypeSyntaxName = typeSyntaxName;
|
||||
ExplicitSlot = explicitSlot;
|
||||
SortKey = sortKey;
|
||||
IsName = isName;
|
||||
|
|
@ -368,7 +429,8 @@ namespace GameCult.Caching.MessagePack.Generator
|
|||
}
|
||||
|
||||
public string Name { get; }
|
||||
public string TypeName { get; }
|
||||
public string SchemaTypeName { get; }
|
||||
public string TypeSyntaxName { get; }
|
||||
public int? ExplicitSlot { get; }
|
||||
public string SortKey { get; }
|
||||
public bool IsName { get; }
|
||||
|
|
@ -384,7 +446,8 @@ namespace GameCult.Caching.MessagePack.Generator
|
|||
return new MemberShape(
|
||||
Name,
|
||||
slot,
|
||||
TypeName,
|
||||
SchemaTypeName,
|
||||
TypeSyntaxName,
|
||||
IsReference,
|
||||
IsMany,
|
||||
TargetSchemaName,
|
||||
|
|
@ -418,6 +481,7 @@ namespace GameCult.Caching.MessagePack.Generator
|
|||
return new MemberShapeSeed(
|
||||
member.Name,
|
||||
GetSchemaTypeName(memberType),
|
||||
memberType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
|
||||
GetExplicitSlot(member),
|
||||
BuildAccessorStem(member),
|
||||
GetAttribute(member, "GameCult.Caching.CultNameAttribute") != null,
|
||||
|
|
@ -469,6 +533,9 @@ namespace GameCult.Caching.MessagePack.Generator
|
|||
IsGlobal = isGlobal;
|
||||
NameMember = nameMember;
|
||||
NameAccessorMethodName = nameAccessorMethodName;
|
||||
var safeStem = SanitizeIdentifier(documentTypeName.Replace("global::", string.Empty).Replace(".", "_"));
|
||||
PayloadSerializerMethodName = $"Serialize_{safeStem}_Payload";
|
||||
PayloadDeserializerMethodName = $"Deserialize_{safeStem}_Payload";
|
||||
IndexAccessors = indexAccessors;
|
||||
Members = members;
|
||||
}
|
||||
|
|
@ -479,6 +546,8 @@ namespace GameCult.Caching.MessagePack.Generator
|
|||
public bool IsGlobal { get; }
|
||||
public string? NameMember { get; }
|
||||
public string? NameAccessorMethodName { get; }
|
||||
public string PayloadSerializerMethodName { get; }
|
||||
public string PayloadDeserializerMethodName { get; }
|
||||
public ImmutableArray<IndexAccessorShape> IndexAccessors { get; }
|
||||
public ImmutableArray<MemberShape> Members { get; }
|
||||
}
|
||||
|
|
@ -500,7 +569,8 @@ namespace GameCult.Caching.MessagePack.Generator
|
|||
public MemberShape(
|
||||
string name,
|
||||
int slot,
|
||||
string typeName,
|
||||
string schemaTypeName,
|
||||
string typeSyntaxName,
|
||||
bool isReference,
|
||||
bool isMany,
|
||||
string? targetSchemaName,
|
||||
|
|
@ -512,7 +582,8 @@ namespace GameCult.Caching.MessagePack.Generator
|
|||
{
|
||||
Name = name;
|
||||
Slot = slot;
|
||||
TypeName = typeName;
|
||||
SchemaTypeName = schemaTypeName;
|
||||
TypeSyntaxName = typeSyntaxName;
|
||||
IsReference = isReference;
|
||||
IsMany = isMany;
|
||||
TargetSchemaName = targetSchemaName;
|
||||
|
|
@ -525,7 +596,8 @@ namespace GameCult.Caching.MessagePack.Generator
|
|||
|
||||
public string Name { get; }
|
||||
public int Slot { get; }
|
||||
public string TypeName { get; }
|
||||
public string SchemaTypeName { get; }
|
||||
public string TypeSyntaxName { get; }
|
||||
public bool IsReference { get; }
|
||||
public bool IsMany { get; }
|
||||
public string? TargetSchemaName { get; }
|
||||
|
|
|
|||
|
|
@ -45,11 +45,26 @@ public static class CultDocumentMessagePackSerialization
|
|||
|
||||
public static byte[] SerializeUntyped(object value, Type type)
|
||||
{
|
||||
if (value != null)
|
||||
{
|
||||
var descriptor = CultDocumentRegistry.Shared.GetRequired(type);
|
||||
if (descriptor.GeneratedPayloadSerializer != null)
|
||||
{
|
||||
return descriptor.GeneratedPayloadSerializer(value);
|
||||
}
|
||||
}
|
||||
|
||||
return MessagePackSerializer.Serialize(type, value, Options);
|
||||
}
|
||||
|
||||
public static object DeserializeUntyped(Type type, byte[] payload)
|
||||
{
|
||||
var descriptor = CultDocumentRegistry.Shared.GetRequired(type);
|
||||
if (descriptor.GeneratedPayloadDeserializer != null)
|
||||
{
|
||||
return descriptor.GeneratedPayloadDeserializer(payload);
|
||||
}
|
||||
|
||||
return MessagePackSerializer.Deserialize(type, payload, Options);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,6 +50,8 @@ namespace GameCult.Caching
|
|||
bool isGlobal,
|
||||
string? nameMember,
|
||||
Func<object, string?>? nameAccessor,
|
||||
Func<object, byte[]>? generatedPayloadSerializer,
|
||||
Func<byte[], object>? generatedPayloadDeserializer,
|
||||
IReadOnlyDictionary<string, Func<object, string>> indexAccessors,
|
||||
IReadOnlyList<CultDocumentMemberDescriptor> members)
|
||||
{
|
||||
|
|
@ -62,6 +64,8 @@ namespace GameCult.Caching
|
|||
IsGlobal = isGlobal;
|
||||
NameMember = nameMember;
|
||||
NameAccessor = nameAccessor;
|
||||
GeneratedPayloadSerializer = generatedPayloadSerializer;
|
||||
GeneratedPayloadDeserializer = generatedPayloadDeserializer;
|
||||
IndexAccessors = indexAccessors;
|
||||
Members = members;
|
||||
}
|
||||
|
|
@ -75,6 +79,8 @@ namespace GameCult.Caching
|
|||
public bool IsGlobal { get; }
|
||||
public string? NameMember { get; }
|
||||
internal Func<object, string?>? NameAccessor { get; }
|
||||
public Func<object, byte[]>? GeneratedPayloadSerializer { get; }
|
||||
public Func<byte[], object>? GeneratedPayloadDeserializer { get; }
|
||||
internal IReadOnlyDictionary<string, Func<object, string>> IndexAccessors { get; }
|
||||
internal IReadOnlyList<CultDocumentMemberDescriptor> Members { get; }
|
||||
|
||||
|
|
@ -292,6 +298,8 @@ namespace GameCult.Caching
|
|||
type.GetCustomAttribute<CultGlobalAttribute>() != null,
|
||||
nameMember?.Member.Name,
|
||||
nameMember?.GetterNullable,
|
||||
null,
|
||||
null,
|
||||
indexAccessors,
|
||||
descriptorMembers);
|
||||
}
|
||||
|
|
@ -327,6 +335,8 @@ namespace GameCult.Caching
|
|||
definition.IsGlobal,
|
||||
definition.NameMember,
|
||||
definition.NameAccessor,
|
||||
definition.SerializePayload,
|
||||
definition.DeserializePayload,
|
||||
definition.IndexAccessors.ToDictionary(accessor => accessor.Alias, accessor => accessor.Accessor, StringComparer.Ordinal),
|
||||
descriptorMembers);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ namespace GameCult.Caching
|
|||
bool isGlobal,
|
||||
string? nameMember,
|
||||
Func<object, string?>? nameAccessor,
|
||||
Func<object, byte[]>? serializePayload,
|
||||
Func<byte[], object>? deserializePayload,
|
||||
IReadOnlyList<CultGeneratedDocumentIndexAccessor> indexAccessors,
|
||||
IReadOnlyList<CultGeneratedDocumentMemberDefinition> members)
|
||||
{
|
||||
|
|
@ -44,6 +46,8 @@ namespace GameCult.Caching
|
|||
IsGlobal = isGlobal;
|
||||
NameMember = nameMember;
|
||||
NameAccessor = nameAccessor;
|
||||
SerializePayload = serializePayload;
|
||||
DeserializePayload = deserializePayload;
|
||||
IndexAccessors = indexAccessors ?? Array.Empty<CultGeneratedDocumentIndexAccessor>();
|
||||
Members = members ?? Array.Empty<CultGeneratedDocumentMemberDefinition>();
|
||||
}
|
||||
|
|
@ -54,6 +58,8 @@ namespace GameCult.Caching
|
|||
public bool IsGlobal { get; }
|
||||
public string? NameMember { get; }
|
||||
public Func<object, string?>? NameAccessor { get; }
|
||||
public Func<object, byte[]>? SerializePayload { get; }
|
||||
public Func<byte[], object>? DeserializePayload { get; }
|
||||
public IReadOnlyList<CultGeneratedDocumentIndexAccessor> IndexAccessors { get; }
|
||||
public IReadOnlyList<CultGeneratedDocumentMemberDefinition> Members { get; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ using MessagePack;
|
|||
namespace GameCult.Networking
|
||||
{
|
||||
[CultDocument("gamecult.player_data", "gamecult.player_data.v1")]
|
||||
[MessagePackObject]
|
||||
public class PlayerData
|
||||
{
|
||||
[Key(0)]
|
||||
|
|
|
|||
|
|
@ -175,6 +175,26 @@ namespace GameCult.Caching.Tests
|
|||
Assert.That(parentMember.TypeName, Does.Contain("CultRecordRef"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GeneratedMetadataProvider_Emits_Payload_Codecs_For_Plain_CultDocuments()
|
||||
{
|
||||
var descriptor = CultDocumentRegistry.Shared.GetRequired<NamedTestEntry>();
|
||||
var original = new NamedTestEntry
|
||||
{
|
||||
Name = "Teeth",
|
||||
Value = "slot-array"
|
||||
};
|
||||
|
||||
Assert.That(descriptor.GeneratedPayloadSerializer, Is.Not.Null);
|
||||
Assert.That(descriptor.GeneratedPayloadDeserializer, Is.Not.Null);
|
||||
|
||||
var payload = descriptor.GeneratedPayloadSerializer!(original);
|
||||
var roundTrip = (NamedTestEntry)descriptor.GeneratedPayloadDeserializer!(payload);
|
||||
|
||||
Assert.That(roundTrip.Name, Is.EqualTo("Teeth"));
|
||||
Assert.That(roundTrip.Value, Is.EqualTo("slot-array"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Registry_CanonicalSchemaJson_Tracks_Reference_Metadata()
|
||||
{
|
||||
|
|
@ -185,7 +205,6 @@ namespace GameCult.Caching.Tests
|
|||
}
|
||||
|
||||
[CultDocument("tests.named_entry", "tests.named_entry.v1")]
|
||||
[MessagePackObject(AllowPrivate = true)]
|
||||
internal sealed class NamedTestEntry
|
||||
{
|
||||
[Key(0)]
|
||||
|
|
|
|||
|
|
@ -666,7 +666,6 @@ static class InteropPeerShared
|
|||
}
|
||||
|
||||
[CultDocument(InteropPeerShared.InteropDocumentType, InteropPeerShared.InteropSchemaVersion)]
|
||||
[MessagePackObject]
|
||||
public sealed class CultNetInteropNote
|
||||
{
|
||||
[Key(0)] public string SchemaVersion { get; set; } = InteropPeerShared.InteropSchemaVersion;
|
||||
|
|
|
|||
Loading…
Reference in a new issue