SD-M1: one reader of a seat's definition for every part (ADR 0608 §1, design 26)

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
This commit is contained in:
2026-11-04 11:51:04 +01:00
parent ba4220ad7d
commit 7d05a39c0c
2 changed files with 664 additions and 0 deletions
+560
View File
@@ -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>/seat.json` in a contracts module, with the parts it includes from
// `parts/<part>.json` and its conformance fixtures in `seats/<seat>/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.<seat>.accept.<name>`.
Accepts []Message `json:"accepts,omitempty"`
// Events are what is published on the seat's own subject, `mesh.seat.<seat>.event.<name>`.
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/<name>"}`.
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 `<holder>` 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:<name>.input`, `verb:<name>.answer`, `accept:<name>`,
// `event:<name>` or `holder-event:<name>`.
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:<name>.input, verb:<name>.answer, "+
"accept:<name>, event:<name>, holder-event:<name>", 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
}
+104
View File
@@ -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")
}
}