CultLib/src/GameCult.Caching/CultVariants.cs
Meta Vens c66d79e248 Variant overrides follow the plain-record outcome; bad writes and shared indexes are refused
F3: an override drifts exactly as the same member would on a plain record. A
slot the type dropped is ignored and reported by the store's schema resolution
(LastSchemaMigrationReports), the one surface a plain record uses; a value that
no longer decodes refuses the load, naming the variant and member. The second
report surface (VariantMigrationReports, the per-variant report map, the
catalog slot-name plumbing) is deleted.

F4: an override for a slot the type does not have, or a value that does not
decode as the member, is refused at write. Drift is for type changes after the
fact, not for bad writes. F5: two overrides of one slot are refused.

F2: a variant that shares an indexed value with another record is refused,
naming both keys and the index (the R6 rule extended to [CultIndex]); plain
record duplicate handling is unchanged.

F7: a flatten now resolves against the same batch, so a base edited in that
batch is in the flattened record; and the directory store refuses to load a
variant page as it refuses to write one.

The C1 vector's big variant now overrides its indexed code, as the unique-index
rule requires.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 06:58:51 +02:00

91 lines
3.6 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
namespace GameCult.Caching
{
// The store's encoding, as the cache needs it: the one pair CultInspectorModel and variant resolution share.
// Serialize takes any value, not only documents, under its owning document type's options; Deserialize is its
// inverse (a document type is checked against the registry, a member type is not); Overlay replaces slots of a
// serialized document payload with already-encoded member values and touches nothing else.
public sealed class CultCodec
{
public CultCodec(
Func<object, Type, Type, byte[]> serialize,
Func<Type, Type, byte[], object?> deserialize,
Func<byte[], IReadOnlyList<KeyValuePair<int, byte[]>>, byte[]> overlay)
{
Serialize = serialize ?? throw new ArgumentNullException(nameof(serialize));
Deserialize = deserialize ?? throw new ArgumentNullException(nameof(deserialize));
Overlay = overlay ?? throw new ArgumentNullException(nameof(overlay));
}
// (value, value type, owning document type)
public Func<object, Type, Type, byte[]> Serialize { get; }
// (value type, owning document type, bytes)
public Func<Type, Type, byte[], object?> Deserialize { get; }
// (document payload, slot and encoded value pairs)
public Func<byte[], IReadOnlyList<KeyValuePair<int, byte[]>>, byte[]> Overlay { get; }
}
public enum CultOverrideOp
{
Set = 0,
Insert = 1,
Remove = 2
}
// One step of a member path: the MessagePack slot, and the id of the object-list element the step enters ("" when it
// does not enter one).
public sealed class CultPathStep
{
public CultPathStep(int slot, string elementId = "")
{
Slot = slot;
ElementId = elementId ?? string.Empty;
}
public int Slot { get; }
public string ElementId { get; }
public override string ToString() => ElementId.Length == 0 ? $"[{Slot}]" : $"[{Slot}:{ElementId}]";
}
// The wire shape [op, path[], id, value]; value is the override's already-encoded MessagePack value.
public sealed class CultVariantOverride
{
public CultVariantOverride(CultOverrideOp op, IReadOnlyList<CultPathStep> path, string id, byte[] value)
{
Op = op;
Path = path ?? throw new ArgumentNullException(nameof(path));
Id = id ?? string.Empty;
Value = value ?? throw new ArgumentNullException(nameof(value));
}
public CultOverrideOp Op { get; }
public IReadOnlyList<CultPathStep> Path { get; }
public string Id { get; }
public byte[] Value { get; }
// A top-level Set: the whole member at slot takes the encoded value.
public static CultVariantOverride Set(int slot, byte[] value) =>
new(CultOverrideOp.Set, new[] { new CultPathStep(slot) }, string.Empty, value);
internal string PathText => string.Concat(Path.Select(step => step.ToString()));
}
// What a variant record stores: its base and its overrides. Never the resolved document.
public sealed class CultVariantDelta
{
public CultVariantDelta(string baseKey, IReadOnlyList<CultVariantOverride> overrides)
{
BaseKey = baseKey ?? throw new ArgumentNullException(nameof(baseKey));
Overrides = overrides ?? throw new ArgumentNullException(nameof(overrides));
}
public string BaseKey { get; }
public IReadOnlyList<CultVariantOverride> Overrides { get; }
}
}