Seat definition format: states replace records (templates, writer, key forms with value schemas; ADR 0608 §1, ADR 0634 §6; issue 1966)

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
2026-11-05 12:47:26 +01:00
parent 863b909504
commit 651c28e70d
2 changed files with 175 additions and 18 deletions
+128 -16
View File
@@ -28,7 +28,7 @@ const MaxSize = 256 << 10
const Format = 1
// The two grains a definition is written at (ADR 0613 §1, §2; ADR 0620 §1): whole, with every verb's messages, its
// events and their bodies, its records and its provisions' forms; or the seat list's, holding what mesh-sdk's
// events and their bodies, its states and its provisions' forms; or the seat list's, holding what mesh-sdk's
// compiled seat list held, until its area's contract next changes.
const (
Whole = "whole"
@@ -74,8 +74,9 @@ type Definition struct {
// HolderEvents are the holder's own events that belong with the seat, listed beside it and never moved onto
// its subject (ADR 0620 §2): they keep the holder's subjects.
HolderEvents []HolderEvent `json:"holder-events,omitempty"`
// Records are the states and records the holder keeps for the seat.
Records []Record `json:"records,omitempty"`
// States are the layouts of the state the seat's holder keeps on the bus, and the callers' writes into it (ADR
// 0608 §1, ADR 0634 §6).
States []State `json:"states,omitempty"`
// Provisions are the forms of the provisions the seat's holder answers.
Provisions []Provision `json:"provisions,omitempty"`
// Receives and Paths are the seat list's contributions and owned paths.
@@ -129,11 +130,48 @@ type Family struct {
// EventsStream, whose test holds the two equal).
const SharedEventsMaxAgeDays = 7
// Record is one state the holder keeps for the seat: its key's form and its value's schema.
type Record struct {
Name string `json:"name"`
Key string `json:"key,omitempty"`
Value map[string]any `json:"value,omitempty"`
// The templates a state is kept in (ADR 0595 §2): the same six names mesh-sdk's catalogue gives a module's `requires`
// entry; a unit test holds the two lists equal. This package imports no catalogue.
const (
TemplateState = "state"
TemplateEvents = "events"
TemplateWorkQueue = "work-queue"
TemplateLog = "log"
TemplateObjects = "objects"
TemplateLedger = "ledger"
)
// Templates are the six, in the order ADR 0595 gives them.
var Templates = []string{TemplateState, TemplateEvents, TemplateWorkQueue, TemplateLog, TemplateObjects, TemplateLedger}
// The writers of a state: the seat's holder (the default), or its callers, each writing its own key.
const (
WriterHolder = "holder"
WriterCaller = "caller"
)
// State is one state of the seat, by its local name: the holder's name is never in it, since a contract names no
// holder (ADR 0625 §4, §7); each reader takes the holder from its own manifest's `reads` entry. A state is kept in
// one of the templates, and its keys have one form each, with the schema of the value under it.
type State struct {
Name string `json:"name"`
Template string `json:"template"`
// Writer is WriterHolder when empty.
Writer string `json:"writer,omitempty"`
KeyForms []KeyForm `json:"key-forms"`
// History is how many past values a TemplateState's key keeps; MaxAgeDays is how long a stream template
// (events, work queue, log) keeps a message. Neither is given where the template has no such setting.
History int `json:"history,omitempty"`
MaxAgeDays int `json:"max-age-days,omitempty"`
Description string `json:"description,omitempty"`
}
// KeyForm is one form a state's key takes: a pattern of named parts (`<asker>.<id>`, a part between angle brackets,
// the rest literal) and the JSON Schema of the value kept under keys of that form.
type KeyForm struct {
Key string `json:"key"`
Value map[string]any `json:"value"`
Description string `json:"description,omitempty"`
}
// Provision is the form of a provision the seat's holder answers.
@@ -271,7 +309,7 @@ func ParseFixtures(raw []byte) ([]Fixture, error) {
}
if _, _, ok := f.Instance(); !ok {
ps = append(ps, Problem{where + ".of", fmt.Sprintf("%q is none of verb:<name>.input, verb:<name>.answer, "+
"accept:<name>, event:<name>, holder-event:<name>", f.Of)})
"accept:<name>, event:<name>, holder-event:<name>, state:<name>/<key form>", f.Of)})
}
if f.Given == nil {
ps = append(ps, Problem{where + ".given", "missing"})
@@ -292,6 +330,11 @@ func (f Fixture) Instance() (kind, name string, ok bool) {
switch k {
case "accept", "event", "holder-event":
return k, rest, true
case "state":
// state:<name>/<key form>: one value of the state under that key form.
if n, form, ok := strings.Cut(rest, "/"); ok && n != "" && form != "" {
return "state", rest, true
}
case "verb":
if n, ok := strings.CutSuffix(rest, ".input"); ok && n != "" {
return "verb-input", n, true
@@ -356,10 +399,10 @@ func (d Definition) check() Problems {
ps = append(ps, Problem{"decision", "missing"})
}
whole := d.Grain == Whole
if !whole && (len(d.Includes) > 0 || d.Family != nil || len(d.HolderEvents) > 0 || len(d.Records) > 0 ||
if !whole && (len(d.Includes) > 0 || d.Family != nil || len(d.HolderEvents) > 0 || len(d.States) > 0 ||
len(d.Provisions) > 0 || len(d.Defs) > 0) {
ps = append(ps, Problem{"grain", "seat-list holds the seat list's fields only; includes, family, " +
"holder-events, records, provisions and defs are a whole definition's"})
"holder-events, states, provisions and defs are a whole definition's"})
}
if d.HolderDeclares && !whole {
ps = append(ps, Problem{"holder-declares", "only a whole definition describes a seat its holder declares"})
@@ -403,11 +446,7 @@ func (d Definition) check() Problems {
ps = append(ps, Problem{where + ".subject", fmt.Sprintf("not %s, nor %s for an event kept in the holder's own stream", own, stream)})
}
}
for i, r := range d.Records {
if !wordName.MatchString(r.Name) {
ps = append(ps, Problem{fmt.Sprintf("records[%d].name", i), "missing, or not a lower-case name"})
}
}
ps = append(ps, checkStates(d.States)...)
for i, p := range d.Provisions {
where := fmt.Sprintf("provisions[%d]", i)
if p.Name == "" {
@@ -430,6 +469,79 @@ func (d Definition) check() Problems {
return ps
}
var (
keyPart = regexp.MustCompile(`<[^<>]*>`)
keyPartOK = regexp.MustCompile(`^<[a-z][a-z0-9-]*>$`)
keyLiteral = regexp.MustCompile(`^[A-Za-z0-9_.\-/=:]*$`)
)
func checkStates(ss []State) Problems {
var ps Problems
seen := map[string]bool{}
for i, s := range ss {
where := fmt.Sprintf("states[%d]", i)
if !wordName.MatchString(s.Name) {
ps = append(ps, Problem{where + ".name", "missing, or not a lower-case name"})
} else if seen[s.Name] {
ps = append(ps, Problem{where + ".name", fmt.Sprintf("%q twice", s.Name)})
}
seen[s.Name] = true
known := false
for _, t := range Templates {
known = known || s.Template == t
}
switch {
case s.Template == "":
ps = append(ps, Problem{where + ".template", "missing"})
case !known:
ps = append(ps, Problem{where + ".template", fmt.Sprintf("%q is none of %s", s.Template, strings.Join(Templates, ", "))})
}
if s.Writer != "" && s.Writer != WriterHolder && s.Writer != WriterCaller {
ps = append(ps, Problem{where + ".writer", fmt.Sprintf("%q is neither holder nor caller", s.Writer)})
}
if s.History < 0 || (s.History > 0 && s.Template != TemplateState) {
ps = append(ps, Problem{where + ".history", "a count of past values, of a state template only"})
}
stream := s.Template == TemplateEvents || s.Template == TemplateWorkQueue || s.Template == TemplateLog
if s.MaxAgeDays < 0 || (s.MaxAgeDays > 0 && !stream) {
ps = append(ps, Problem{where + ".max-age-days", "days, of an events, work-queue or log template only"})
}
if len(s.KeyForms) == 0 {
ps = append(ps, Problem{where + ".key-forms", "missing: a state gives at least one key form"})
}
forms := map[string]bool{}
for j, f := range s.KeyForms {
at := fmt.Sprintf("%s.key-forms[%d]", where, j)
if f.Key == "" {
ps = append(ps, Problem{at + ".key", "missing"})
} else if bad := badKey(f.Key); bad != "" {
ps = append(ps, Problem{at + ".key", bad})
} else if forms[f.Key] {
ps = append(ps, Problem{at + ".key", fmt.Sprintf("%q twice", f.Key)})
}
forms[f.Key] = true
if f.Value == nil {
ps = append(ps, Problem{at + ".value", fmt.Sprintf("missing: key form %q gives its value's schema", f.Key)})
}
}
}
return ps
}
// badKey says what is wrong with a key pattern: a part between angle brackets that is not a lower-case name, or a
// literal outside what a bus key may hold.
func badKey(key string) string {
for _, part := range keyPart.FindAllString(key, -1) {
if !keyPartOK.MatchString(part) {
return fmt.Sprintf("part %s is not a lower-case name between < and >", part)
}
}
if lit := keyPart.ReplaceAllString(key, ""); !keyLiteral.MatchString(lit) || strings.ContainsAny(lit, "<>") {
return fmt.Sprintf("%q holds a character a key may not", key)
}
return ""
}
func checkVerbs(vs []Verb, whole bool) Problems {
var ps Problems
seen := map[string]bool{}
+47 -2
View File
@@ -4,6 +4,8 @@ import (
"errors"
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
// TestADefinitionIsReadStrictlyNamingWhatIsWrong is the format's own table (design 26: a valid definition parses;
@@ -50,8 +52,27 @@ func TestADefinitionIsReadStrictlyNamingWhatIsWrong(t *testing.T) {
{"a holder's event on a subject of no module", strings.Replace(whole, `"family":`, `"holder-events":[{"name":"verdict",`+
`"subject":"mesh.event.verdict"}],"family":`, 1), "holder-events[0].subject: not mesh.mod.<holder>.event.verdict"},
{"a verb twice", strings.Replace(listGrain, `]}`, `,{"name":"add"}]}`, 1), `"add" twice`},
{"records at the seat list's grain", strings.Replace(listGrain, `"decision":"ADR 1",`,
`"decision":"ADR 1","records":[{"name":"asks"}],`, 1), "grain: seat-list holds"},
{"states at the seat list's grain", strings.Replace(listGrain, `"decision":"ADR 1",`,
`"decision":"ADR 1","states":[{"name":"asks"}],`, 1), "grain: seat-list holds"},
{"the old records field is gone", strings.Replace(whole, `"decision":"ADR 1",`,
`"decision":"ADR 1","records":[{"name":"asks"}],`, 1), "records: a field the format does not know"},
{"a state with a key form and a value", withState(whole, `{"name":"asks","template":"state","writer":"caller",
"history":3,"key-forms":[{"key":"<asker>.<id>","value":{"type":"object"}}]}`), ""},
{"a stream state with an age", withState(whole, `{"name":"placement","template":"events","max-age-days":7,
"key-forms":[{"key":"changed.<node>","value":{"type":"object"}}]}`), ""},
{"a state of an unknown template", withState(whole, `{"name":"asks","template":"bucket",
"key-forms":[{"key":"<id>","value":{"type":"object"}}]}`), `states[0].template: "bucket" is none of state, events`},
{"a key form with no value", withState(whole, `{"name":"asks","template":"state","key-forms":[{"key":"<id>"}]}`),
`states[0].key-forms[0].value: missing: key form "<id>"`},
{"a state with no key form", withState(whole, `{"name":"asks","template":"state"}`), "states[0].key-forms: missing"},
{"a key part that is no name", withState(whole, `{"name":"asks","template":"state",
"key-forms":[{"key":"<Asker>.<id>","value":{}}]}`), "part <Asker> is not a lower-case name"},
{"a state written by neither", withState(whole, `{"name":"asks","template":"state","writer":"anyone",
"key-forms":[{"key":"<id>","value":{}}]}`), "states[0].writer"},
{"history of a stream", withState(whole, `{"name":"asks","template":"log","history":2,
"key-forms":[{"key":"<id>","value":{}}]}`), "states[0].history"},
{"a state twice", withState(withState(whole, `{"name":"asks","template":"state","key-forms":[{"key":"<id>","value":{}}]}`),
`{"name":"asks","template":"log","key-forms":[{"key":"<id>","value":{}}]}`), `"asks" twice`},
{"over 256 KiB", listGrain[:len(listGrain)-1] + `,"description":"` + strings.Repeat("x", MaxSize) + `"}`,
ErrTooLarge.Error()},
}
@@ -106,6 +127,12 @@ func TestADefinitionIsReadStrictlyNamingWhatIsWrong(t *testing.T) {
"subject":"mesh.seat.mesh-broker.event.created","given":{}}]`)); err != nil {
t.Errorf("fixtures: %v", err)
}
if _, err := ParseFixtures([]byte(`[{"name":"a","of":"state:asks/<asker>.<id>","given":{}}]`)); err != nil {
t.Errorf("a state's fixture: %v", err)
}
if _, err := ParseFixtures([]byte(`[{"name":"a","of":"state:asks","given":{}}]`)); err == nil {
t.Error("a fixture of a state with no key form parsed")
}
if _, err := ParseFixtures([]byte(`[{"name":"a","of":"verb:list","given":{}}]`)); err == nil ||
!strings.Contains(err.Error(), "fixtures[0].of") {
t.Errorf("a fixture of nothing known: %v", err)
@@ -114,3 +141,21 @@ func TestADefinitionIsReadStrictlyNamingWhatIsWrong(t *testing.T) {
t.Error("an index naming a seat twice parsed")
}
}
// withState adds one state to a whole definition's JSON.
func withState(def, state string) string {
if strings.Contains(def, `"states":[`) {
return strings.Replace(def, `"states":[`, `"states":[`+state+`,`, 1)
}
return strings.Replace(def, `"decision":"ADR 1",`, `"decision":"ADR 1","states":[`+state+`],`, 1)
}
// TestTheTemplatesAreTheCataloguesSix holds this reader's list of templates equal to the one a module's `requires`
// entry takes (ADR 0595 §2), so a seventh template cannot be added to one and not the other.
func TestTheTemplatesAreTheCataloguesSix(t *testing.T) {
want := []string{catalogue.TemplateState, catalogue.TemplateEvents, catalogue.TemplateWorkQueue,
catalogue.TemplateLog, catalogue.TemplateObjects, catalogue.TemplateLedger}
if strings.Join(Templates, ",") != strings.Join(want, ",") {
t.Errorf("templates %v, the catalogue's %v", Templates, want)
}
}