Element ids: string keys read from the members MessagePack writes; AllowPrivate, inherited-formatter and unwritten-string-key pins; README states the whole-value exception
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
7e69daa85e
commit
0c797ec3f7
3 changed files with 51 additions and 3 deletions
|
|
@ -617,12 +617,12 @@ namespace GameCult.Caching
|
|||
|
||||
// A type MessagePack writes as one value the walk cannot look into: its own formatter ([MessagePackFormatter] on the type,
|
||||
// inherited), [DataContract] on a type that is not [MessagePackObject] (the object resolver writes its members), or string
|
||||
// keys (KeyAsPropertyName, or any [Key] with a string key). Such a type is Leaf, even when it is also enumerable.
|
||||
// keys (KeyAsPropertyName, or a [Key] with a string key on a member MessagePack writes: a private or [IgnoreMember] one is not written). Such a type is Leaf, even when it is also enumerable.
|
||||
private static bool IsWrittenWhole(Type type) =>
|
||||
type.IsDefined(typeof(MessagePackFormatterAttribute), true) ||
|
||||
(type.IsDefined(typeof(DataContractAttribute), true) && !type.IsDefined(typeof(MessagePackObjectAttribute), true)) ||
|
||||
type.GetCustomAttribute<MessagePackObjectAttribute>(true)?.KeyAsPropertyName == true ||
|
||||
AllInstanceMembers(type).Any(member => member.GetCustomAttribute<KeyAttribute>(true)?.StringKey != null);
|
||||
MembersOf(type).Any(member => member.GetCustomAttribute<KeyAttribute>(true)?.StringKey != null);
|
||||
|
||||
// A type MessagePack serializes by key: [MessagePackObject] or [Union].
|
||||
private static bool IsKeyed(Type type) =>
|
||||
|
|
|
|||
|
|
@ -195,6 +195,15 @@ A document type that holds a list of objects gives every object type in that lis
|
|||
`[Key]`. The id is what lets a variant override one list element instead of the
|
||||
whole list. Lists of strings, numbers and other non-object values need none.
|
||||
|
||||
The rule covers the shapes the id walk owns: arrays, `IEnumerable<T>` classes and
|
||||
interfaces, dictionary values, and integer-keyed `[MessagePackObject]` and `[Union]`
|
||||
types. Any other holder is one whole value: `ImmutableArray<T>`, `ArraySegment<T>`,
|
||||
`Memory<T>`, `KeyValuePair`, tuples, `Lazy<T>`, a class without `[MessagePackObject]`,
|
||||
a `[DataContract]` or string-keyed type, and a type or member declaring
|
||||
`[MessagePackFormatter]`. Its elements carry no element ids: a `[CultElementId]`
|
||||
member inside it is a plain string, never minted or checked, and a variant can only
|
||||
replace the holder whole.
|
||||
|
||||
```csharp
|
||||
[MessagePackObject]
|
||||
public sealed class Line
|
||||
|
|
|
|||
|
|
@ -357,6 +357,8 @@ namespace GameCult.Caching.Tests
|
|||
{
|
||||
Assert.That(Problems(typeof(HOverFormattedId)).Single(), Does.Contain("Id").And.Contain("[MessagePackFormatter]"));
|
||||
Assert.That(Problems(typeof(OwnerOverFormatted)), Is.Empty, "the override inherits its base's formatter, so the member is a whole value");
|
||||
Assert.That(CultElementIds.Inspect(typeof(OwnerOverFormatted)).IdPath, Is.Empty, "a walked override would put its list on the id path");
|
||||
Assert.That(Assign(new OwnerOverFormatted { Items = { new EIded() } }, "r", deterministic: false), Is.EqualTo(0), "and mint into a list its formatter may drop ids from");
|
||||
|
||||
Assert.That(Problems(typeof(HIdOverride)), Is.Empty, "a base [CultElementId] is the override's id");
|
||||
var root = new HIdOverride { Items = { new IdOverride() } };
|
||||
|
|
@ -544,7 +546,7 @@ namespace GameCult.Caching.Tests
|
|||
Assert.That(Problems(typeof(ApIgn)), Is.Empty, "an ignored private member is not written");
|
||||
Assert.That(Problems(typeof(PgHold)), Is.Empty, "a list behind a private getter is not written without AllowPrivate");
|
||||
|
||||
foreach (var type in new[] { typeof(EIded), typeof(HHiddenLeaf), typeof(PubIgn), typeof(ApIgn), typeof(PgHold), typeof(MemberMix), typeof(HOptHidden), typeof(ApDerived), typeof(NoApDerived), typeof(ApInheritor) })
|
||||
foreach (var type in new[] { typeof(EIded), typeof(HHiddenLeaf), typeof(PubIgn), typeof(ApIgn), typeof(PgHold), typeof(MemberMix), typeof(HOptHidden), typeof(ApDerived), typeof(NoApDerived), typeof(ApInheritor), typeof(ApIgnAp), typeof(PSkH), typeof(ISkH) })
|
||||
{
|
||||
var allowPrivate = type.GetCustomAttribute<MessagePackObjectAttribute>(true)!.AllowPrivate;
|
||||
Assert.That(CultElementIds.WrittenKeyedMembers(type).Select(member => member.Name),
|
||||
|
|
@ -661,6 +663,33 @@ namespace GameCult.Caching.Tests
|
|||
RefusesBare(typeof(Whole<List<EBare>>));
|
||||
}
|
||||
|
||||
// A string key counts only on a member MessagePack writes: a private or [IgnoreMember] one leaves the type integer-keyed and owned.
|
||||
[Test]
|
||||
public void AStringKeyOnAMemberMessagePackDoesNotWriteLeavesTheTypeOwned()
|
||||
{
|
||||
foreach (var holder in new object[] { new PSkH { Items = { new EIded(), new EIded() } }, new ISkH { Items = { new EIded(), new EIded() } } })
|
||||
{
|
||||
Assert.That(Problems(holder.GetType()), Is.Empty, holder.GetType().Name);
|
||||
Assert.That(CultElementIds.Inspect(holder.GetType()).IdPath, Does.Contain(holder.GetType()), holder.GetType().Name);
|
||||
Assert.That(Assign(holder, "r", deterministic: false), Is.EqualTo(2), holder.GetType().Name);
|
||||
Assert.That(CultElementIds.Holds(holder.GetType(), holder), Is.True, holder.GetType().Name);
|
||||
}
|
||||
|
||||
Assert.That(Problems(typeof(Whole<List<NoIdPlain>>)).Single(), Does.Contain("[CultElementId]"));
|
||||
Assert.That(Problems(typeof(Whole<List<NoIdEl>>)).Single(), Does.Contain("[CultElementId]"), "an element whose only string key is ignored is as bare as its twin");
|
||||
}
|
||||
|
||||
// The AllowPrivate edges: an [IgnoreMember] member is not written even under it, and only the element's own most derived
|
||||
// declaration lets a private setter be read back.
|
||||
[Test]
|
||||
public void TheAllowPrivateEdgesFollowWhatMessagePackWrites()
|
||||
{
|
||||
Assert.That(CultElementIds.WrittenKeyedMembers(typeof(ApIgnAp)), Is.Empty, "an ignored member is not written under AllowPrivate");
|
||||
Assert.That(Problems(typeof(HApIgnId)).Single(), Does.Contain("does not write").And.Contain("Id"));
|
||||
Assert.That(Problems(typeof(HNoApPrivSet)).Single(), Does.Contain("setter is not public").And.Contain("Id"),
|
||||
"the derived plain [MessagePackObject] decides: its private setter is not read back, whatever its base declares");
|
||||
}
|
||||
|
||||
// MessagePack writes an AllowPrivate type with every member up its base chain, and the most derived [MessagePackObject]
|
||||
// declaration decides whether that is so.
|
||||
[Test]
|
||||
|
|
@ -993,6 +1022,16 @@ namespace GameCult.Caching.Tests
|
|||
public class ApInheritor : ApDerived { }
|
||||
[MessagePackObject] public class SkElem { [Key("x")] public string X { get; set; } = ""; }
|
||||
[System.Runtime.Serialization.DataContract] public class DcHold { [System.Runtime.Serialization.DataMember(Order = 0)] public List<EIded> Items = new(); }
|
||||
[MessagePackObject(AllowPrivate = true)] public class ApIgnAp { [Key(0)] [IgnoreMember] private List<EIded> l = new(); }
|
||||
[MessagePackObject] public class PSkH { [Key(0)] public List<EIded> Items = new(); [Key("x")] private int x = 1; }
|
||||
[MessagePackObject] public class ISkH { [Key(0)] public List<EIded> Items = new(); [IgnoreMember] [Key("x")] public int X { get; set; } }
|
||||
[MessagePackObject] public class NoIdEl { [Key(0)] public int N { get; set; } [IgnoreMember] [Key("x")] public int X { get; set; } }
|
||||
[MessagePackObject] public class NoIdPlain { [Key(0)] public int N { get; set; } }
|
||||
[MessagePackObject(AllowPrivate = true)] public class EApIgnId { [Key(0)] public int N { get; set; } [Key(1)] [IgnoreMember] [CultElementId] public string Id { get; set; } = ""; }
|
||||
[MessagePackObject] public class HApIgnId { [Key(0)] public List<EApIgnId> Items { get; set; } = new(); }
|
||||
[MessagePackObject(AllowPrivate = true)] public class ApIdBase { [Key(0)] public int N { get; set; } }
|
||||
[MessagePackObject] public class ENoApPrivSet : ApIdBase { [Key(1)] [CultElementId] public string? Id { get; private set; } }
|
||||
[MessagePackObject] public class HNoApPrivSet { [Key(0)] public List<ENoApPrivSet> Items { get; set; } = new(); }
|
||||
[MessagePackObject(AllowPrivate = true)] public class EApPrivSet { [Key(0)] [CultElementId] public string? Id { get; private set; } }
|
||||
[MessagePackObject] public class HApPrivSet { [Key(0)] public List<EApPrivSet> Items { get; set; } = new(); }
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue