diff --git a/src/GameCult.Caching.MessagePack/CultDocumentMessagePackSerialization.cs b/src/GameCult.Caching.MessagePack/CultDocumentMessagePackSerialization.cs index 8faab41e..379d3e36 100644 --- a/src/GameCult.Caching.MessagePack/CultDocumentMessagePackSerialization.cs +++ b/src/GameCult.Caching.MessagePack/CultDocumentMessagePackSerialization.cs @@ -149,8 +149,6 @@ public static class CultDocumentMessagePackSerialization // collection resolver; a keyed class that is also an ICollection is written as a collection and its [Key] layout dropped. private static bool WrittenAsWalked(Type type, MethodInfo generic, object? standard) { - if (standard == null) - return true; var families = CultElementIds.WalksAsOwner(type) ? new IFormatterResolver[] { DynamicUnionResolver.Instance, DynamicObjectResolver.Instance } : new IFormatterResolver[] { DynamicGenericResolver.Instance, ImmutableCollectionResolver.Instance, BuiltinResolver.Instance }; diff --git a/src/GameCult.Caching/CultElementIds.cs b/src/GameCult.Caching/CultElementIds.cs index eb6fb815..4746b59c 100644 --- a/src/GameCult.Caching/CultElementIds.cs +++ b/src/GameCult.Caching/CultElementIds.cs @@ -607,7 +607,7 @@ namespace GameCult.Caching internal static bool WalksAsOwner(Type type) => Classify(Nullable.GetUnderlyingType(type) ?? type).Kind == Kind.Owner; private static bool EnumeratesObjects(Type type) => - (ElementTypeOf(type) ?? MapTypes(type)?.Value) is { } inner && Classify(inner).Kind != Kind.Leaf; + (MapTypes(type)?.Value ?? ElementTypeOf(type)) is { } inner && Classify(inner).Kind != Kind.Leaf; private static int? KeyOf(MemberInfo member) => member.GetCustomAttribute(true)?.IntKey; diff --git a/tests/GameCult.Caching.Tests/UnionArmGuardTests.cs b/tests/GameCult.Caching.Tests/UnionArmGuardTests.cs index 37f51cd8..91c94d3c 100644 --- a/tests/GameCult.Caching.Tests/UnionArmGuardTests.cs +++ b/tests/GameCult.Caching.Tests/UnionArmGuardTests.cs @@ -368,7 +368,11 @@ namespace GameCult.Caching.Tests var coll = new KCollDoc { Id = "k1" }; coll.Items.Add(new FbArm { Id = "00000000000c" }); var thrown = Assert.Throws(() => CultDocumentMessagePackSerialization.SerializeUntyped(coll, typeof(KCollDoc), registry))!; - Assert.That(thrown.Message, Does.Contain(nameof(KColl)).And.Contain("as a collection")); + Assert.That(thrown.Message, Does.Contain($"writes {nameof(KColl)} as a collection, but the element-id walk reads it by its [Key] layout")); + + // A keyed map is read by key too, so a keyed Dictionary subclass is on the path and refused the same way. + var map = Assert.Throws(() => CultDocumentMessagePackSerialization.OptionsForDocument(typeof(KDictDoc)))!; + Assert.That(map.Message, Does.Contain($"writes {nameof(KDict)} as a collection")); var bag = new KBagDoc { Id = "b1", Bag = new KBagE { Items = { new FbArm { Id = "00000000000d" } } } }; var bytes = CultDocumentMessagePackSerialization.SerializeUntyped(bag, typeof(KBagDoc), registry); @@ -399,6 +403,14 @@ namespace GameCult.Caching.Tests } } + // The walk reads a List subclass as a collection; a type MessagePack writes by an attribute formatter instead is a mismatch. + [Test] + public void ACollectionMessagePackWritesByAnotherFormatterIsRefused() + { + var thrown = Assert.Throws(() => CultDocumentMessagePackSerialization.OptionsForDocument(typeof(G)))!; + Assert.That(thrown.Message, Does.Contain($"writes {nameof(FormattedRoles)} by its [Key] layout, but the element-id walk reads it as a collection")); + } + // A resolver that throws while asked about an id-path type is a typed refusal, whether it is the AllowPrivate formatter // the claim check asks about (the claim stands) or a resolver ahead (it fails to build). [Test] @@ -407,11 +419,12 @@ namespace GameCult.Caching.Tests var first = Assert.Throws(() => CultDocumentMessagePackSerialization.OptionsForDocument(typeof(DkDoc)))!; Assert.That(first.Message, Does.Contain(nameof(ListClaimResolver)).And.Contain(nameof(DkEl))); var second = Assert.Throws(() => CultDocumentMessagePackSerialization.OptionsForDocument(typeof(DkDoc2)))!; - Assert.That(second.Message, Does.Contain(nameof(ListClaimResolver)).And.Contain(nameof(DkEl2)).And.Contain("failed to build")); + Assert.That(second.Message, Does.Contain(nameof(ListClaimResolver)).And.Contain(nameof(DkEl2)).And.Contain("failed to build").And.Not.Contain("target of an invocation"), + "the resolver's own exception is named, not the reflection wrapper"); // A type MessagePack itself cannot build a formatter for (two public members on one key) is refused, naming the type. var third = Assert.Throws(() => CultDocumentMessagePackSerialization.OptionsForDocument(typeof(DkDoc3)))!; - Assert.That(third.Message, Does.Contain(nameof(DkEl3)).And.Contain("cannot build a formatter")); + Assert.That(third.Message, Does.Contain(nameof(DkEl3)).And.Contain("cannot build a formatter").And.Not.Contain("target of an invocation")); } [MessagePackObject] @@ -428,6 +441,20 @@ namespace GameCult.Caching.Tests [Key(1)] public KColl Items = new(); } + [MessagePackObject] + public sealed class KDict : Dictionary + { + [Key(0)] public int N; + } + + [CultDocument("tests.kdict_doc", "tests.kdict_doc.v1")] + [MessagePackObject] + public sealed class KDictDoc + { + [Key(0)] [CultName] public string Id = string.Empty; + [Key(1)] public KDict Items = new(); + } + [MessagePackObject] public sealed class KBagE : IEnumerable { @@ -465,6 +492,13 @@ namespace GameCult.Caching.Tests [MessagePackObject] public sealed class DkDoc2 { [Key(0)] [CultName] public string Id = string.Empty; [Key(1)] public List Items = new(); } + [MessagePackFormatter(typeof(FormattedRolesFormatter))] public sealed class FormattedRoles : List { } + public sealed class FormattedRolesFormatter : IMessagePackFormatter + { + public void Serialize(ref MessagePackWriter writer, FormattedRoles value, MessagePackSerializerOptions options) => writer.WriteNil(); + public FormattedRoles Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options) { reader.Skip(); return new FormattedRoles(); } + } + public sealed class DkFormatter : IMessagePackFormatter { public void Serialize(ref MessagePackWriter writer, DkEl value, MessagePackSerializerOptions options) => writer.Write(value.N);