Generate explicit CultCache payload codecs

This commit is contained in:
Meta Vens 2026-05-08 02:19:27 +01:00
commit c26870b803
7 changed files with 135 additions and 15 deletions

View file

@ -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; }

View file

@ -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);
}
}

View file

@ -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);
}

View file

@ -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; }
}

View file

@ -5,7 +5,6 @@ using MessagePack;
namespace GameCult.Networking
{
[CultDocument("gamecult.player_data", "gamecult.player_data.v1")]
[MessagePackObject]
public class PlayerData
{
[Key(0)]

View file

@ -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)]

View file

@ -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;