diff --git a/src/GameCult.Caching/CultElementIds.cs b/src/GameCult.Caching/CultElementIds.cs index 81fada0a..8e10198b 100644 --- a/src/GameCult.Caching/CultElementIds.cs +++ b/src/GameCult.Caching/CultElementIds.cs @@ -290,7 +290,7 @@ namespace GameCult.Caching continue; if (shape.Derive == null && !IsRandomId(id!)) throw new CultElementIdException( - $"Record {rootPath}: element id '{id}' in the list at {path} is not 12 lowercase hex characters.", + $"Record {rootPath}: element id '{id}' in the list at {path} is not 12 lowercase hex characters. An id you write by hand must be 12 lowercase hex; for readable ids, derive them from a member with [CultElementId(nameof(Member))].", rootPath, path, id); if (!taken.Add(id!)) throw Duplicate(id!, path); diff --git a/src/GameCult.Caching/README.md b/src/GameCult.Caching/README.md index e9edb63d..6e47742f 100644 --- a/src/GameCult.Caching/README.md +++ b/src/GameCult.Caching/README.md @@ -188,6 +188,31 @@ for (var row = 0; row < table.Count; row++) cache-owned physical table for hot loops and diagnostics; it is not a second domain model callers have to maintain. +## Element ids + +A document type that holds a list of objects gives every object type in that list +(union arms included) a string member marked `[CultElementId]`, next to its +`[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. + +```csharp +[MessagePackObject] +public sealed class Line +{ + [Key(0)] [CultElementId] public string Id = string.Empty; + [Key(1)] public string Text = string.Empty; +} +``` + +Leave `Id` empty and the cache mints a random id when the record is written. An +id you set yourself must be exactly 12 lowercase hex characters +(`0123456789ab`); anything else is refused with a `CultElementIdException`. + +For readable ids, derive them: `[CultElementId(nameof(Name))]` makes an unset id +the text of that member (a string or number on the same type, unique within its +list), and a derived id may be any text. A readable name set by hand on a +non-derived list id is refused; the same member outside a list is unconstrained. + ## Persistence discipline Persistence stays manual by default. Mutations mark the cache and attached diff --git a/tests/GameCult.Caching.Tests/ElementIdEngineTests.cs b/tests/GameCult.Caching.Tests/ElementIdEngineTests.cs index dae82cee..7ae2ee20 100644 --- a/tests/GameCult.Caching.Tests/ElementIdEngineTests.cs +++ b/tests/GameCult.Caching.Tests/ElementIdEngineTests.cs @@ -135,6 +135,13 @@ namespace GameCult.Caching.Tests Assert.Throws(() => Assign(grid, "r", deterministic: false)); } + [Test] + public void TheHexRefusalNamesTheDerivedFormForReadableIds() + { + var refusal = Assert.Throws(() => Assign(new HHasIds { Items = { new EIded { Id = "root" } } }, "r", deterministic: false))!; + Assert.That(refusal.Message, Does.Contain("12 lowercase hex").And.Contain("[CultElementId(nameof(Member))]")); + } + [Test] public void ARandomIdMustBeTwelveLowercaseHexCharactersAndADerivedIdNeedOnlyBeText() {