Element ids: a keyed map counts as enumerating objects; family check tests assert the exact refusal
EnumeratesObjects took ElementTypeOf first, which for a dictionary is the KeyValuePair (a leaf), so a keyed Dictionary subclass never reached the path; the map's value type is asked first. The unreachable null-standard guard in WrittenAsWalked is removed. Tests pin the direction and wording of the refusal, the attribute-formatter collection case and the unwrapped inner exception. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
956c077a1a
commit
48d3f667e6
3 changed files with 38 additions and 6 deletions
|
|
@ -149,8 +149,6 @@ public static class CultDocumentMessagePackSerialization
|
|||
// collection resolver; a keyed class that is also an ICollection<T> 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 };
|
||||
|
|
|
|||
|
|
@ -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<KeyAttribute>(true)?.IntKey;
|
||||
|
||||
|
|
|
|||
|
|
@ -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<InvalidOperationException>(() => 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<InvalidOperationException>(() => 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<InvalidOperationException>(() => CultDocumentMessagePackSerialization.OptionsForDocument(typeof(G<FormattedRoles>)))!;
|
||||
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<InvalidOperationException>(() => CultDocumentMessagePackSerialization.OptionsForDocument(typeof(DkDoc)))!;
|
||||
Assert.That(first.Message, Does.Contain(nameof(ListClaimResolver)).And.Contain(nameof(DkEl)));
|
||||
var second = Assert.Throws<InvalidOperationException>(() => 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<InvalidOperationException>(() => 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<string, GHold>
|
||||
{
|
||||
[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<FbArm>
|
||||
{
|
||||
|
|
@ -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<DkEl2> Items = new(); }
|
||||
|
||||
[MessagePackFormatter(typeof(FormattedRolesFormatter))] public sealed class FormattedRoles : List<FbArm> { }
|
||||
public sealed class FormattedRolesFormatter : IMessagePackFormatter<FormattedRoles>
|
||||
{
|
||||
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<DkEl>
|
||||
{
|
||||
public void Serialize(ref MessagePackWriter writer, DkEl value, MessagePackSerializerOptions options) => writer.Write(value.N);
|
||||
|
|
|
|||
Loading…
Reference in a new issue