From 7d05a39c0c184a23b8ce631859ee7fd3949d83f5 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 11:41:29 +0100 Subject: [PATCH] =?UTF-8?q?SD-M1:=20one=20reader=20of=20a=20seat's=20defin?= =?UTF-8?q?ition=20for=20every=20part=20(ADR=200608=20=C2=A71,=20design=20?= =?UTF-8?q?26)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The indexer keeps the definitions and the build modules generate from them; both read them through go/seatdef, which refuses a missing or unknown field by name, a later format and a definition over 256 KiB. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- go/seatdef/seatdef.go | 560 +++++++++++++++++++++++++++++++++++++ go/seatdef/seatdef_test.go | 104 +++++++ 2 files changed, 664 insertions(+) create mode 100644 go/seatdef/seatdef.go create mode 100644 go/seatdef/seatdef_test.go diff --git a/go/seatdef/seatdef.go b/go/seatdef/seatdef.go new file mode 100644 index 0000000..c915c93 --- /dev/null +++ b/go/seatdef/seatdef.go @@ -0,0 +1,560 @@ +// Package seatdef reads a seat's definition (novox/hq ADR 0608 §1, design 26 *A seat's definition*): one +// language-neutral file per seat, `seats//seat.json` in a contracts module, with the parts it includes from +// `parts/.json` and its conformance fixtures in `seats//fixtures/`. The format's own JSON Schema is +// checked in beside the definitions (mesh-contracts `format/seat-definition.schema.json`); this package is the one +// reader every part shares: the indexer, which keeps the definitions on the bus, and the build modules' generators, +// which write the Go and TypeScript types from them. It generates nothing and holds no seat's contract. +// +// **A definition is read strictly.** A key the format does not know is refused, naming it, since a misspelt key would +// describe a contract nobody serves; a newer format is a new number, which an older reader refuses by name. The +// messages a definition describes are read leniently by their readers (ADR 0608 §6): that is the generated code's +// rule, not this one's. +package seatdef + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "regexp" + "sort" + "strings" +) + +// MaxSize is the largest definition, or part, the mesh keeps (design 26: at most 256 KiB a definition). +const MaxSize = 256 << 10 + +// Format is the one format number this reader knows. +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 +// compiled seat list held, until its area's contract next changes. +const ( + Whole = "whole" + SeatList = "seat-list" +) + +// The two scopes a seat is held at. +const ( + ScopeMesh = "mesh" + ScopeNode = "node" +) + +// Definition is one seat's contract. +type Definition struct { + Format int `json:"format"` + Seat string `json:"seat"` + Scope string `json:"scope"` + Grain string `json:"grain"` + // Decision is the record that made it a seat. + Decision string `json:"decision"` + // Description says the role in one paragraph, for a person reading the definition. + Description string `json:"description,omitempty"` + // HolderDeclares says the seat is not in the mesh's compiled seat list: its holder's manifest declares it as a + // seat of its own (ADR 0118), and the definition is its contract all the same (design 26: operator-channel). + HolderDeclares bool `json:"holder-declares,omitempty"` + // Delivers is the provision the seat's holder answers for, or empty. + Delivers string `json:"delivers,omitempty"` + // Replicated, PulledByKind, Yields and HeldByTheEngine are the seat list's marks of the same names. + Replicated bool `json:"replicated,omitempty"` + PulledByKind bool `json:"pulled-by-kind,omitempty"` + Yields bool `json:"yields,omitempty"` + HeldByTheEngine bool `json:"held-by-the-engine,omitempty"` + // Includes names the parts whose verbs, events and types this definition takes whole, written once so the seats + // sharing them cannot drift (design 26: the four registry seats' common part). + Includes []string `json:"includes,omitempty"` + Verbs []Verb `json:"verbs,omitempty"` + // Accepts are what may be sent to the seat's holder on `mesh.seat..accept.`. + Accepts []Message `json:"accepts,omitempty"` + // Events are what is published on the seat's own subject, `mesh.seat..event.`. + Events []Message `json:"events,omitempty"` + // Family is where the seat's events are kept, when the definition fixes it. + Family *Family `json:"family,omitempty"` + // 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"` + // 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. + Receives []Receivable `json:"receives,omitempty"` + Paths []Path `json:"paths,omitempty"` + // Defs are named types a schema refers to as `{"$ref": "#/defs/"}`. + Defs map[string]map[string]any `json:"defs,omitempty"` +} + +// Verb is one verb the seat serves: its input and, written whole, its answer, each a JSON Schema. +type Verb struct { + Name string `json:"name"` + Description string `json:"description,omitempty"` + // Optional marks a verb a holder may not serve yet and still hold the seat (design 33 §7). + Optional bool `json:"optional,omitempty"` + // Replaces are the shell commands this verb is the mesh's way to do (ADR 0245). + Replaces []string `json:"replaces,omitempty"` + Input map[string]any `json:"input,omitempty"` + Answer map[string]any `json:"answer,omitempty"` + // Callers says who may call it, in words: the list itself is the holder's trusted config value (design 51). + Callers string `json:"callers,omitempty"` +} + +// Message is one accepted message or one event of the seat, its body a JSON Schema. +type Message struct { + Name string `json:"name"` + Body map[string]any `json:"body,omitempty"` + // ByCaller says the subject ends in the name of who sent it (or, for an event, of who it is for). + ByCaller bool `json:"by-caller,omitempty"` +} + +// HolderEvent is one of the holder's own events listed beside the seat. +type HolderEvent struct { + Name string `json:"name"` + // Subject is where it is published, with `` for the holding module's name. + Subject string `json:"subject"` + // Family is the stream or family it is kept in, when it is not the shared module events stream. + Family string `json:"family,omitempty"` + Body map[string]any `json:"body,omitempty"` +} + +// Family is where a seat's events are kept. +type Family struct { + Name string `json:"name"` + MaxAgeDays int `json:"max-age-days"` + MaxMiB int `json:"max-mib,omitempty"` +} + +// 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"` +} + +// Provision is the form of a provision the seat's holder answers. +type Provision struct { + Name string `json:"name"` + Form map[string]any `json:"form"` +} + +// Receivable is one kind of contribution the seat receives (ADR 0212), as the seat list has it. +type Receivable struct { + Kind string `json:"kind"` + Comment string `json:"comment,omitempty"` + Dirs bool `json:"dirs,omitempty"` + Offered bool `json:"offered,omitempty"` + Shape *Shape `json:"shape,omitempty"` +} + +// Shape is a contribution kind given as data (ADR 0255). Its checks are code, the runtime's, never the definition's. +type Shape struct { + Places []PlaceField `json:"places,omitempty"` + Order string `json:"order,omitempty"` + Examples []map[string]any `json:"examples,omitempty"` + Text bool `json:"text,omitempty"` +} + +// PlaceField is one field a holder places by, and the values it takes. +type PlaceField struct { + Field string `json:"field"` + Values []string `json:"values,omitempty"` +} + +// Path is one path on its machine the seat's holder owns (ADR 0287). +type Path struct { + Path string `json:"path"` + StepsBackFor string `json:"steps-back-for,omitempty"` +} + +// Part is verbs, events and types several definitions include whole. +type Part struct { + Format int `json:"format"` + Part string `json:"part"` + Description string `json:"description,omitempty"` + Verbs []Verb `json:"verbs,omitempty"` + Events []Message `json:"events,omitempty"` + Defs map[string]map[string]any `json:"defs,omitempty"` +} + +// Fixture is one conformance case of a definition (design 26: data, never functions): a message as given, the +// subject it travels on, and, when it differs from what was given, what a reader must read back. +type Fixture struct { + Name string `json:"name"` + // Of is what the fixture is an instance of: `verb:.input`, `verb:.answer`, `accept:`, + // `event:` or `holder-event:`. + Of string `json:"of"` + Subject string `json:"subject,omitempty"` + Given any `json:"given"` + // Wire is what a reader reads back when it is not Given: a field it does not know dropped (ADR 0608 §6). + Wire any `json:"wire,omitempty"` + Why string `json:"why,omitempty"` +} + +// Problem is one thing wrong with a definition, naming where. +type Problem struct { + Where string + What string +} + +func (p Problem) Error() string { return p.Where + ": " + p.What } + +// Problems is every problem a definition has, in the order found. +type Problems []Problem + +func (ps Problems) Error() string { + out := make([]string, len(ps)) + for i, p := range ps { + out[i] = p.Error() + } + return strings.Join(out, "; ") +} + +var ( + seatName = regexp.MustCompile(`^[a-z][a-z0-9-]*[a-z0-9]$`) + wordName = regexp.MustCompile(`^[a-z][a-z0-9-]*$`) + // eventName is a holder's event: its own words, dot-separated as a module's event keys are (preview.written). + eventName = regexp.MustCompile(`^[a-z][a-z0-9-]*(\.[a-z][a-z0-9-]*)*$`) +) + +// ErrTooLarge is a definition or part over MaxSize. +var ErrTooLarge = errors.New("larger than 256 KiB, the most a seat's definition may be (design 26)") + +// Parse reads one seat's definition and checks it, naming every field that is missing or wrong. +func Parse(raw []byte) (Definition, error) { + var d Definition + if err := strict(raw, &d); err != nil { + return Definition{}, err + } + if ps := d.check(); len(ps) > 0 { + return Definition{}, ps + } + return d, nil +} + +// ParsePart reads one part. +func ParsePart(raw []byte) (Part, error) { + var p Part + if err := strict(raw, &p); err != nil { + return Part{}, err + } + var ps Problems + if p.Format != Format { + ps = append(ps, Problem{"format", formatProblem(p.Format)}) + } + if !wordName.MatchString(p.Part) { + ps = append(ps, Problem{"part", "missing, or not a lower-case name"}) + } + ps = append(ps, checkVerbs(p.Verbs, true)...) + ps = append(ps, checkMessages("events", p.Events, true)...) + if len(ps) > 0 { + return Part{}, ps + } + return p, nil +} + +// ParseFixtures reads a fixtures file: a list of cases. +func ParseFixtures(raw []byte) ([]Fixture, error) { + var fs []Fixture + if err := strict(raw, &fs); err != nil { + return nil, err + } + var ps Problems + for i, f := range fs { + where := fmt.Sprintf("fixtures[%d]", i) + if f.Name == "" { + ps = append(ps, Problem{where + ".name", "missing"}) + } + if _, _, ok := f.Instance(); !ok { + ps = append(ps, Problem{where + ".of", fmt.Sprintf("%q is none of verb:.input, verb:.answer, "+ + "accept:, event:, holder-event:", f.Of)}) + } + if f.Given == nil { + ps = append(ps, Problem{where + ".given", "missing"}) + } + } + if len(ps) > 0 { + return nil, ps + } + return fs, nil +} + +// Instance splits a fixture's Of into its kind (verb-input, verb-answer, accept, event, holder-event) and name. +func (f Fixture) Instance() (kind, name string, ok bool) { + k, rest, found := strings.Cut(f.Of, ":") + if !found || rest == "" { + return "", "", false + } + switch k { + case "accept", "event", "holder-event": + return k, rest, true + case "verb": + if n, ok := strings.CutSuffix(rest, ".input"); ok && n != "" { + return "verb-input", n, true + } + if n, ok := strings.CutSuffix(rest, ".answer"); ok && n != "" { + return "verb-answer", n, true + } + } + return "", "", false +} + +func strict(raw []byte, into any) error { + if len(raw) > MaxSize { + return ErrTooLarge + } + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(into); err != nil { + if f, ok := strings.CutPrefix(err.Error(), "json: unknown field "); ok { + return Problem{strings.Trim(f, `"`), "a field the format does not know"} + } + return fmt.Errorf("not a seat definition's JSON: %w", err) + } + if dec.More() { + return errors.New("not a seat definition's JSON: more than one value") + } + return nil +} + +func formatProblem(n int) string { + if n == 0 { + return "missing" + } + return fmt.Sprintf("format %d, and this reader knows format %d only", n, Format) +} + +func (d Definition) check() Problems { + var ps Problems + if d.Format != Format { + ps = append(ps, Problem{"format", formatProblem(d.Format)}) + } + if d.Seat == "" { + ps = append(ps, Problem{"seat", "missing"}) + } else if !seatName.MatchString(d.Seat) { + ps = append(ps, Problem{"seat", fmt.Sprintf("%q is not a lower-case name", d.Seat)}) + } + switch d.Scope { + case ScopeMesh, ScopeNode: + case "": + ps = append(ps, Problem{"scope", "missing"}) + default: + ps = append(ps, Problem{"scope", fmt.Sprintf("%q is neither mesh nor node", d.Scope)}) + } + switch d.Grain { + case Whole, SeatList: + case "": + ps = append(ps, Problem{"grain", "missing"}) + default: + ps = append(ps, Problem{"grain", fmt.Sprintf("%q is neither whole nor seat-list", d.Grain)}) + } + if d.Decision == "" { + 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 || + 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"}) + } + if d.HolderDeclares && !whole { + ps = append(ps, Problem{"holder-declares", "only a whole definition describes a seat its holder declares"}) + } + for i, inc := range d.Includes { + if !wordName.MatchString(inc) { + ps = append(ps, Problem{fmt.Sprintf("includes[%d]", i), fmt.Sprintf("%q is not a part's name", inc)}) + } + } + ps = append(ps, checkVerbs(d.Verbs, whole)...) + ps = append(ps, checkMessages("accepts", d.Accepts, whole)...) + ps = append(ps, checkMessages("events", d.Events, whole)...) + if whole && len(d.Events) > 0 && d.Family == nil { + ps = append(ps, Problem{"family", "missing: a whole definition with events says where they are kept"}) + } + if d.Family != nil { + if d.Family.Name == "" { + ps = append(ps, Problem{"family.name", "missing"}) + } + if d.Family.MaxAgeDays < 1 { + ps = append(ps, Problem{"family.max-age-days", "missing, or under one day"}) + } + } + for i, e := range d.HolderEvents { + where := fmt.Sprintf("holder-events[%d]", i) + if !eventName.MatchString(e.Name) { + ps = append(ps, Problem{where + ".name", "missing, or not lower-case words joined by dots"}) + } + if e.Subject == "" { + ps = append(ps, Problem{where + ".subject", "missing"}) + } + } + 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"}) + } + } + for i, p := range d.Provisions { + where := fmt.Sprintf("provisions[%d]", i) + if p.Name == "" { + ps = append(ps, Problem{where + ".name", "missing"}) + } + if p.Form == nil { + ps = append(ps, Problem{where + ".form", "missing"}) + } + } + for i, r := range d.Receives { + if r.Kind == "" { + ps = append(ps, Problem{fmt.Sprintf("receives[%d].kind", i), "missing"}) + } + } + for i, p := range d.Paths { + if !strings.HasPrefix(p.Path, "/") { + ps = append(ps, Problem{fmt.Sprintf("paths[%d].path", i), "missing, or not absolute"}) + } + } + return ps +} + +func checkVerbs(vs []Verb, whole bool) Problems { + var ps Problems + seen := map[string]bool{} + for i, v := range vs { + where := fmt.Sprintf("verbs[%d]", i) + if !wordName.MatchString(v.Name) { + ps = append(ps, Problem{where + ".name", "missing, or not a lower-case name"}) + } else if seen[v.Name] { + ps = append(ps, Problem{where + ".name", fmt.Sprintf("%q twice", v.Name)}) + } + seen[v.Name] = true + if whole && v.Input == nil { + ps = append(ps, Problem{where + ".input", "missing: a whole definition gives every verb's input"}) + } + if whole && v.Answer == nil { + ps = append(ps, Problem{where + ".answer", "missing: a whole definition gives every verb's answer"}) + } + } + return ps +} + +func checkMessages(field string, ms []Message, whole bool) Problems { + var ps Problems + seen := map[string]bool{} + for i, m := range ms { + where := fmt.Sprintf("%s[%d]", field, i) + if !wordName.MatchString(m.Name) { + ps = append(ps, Problem{where + ".name", "missing, or not a lower-case name"}) + } else if seen[m.Name] { + ps = append(ps, Problem{where + ".name", fmt.Sprintf("%q twice", m.Name)}) + } + seen[m.Name] = true + if whole && m.Body == nil { + ps = append(ps, Problem{where + ".body", "missing: a whole definition gives every message's body"}) + } + } + return ps +} + +// Resolve is a definition with every part it includes taken in: the part's verbs and events first, in the order +// the definition names the parts, then its own; and the part's types beside its own. A part named that is not given, +// or a name the part and the definition both define, is refused. +func Resolve(d Definition, parts map[string]Part) (Definition, error) { + if len(d.Includes) == 0 { + return d, nil + } + out := d + out.Includes = nil + out.Verbs, out.Events = nil, nil + out.Defs = map[string]map[string]any{} + var ps Problems + for _, name := range d.Includes { + p, ok := parts[name] + if !ok { + ps = append(ps, Problem{"includes", fmt.Sprintf("part %q is not given", name)}) + continue + } + out.Verbs = append(out.Verbs, p.Verbs...) + out.Events = append(out.Events, p.Events...) + for k, v := range p.Defs { + out.Defs[k] = v + } + } + out.Verbs = append(out.Verbs, d.Verbs...) + out.Events = append(out.Events, d.Events...) + for k, v := range d.Defs { + if _, twice := out.Defs[k]; twice { + ps = append(ps, Problem{"defs." + k, "defined by a part it includes too"}) + } + out.Defs[k] = v + } + if len(out.Defs) == 0 { + out.Defs = nil + } + ps = append(ps, checkVerbs(out.Verbs, d.Grain == Whole)...) + ps = append(ps, checkMessages("events", out.Events, d.Grain == Whole)...) + if len(ps) > 0 { + return Definition{}, ps + } + return out, nil +} + +// EventSubject is where the seat's event is published; by a caller, the caller's name follows. +func EventSubject(seat, event string) string { return "mesh.seat." + seat + ".event." + event } + +// AcceptSubject is where a message the seat accepts is sent. +func AcceptSubject(seat, accept string) string { return "mesh.seat." + seat + ".accept." + accept } + +// Index is the reading order of a contracts module's seats, `seats/index.json`: the seat list is read in it. +type Index struct { + Format int `json:"format"` + Seats []string `json:"seats"` +} + +// ParseIndex reads the index, refusing a seat named twice. +func ParseIndex(raw []byte) (Index, error) { + var ix Index + if err := strict(raw, &ix); err != nil { + return Index{}, err + } + var ps Problems + if ix.Format != Format { + ps = append(ps, Problem{"format", formatProblem(ix.Format)}) + } + seen := map[string]bool{} + for i, s := range ix.Seats { + if seen[s] { + ps = append(ps, Problem{fmt.Sprintf("seats[%d]", i), fmt.Sprintf("%q twice", s)}) + } + seen[s] = true + } + if len(ps) > 0 { + return Index{}, ps + } + return ix, nil +} + +// Names are the verbs', or messages', names in order. +func VerbNames(vs []Verb) []string { + out := make([]string, len(vs)) + for i, v := range vs { + out[i] = v.Name + } + return out +} + +// MessageNames are the messages' names in order. +func MessageNames(ms []Message) []string { + out := make([]string, len(ms)) + for i, m := range ms { + out[i] = m.Name + } + return out +} + +// DefNames are a definition's named types, sorted. +func (d Definition) DefNames() []string { + out := make([]string, 0, len(d.Defs)) + for k := range d.Defs { + out = append(out, k) + } + sort.Strings(out) + return out +} diff --git a/go/seatdef/seatdef_test.go b/go/seatdef/seatdef_test.go new file mode 100644 index 0000000..e1ca81e --- /dev/null +++ b/go/seatdef/seatdef_test.go @@ -0,0 +1,104 @@ +package seatdef + +import ( + "errors" + "strings" + "testing" +) + +// TestADefinitionIsReadStrictlyNamingWhatIsWrong is the format's own table (design 26: a valid definition parses; +// each missing required field is refused naming it; one over 256 KiB is refused), with the parts and fixtures beside. +func TestADefinitionIsReadStrictlyNamingWhatIsWrong(t *testing.T) { + const listGrain = `{"format":1,"seat":"node-hostname","scope":"node","grain":"seat-list","decision":"ADR 1", + "verbs":[{"name":"add","input":{"type":"object"}}]}` + const whole = `{"format":1,"seat":"mesh-broker","scope":"mesh","grain":"whole","decision":"ADR 1", + "verbs":[{"name":"list","input":{"type":"object"},"answer":{"type":"object"}}], + "events":[{"name":"created","body":{"type":"object"}}],"family":{"name":"broker.events","max-age-days":30}}` + cases := []struct { + name, raw string + refused string // a text the refusal names, empty when it parses + }{ + {"the seat list's grain parses", listGrain, ""}, + {"a whole definition parses", whole, ""}, + {"no format", `{"seat":"a-b","scope":"mesh","grain":"seat-list","decision":"x"}`, "format: missing"}, + {"a later format", `{"format":2,"seat":"a-b","scope":"mesh","grain":"seat-list","decision":"x"}`, "format 2"}, + {"no seat", `{"format":1,"scope":"mesh","grain":"seat-list","decision":"x"}`, "seat: missing"}, + {"no scope", `{"format":1,"seat":"a-b","grain":"seat-list","decision":"x"}`, "scope: missing"}, + {"another scope", `{"format":1,"seat":"a-b","scope":"room","grain":"seat-list","decision":"x"}`, "scope"}, + {"no grain", `{"format":1,"seat":"a-b","scope":"mesh","decision":"x"}`, "grain: missing"}, + {"no decision", `{"format":1,"seat":"a-b","scope":"mesh","grain":"seat-list"}`, "decision: missing"}, + {"an unknown field", `{"format":1,"seat":"a-b","scope":"mesh","grain":"seat-list","decision":"x","sevres":[]}`, + "sevres: a field the format does not know"}, + {"a whole verb with no answer", strings.Replace(whole, `,"answer":{"type":"object"}`, "", 1), + "verbs[0].answer: missing"}, + {"a whole verb with no input", strings.Replace(whole, `"input":{"type":"object"},`, "", 1), + "verbs[0].input: missing"}, + {"a whole event with no body", strings.Replace(whole, `"name":"created","body":{"type":"object"}`, + `"name":"created"`, 1), "events[0].body: missing"}, + {"whole events kept nowhere", strings.Replace(whole, `,"family":{"name":"broker.events","max-age-days":30}`, + "", 1), "family: missing"}, + {"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"}, + {"over 256 KiB", listGrain[:len(listGrain)-1] + `,"description":"` + strings.Repeat("x", MaxSize) + `"}`, + ErrTooLarge.Error()}, + } + for _, c := range cases { + _, err := Parse([]byte(c.raw)) + switch { + case c.refused == "" && err != nil: + t.Errorf("%s: refused: %v", c.name, err) + case c.refused != "" && err == nil: + t.Errorf("%s: parsed, want refused naming %q", c.name, c.refused) + case c.refused != "" && !strings.Contains(err.Error(), c.refused): + t.Errorf("%s: refused with %q, want it to name %q", c.name, err, c.refused) + } + } + if _, err := Parse([]byte(listGrain[:len(listGrain)-1] + `,"description":"` + strings.Repeat("x", MaxSize) + + `"}`)); !errors.Is(err, ErrTooLarge) { + t.Errorf("over 256 KiB is ErrTooLarge, got %v", err) + } + + // A part is taken in whole, its verbs before the definition's own; a part not given, or a type defined twice, + // is refused. + part, err := ParsePart([]byte(`{"format":1,"part":"registry","verbs":[{"name":"publish","input":{"type":"object"}, + "answer":{"type":"object"}}],"events":[{"name":"published","body":{"type":"object"}}], + "defs":{"Ref":{"type":"string"}}}`)) + if err != nil { + t.Fatal(err) + } + d, err := Parse([]byte(strings.Replace(whole, `"decision":"ADR 1",`, `"decision":"ADR 1","includes":["registry"],`, 1))) + if err != nil { + t.Fatal(err) + } + got, err := Resolve(d, map[string]Part{"registry": part}) + if err != nil { + t.Fatal(err) + } + if v := VerbNames(got.Verbs); strings.Join(v, ",") != "publish,list" { + t.Errorf("resolved verbs %v, want the part's first", v) + } + if m := MessageNames(got.Events); strings.Join(m, ",") != "published,created" || got.Defs["Ref"] == nil { + t.Errorf("resolved events %v, defs %v", m, got.Defs) + } + if _, err := Resolve(d, nil); err == nil || !strings.Contains(err.Error(), `part "registry" is not given`) { + t.Errorf("a part not given: %v", err) + } + d.Defs = map[string]map[string]any{"Ref": {"type": "string"}} + if _, err := Resolve(d, map[string]Part{"registry": part}); err == nil || !strings.Contains(err.Error(), "defs.Ref") { + t.Errorf("a type defined twice: %v", err) + } + + // Fixtures name what they are an instance of. + if _, err := ParseFixtures([]byte(`[{"name":"a","of":"verb:list.input","given":{}},{"name":"b","of":"event:created", + "subject":"mesh.seat.mesh-broker.event.created","given":{}}]`)); err != nil { + t.Errorf("fixtures: %v", err) + } + 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) + } + if _, err := ParseIndex([]byte(`{"format":1,"seats":["a-b","a-b"]}`)); err == nil { + t.Error("an index naming a seat twice parsed") + } +}