From 0bec849c40b5e9747fd3efd55cee007e61f2b952 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 13:20:35 +0100 Subject: [PATCH] Name the storage types, templates, options and ledger effects, and give a ledger its types (issue 1966 S2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The words of design 25 § 12.1 and ADR 0597 §1-2 as constants, and a ledger's types (schema, operations each with its effect, indexes) on the bus-state entry, so the judge, the bus module and the tool runner read one contract. No bound and no rule: those are the judge's (ADR 0578 §7). --- go/catalogue/busstate.go | 111 +++++++++++++++++++++ go/catalogue/optional_requirements.go | 2 +- go/catalogue/optional_requirements_test.go | 20 ++++ 3 files changed, 132 insertions(+), 1 deletion(-) diff --git a/go/catalogue/busstate.go b/go/catalogue/busstate.go index 865465c..21dc379 100644 --- a/go/catalogue/busstate.go +++ b/go/catalogue/busstate.go @@ -13,6 +13,115 @@ import ( // leniently. Every rule that judges an entry (a template known, an option within its bound, a fixed setting not given) // is the indexer's judge's. +// The storage types (design 25 § 12.1): the stream is the base; the key-value bucket, the object store and the ledger +// are built on it, each with calls of its own (ADR 0595 §2). +const ( + StorageStream = "stream" + StorageKeyValue = "key-value" + StorageObjectStore = "object-store" + StorageLedger = "ledger" +) + +// The six templates, each a storage type with a predefined configuration (ADR 0595 §2, design 25 § 12.1): state on +// key-value, events, work queue and log on stream, objects on object store, ledger on ledger. A bare stream names +// none and gives its own Configuration. +const ( + TemplateState = "state" + TemplateEvents = "events" + TemplateWorkQueue = "work-queue" + TemplateLog = "log" + TemplateObjects = "objects" + TemplateLedger = "ledger" +) + +// The option names, a template's settings a module may change (design 25 § 12.1's table), written as the old sections +// wrote them, so a module that moves carries its old settings as options unchanged (ADR 0595 §11). Which option a +// template takes, and its bound, is the judge's. +const ( + // OptionPastValues is how many past values a state's key keeps (the old `state`'s `history`). + OptionPastValues = "history" + // OptionTimeToLive is how long a state's value lives, in seconds. + OptionTimeToLive = "ttl-seconds" + // OptionLifetimePerKey is whether a state's key may be given a lifetime of its own. + OptionLifetimePerKey = "key-ttl" + // OptionPerMachine is whether a state is kept one bucket per machine. + OptionPerMachine = "per-machine" + // OptionAgeDays is how long a stream keeps a message, in days. + OptionAgeDays = "max-age-days" + // OptionCapMiB is the cap of a stream, a bucket or an object store, in MiB. + OptionCapMiB = "max-mib" + // OptionGrant names the trusted config value granting another module a part of it (ADR 0363 §4, ADR 0592 §9). + OptionGrant = "grant" + // OptionDeliveries is how many times a work queue's message is delivered before it is a dead letter. + OptionDeliveries = "max-deliveries" + // OptionDuplicates is how long, in seconds, a work queue detects a message sent twice. + OptionDuplicates = "duplicates" +) + +// ConfigWhenFull is what a configuration of its own says a bare stream does when full: WhenFullRefuse or +// WhenFullDropOldest. In a template it is fixed, never an option (ADR 0595 §3). +const ( + ConfigWhenFull = "when-full" + WhenFullRefuse = "refuse" + WhenFullDropOldest = "drop-oldest" +) + +// The ledger effects, what an operation does to an instance's state (ADR 0597 §2): one of this fixed set. +const ( + // EffectSetFields sets the state's fields to the operation's Fields. + EffectSetFields = "set-fields" + // EffectSetEntry sets one entry of the state's map Into, under the key the operation's field Key holds, to its Fields. + EffectSetEntry = "set-entry" + // EffectAppend appends the operation's Fields to the state's list Into. + EffectAppend = "append" + // EffectRemove removes the entry of the map Into under the operation's field Key, or, naming no Into, the instance's + // state. + EffectRemove = "remove" +) + +// LedgerType is one type of a ledger (ADR 0597 §1): a schema and the operations allowed on it, with its indexes. An +// instance of it is named by a path whose first part is the type's name (`decision/0290`). +type LedgerType struct { + Name string `json:"name"` + // Schema is the type's fields, by name. + Schema map[string]LedgerField `json:"schema"` + // Operations are the operations allowed on an instance, each with its effect. + Operations []LedgerOperation `json:"operations"` + // Indexes are its declared indexes, each a second key-value bucket kept by a watcher (ADR 0597 §4). + Indexes []LedgerIndex `json:"indexes,omitempty"` +} + +// LedgerField is one field of a ledger type's schema. +type LedgerField struct { + // Type is the field's value type, as the schema names it. + Type string `json:"type"` + // DigestOf names the text field this field is the digest of (ADR 0597 §2): `apply` refuses an operation whose + // digest does not match its text. + DigestOf string `json:"digest-of,omitempty"` +} + +// LedgerOperation is one operation a ledger type allows, and its effect on the state. +type LedgerOperation struct { + Name string `json:"name"` + // Effect is one of the ledger effects (EffectSetFields, EffectSetEntry, EffectAppend, EffectRemove). + Effect string `json:"effect"` + // Fields are the operation's fields its effect copies into the state; it may carry more (ADR 0597 §2). + Fields []string `json:"fields,omitempty"` + // Into is the state's map (EffectSetEntry, EffectRemove) or list (EffectAppend) the effect writes. + Into string `json:"into,omitempty"` + // Key is the operation's field holding the entry's key (EffectSetEntry, EffectRemove). + Key string `json:"key,omitempty"` + // Sequence and Author are whether the effect also writes the operation's sequence and its proven author. + Sequence bool `json:"sequence,omitempty"` + Author bool `json:"author,omitempty"` +} + +// LedgerIndex is one declared index of a ledger type: its name, and the fields it is kept on. +type LedgerIndex struct { + Name string `json:"name"` + On []string `json:"on"` +} + // BusStateField is where a stored manifest keeps its bus-state entries: beside `requires`, which stays a plain list of // names, as OptionalRequiresKey keeps the optional ones. const BusStateField = "bus-state" @@ -31,6 +140,8 @@ type BusStateEntry struct { // Configuration is a configuration of the module's own, where no template fits: a bare stream's, or a ledger's // whose history expires (ADR 0595 §2). Configuration map[string]json.RawMessage `json:"configuration,omitempty"` + // Types are a ledger's types (ADR 0597 §1), for the storage type ledger only. + Types []LedgerType `json:"types,omitempty"` } // isBusStateEntry is whether a `requires` item in its long form asks for bus state: it names a storage type. diff --git a/go/catalogue/optional_requirements.go b/go/catalogue/optional_requirements.go index 55505b4..a4d0c61 100644 --- a/go/catalogue/optional_requirements.go +++ b/go/catalogue/optional_requirements.go @@ -53,7 +53,7 @@ func readRequires(raw json.RawMessage) (requiresRead, error) { if isBusStateEntry(e) { entry, unknown, err := readBusStateEntry(e) if err != nil { - return requiresRead{}, fmt.Errorf("requires[%d]: a bus-state entry is {name, storage, template, options, configuration} (novox/hq ADR 0595 §1): %w", i, err) + return requiresRead{}, fmt.Errorf("requires[%d]: a bus-state entry is {name, storage, template, options, configuration, types} (novox/hq ADR 0595 §1): %w", i, err) } names = append(names, entry.Name) busState = append(busState, entry) diff --git a/go/catalogue/optional_requirements_test.go b/go/catalogue/optional_requirements_test.go index 1475676..d0e7e32 100644 --- a/go/catalogue/optional_requirements_test.go +++ b/go/catalogue/optional_requirements_test.go @@ -41,6 +41,23 @@ func TestARequirementsLongForm(t *testing.T) { records := BusStateEntry{Name: "records", Storage: "ledger", Template: "ledger", Options: map[string]json.RawMessage{"max-mib": json.RawMessage(`1024`)}} + // A ledger type with each of the four effects. + listing := LedgerType{Name: "listing", + Schema: map[string]LedgerField{"price": {Type: "number"}, "seen": {Type: "list"}, "notes": {Type: "map"}, + "text": {Type: "text"}, "digest": {Type: "text", DigestOf: "text"}}, + Operations: []LedgerOperation{ + {Name: "priced", Effect: EffectSetFields, Fields: []string{"price"}, Sequence: true, Author: true}, + {Name: "noted", Effect: EffectSetEntry, Fields: []string{"text", "digest"}, Into: "notes", Key: "title"}, + {Name: "seen", Effect: EffectAppend, Fields: []string{"at"}, Into: "seen"}, + {Name: "unnoted", Effect: EffectRemove, Into: "notes", Key: "title"}, + }, + Indexes: []LedgerIndex{{Name: "cheapest", On: []string{"price"}}}} + typeJSON, err := json.Marshal(listing) + if err != nil { + t.Fatal(err) + } + ledger := records + ledger.Types = []LedgerType{listing} for _, c := range []struct { name string requires string @@ -56,6 +73,9 @@ func TestARequirementsLongForm(t *testing.T) { {"a bus-state entry with a key it does not know", `[ {"name":"records","storage":"ledger","template":"ledger","colour":"red","options":{"max-mib":1024}}]`, "records", []BusStateEntry{records}, []string{"requires[0].colour"}}, + // 1966/AC3: a ledger type with each of the four effects round-trips through JSON unchanged. + {"a ledger with its type", `[{"name":"records","storage":"ledger","template":"ledger","options":{"max-mib":1024}, + "types":[` + string(typeJSON) + `]}]`, "records", []BusStateEntry{ledger}, nil}, } { got, err := decodeForTest([]byte(`{"module":"x","version":"1","requires":` + c.requires + `}`)) if err != nil {