Files
mesh-sdk/go/seatdef/seatdef.go
T
jschoubben 078d3f53bf RR-1, RR-2: the seat-definition reader reads callers as text or list, class, keep as an object, step pipeline and on kind:image
Text callers stay read in the release (G's own reader refuses them); both old and new keep forms are read; check
stays beside pipeline.

Builds: design 47 G §340, §409, §336, §338, §391, §396, §397, §203, §266, §324

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 10:35:22 +01:00

954 lines
37 KiB
Go

// 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"
"slices"
"sort"
"strings"
"time"
)
// 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 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"
SeatList = "seat-list"
)
// The two scopes a seat is held at.
const (
ScopeMesh = "mesh"
ScopeNode = "node"
)
// Domains are the glossary's domains that own a seat, by their headings (hq 00-META/glossary.md, ADR 0244).
var Domains = []string{"Module", "Config", "Core", "Provisioning", "Identity and access", "Delivery",
"Health and repair", "Data", "Connectivity", "Operator and conversation"}
// 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"`
// Domain is the glossary domain the seat belongs to, by its heading (ADR 0636 §5): the indexer takes a seat of the
// mesh's own contracts only from that domain's contracts module.
Domain string `json:"domain"`
// 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"`
// 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.
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"`
// Acts are the seat's acting rules (novox/hq ADR 0656 §1; design 47 *What ADR 0656 leaves to this design*): each step
// of a delivery the seat's holder acts in, on which modules, and how long an entry waits there for the holder's new
// version. At either grain: a seat-list definition names its holder's step too.
Acts []ActingRule `json:"acts,omitempty"`
}
// ActingRule is one acting rule of a seat: the step its holder acts in, the modules it acts on (every module, or those
// whose manifest says this language), and how long an entry waits in that step for the holder's new version, a
// duration such as "30m"; none is the step's default (DefaultBound).
type ActingRule struct {
Step string `json:"step"`
On string `json:"on,omitempty"`
Bound string `json:"bound,omitempty"`
}
// The steps of a delivery a seat's holder acts in (design 47): what the indexer, the checker, the build modules, the
// node-engine, the grants part and the assigner each do for a change.
const (
StepTakeIn = "take-in"
StepValidate = "validate"
StepCheck = "check"
StepBuild = "build"
StepCompose = "compose"
StepGrant = "grant"
StepPlace = "place"
// StepPipeline is the runner's step, in the place of StepCheck from G's delivery (design 47 G §203).
StepPipeline = "pipeline"
)
// Steps are the steps an acting rule names, in a delivery's order.
var Steps = []string{StepTakeIn, StepValidate, StepCheck, StepBuild, StepCompose, StepGrant, StepPlace, StepPipeline}
// OnEvery is an acting rule's `on` for every module; an absent `on` reads as it.
const OnEvery = "every"
// OnKindImage is an acting rule's `on` for the modules whose artifact entry's kind is image (design 47 G §324, §397).
const OnKindImage = "kind:image"
// DefaultBound is how long an entry waits in step for its acting part's new version when the rule names no bound
// (design 47 *An acting part's new version acts first*): 60 minutes to build, 30 to compose, 20 to check (the first
// tier's), 10 to grant and to place; the indexer's take-in and validate have no wait, so zero.
func DefaultBound(step string) time.Duration {
switch step {
case StepBuild:
return 60 * time.Minute
case StepCompose:
return 30 * time.Minute
case StepCheck:
return 20 * time.Minute
case StepGrant, StepPlace:
return 10 * time.Minute
case StepPipeline:
return 30 * time.Minute
}
return 0
}
// Acting is the modules the rule acts on: OnEvery, or a language.
func (r ActingRule) Acting() string {
if r.On == "" {
return OnEvery
}
return r.On
}
// ActsOn says whether the rule acts on a module whose manifest says language.
func (r ActingRule) ActsOn(language string) bool {
on := r.Acting()
return on == OnEvery || strings.EqualFold(on, strings.TrimSpace(language))
}
// Wait is how long an entry waits in the rule's step for its acting part's new version: its bound, or the step's
// default. A bound the reader refused never reaches here; one that does not parse reads as the default.
func (r ActingRule) Wait() time.Duration {
if d, err := time.ParseDuration(r.Bound); err == nil && d > 0 {
return d
}
return DefaultBound(r.Step)
}
func checkActs(rs []ActingRule) Problems {
var ps Problems
seen := map[string]bool{}
for i, r := range rs {
where := fmt.Sprintf("acts[%d]", i)
switch {
case r.Step == "":
ps = append(ps, Problem{where + ".step", "missing"})
case !slices.Contains(Steps, r.Step):
ps = append(ps, Problem{where + ".step", fmt.Sprintf("%q is not a step (%s)", r.Step, strings.Join(Steps, ", "))})
}
if r.On != "" && r.On != OnEvery && r.On != OnKindImage && !wordName.MatchString(r.On) {
ps = append(ps, Problem{where + ".on", fmt.Sprintf("%q is neither every, kind:image nor a language's lower-case name", r.On)})
}
if r.Bound != "" {
if d, err := time.ParseDuration(r.Bound); err != nil || d <= 0 {
ps = append(ps, Problem{where + ".bound", fmt.Sprintf("%q is not a duration such as 30m", r.Bound)})
}
}
if k := r.Step + " " + r.Acting(); r.Step != "" && seen[k] {
ps = append(ps, Problem{where, fmt.Sprintf("the step %s on %s twice", r.Step, r.Acting())})
} else {
seen[k] = true
}
}
return ps
}
// 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, as a list (design 47 G §336) or, in the definitions before G, in words.
Callers *Callers `json:"callers,omitempty"`
// Class is the verb's class, ClassRead, ClassAct or ClassWarrant (design 47 G §391).
Class string `json:"class,omitempty"`
}
// Message is one accepted message or one event of the seat, its body a JSON Schema.
type Message struct {
// Name is the message's name; an event's may be lower-case words joined by dots (pull.updated), as a module's
// event keys are.
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"`
// Subject is an event's subject after `mesh.seat.<seat>.event.` when it carries parts (design 47 *What each step
// says*: `owed.<repository>.<number>`, `running.<node>.<module>`, `milestone.<milestone>.asked`): its tokens are
// the name's words in order with parts between angle brackets among them, so a reader that knows only the name
// still knows the event. Empty, the subject is the name. A part `<node>` is the node that publishes it, which
// the grants part grants a node's account alone (ADR 0363 §2).
Subject string `json:"subject,omitempty"`
// Keep says how an event is kept when not as the family keeps the seat's others, in a stream of its own named
// KeptStream: KeepLastPerSubject, the last message of each subject and no age limit (design 47: the node-engine's
// running.<node>.<module>, so the last build of every module on every node stays on the bus however long ago it
// was said, and a durable consumer starting from the last message per subject holds them all); or KeepStream,
// every message within MaxAgeDays and MaxMiB.
Keep *Keep `json:"keep,omitempty"`
// Callers says who may send an accepted kind, as a verb's (design 47 G §338).
Callers *Callers `json:"callers,omitempty"`
// MaxAgeDays and MaxMiB bound an event kept KeepStream: how long a message is kept and how much the stream holds
// (design 47 *What the gate owes the forge*: the stream `owed`, 7 days and at most 64 MiB). Given with KeepStream
// alone.
MaxAgeDays int `json:"max-age-days,omitempty"`
MaxMiB int `json:"max-mib,omitempty"`
}
// The ways an event is kept in a stream of its own, KeptStream, instead of its seat's family.
const (
// KeepLastPerSubject keeps the last message of each subject, with no age limit.
KeepLastPerSubject = "last-per-subject"
// KeepStream keeps every message for MaxAgeDays, at most MaxMiB in all.
KeepStream = "stream"
)
// KeptStream is the stream an event with Keep is kept in: `<seat>.<event>`, as a family is `<seat>.events`.
func KeptStream(seat string, m Message) string { return seat + "." + m.Name }
// legacyMode is the string form of Keep, empty for none or for the object form.
func (m Message) legacyMode() string {
if m.Keep == nil {
return ""
}
return m.Keep.Mode
}
// SubjectPattern is the event's subject after the seat's event prefix with its parts between angle brackets: the
// name itself when the definition gives no subject.
func (m Message) SubjectPattern() string {
if m.Subject == "" {
return m.Name
}
return m.Subject
}
// Parts are the names of the subject's parts, in order; none for a subject that is the name.
func (m Message) Parts() []string {
var out []string
for _, tok := range strings.Split(m.SubjectPattern(), ".") {
if keyPartOK.MatchString(tok) {
out = append(out, tok[1:len(tok)-1])
}
}
return out
}
// Local is the subject after the seat's event prefix with `*` for each part: what a manifest's `emits` and
// `consumes` name of the event, and the filter that hears every instance of it.
func (m Message) Local() string {
toks := strings.Split(m.SubjectPattern(), ".")
for i, tok := range toks {
if keyPartOK.MatchString(tok) {
toks[i] = "*"
}
}
return strings.Join(toks, ".")
}
// checkSubject says what is wrong with an event's subject, or "".
func checkSubject(m Message) string {
if m.Subject == "" {
return ""
}
if m.ByCaller {
return "an event named by its caller ends in the caller's name; it carries no subject of its own"
}
var words, parts []string
for _, tok := range strings.Split(m.Subject, ".") {
switch {
case keyPartOK.MatchString(tok):
if slices.Contains(parts, tok) {
return fmt.Sprintf("part %s twice", tok)
}
parts = append(parts, tok)
case wordName.MatchString(tok):
words = append(words, tok)
default:
return fmt.Sprintf("%q is neither a lower-case word nor a part between < and >", tok)
}
}
if len(parts) == 0 {
return "no part: an event whose subject is its name gives no subject"
}
if strings.Join(words, ".") != m.Name {
return fmt.Sprintf("its words are %q, not the event's name %q", strings.Join(words, "."), m.Name)
}
return ""
}
// 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: the shared events stream filtered to the seat's subjects, so its age is
// the stream's (SharedEventsMaxAgeDays) and it has no cap of its own (the lead's ruling of 2026-11-04 13:0x: a
// holder that keeps a longer history keeps it in a stream of its own module).
type Family struct {
Name string `json:"name"`
MaxAgeDays int `json:"max-age-days"`
}
// SharedEventsMaxAgeDays is how long the shared events stream keeps a seat's events (mesh-sdk buscompose's
// EventsStream, whose test holds the two equal).
const SharedEventsMaxAgeDays = 7
// 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.
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>, state:<name>/<key form>", 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 "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
}
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"})
}
switch {
case d.Domain == "":
ps = append(ps, Problem{"domain", "missing"})
case !slices.Contains(Domains, d.Domain):
ps = append(ps, Problem{"domain", fmt.Sprintf("%q is not a domain of the glossary (%s)", d.Domain, strings.Join(Domains, ", "))})
}
whole := d.Grain == Whole
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, 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"})
}
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 != SharedEventsMaxAgeDays {
ps = append(ps, Problem{"family.max-age-days", fmt.Sprintf("missing, or not %d: a seat's events ride the "+
"shared events stream, which keeps them %d days", SharedEventsMaxAgeDays, SharedEventsMaxAgeDays)})
}
}
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"})
}
// A holder's event is published under the holder's own module subjects, or, when it is kept in a stream of the
// holder's own module (its family), under that stream's (buscompose.ModuleStreamSubject).
own, stream := "mesh.mod.<holder>.event."+e.Name, "mesh.stream.<holder>.event."+e.Name
switch {
case e.Subject == "":
ps = append(ps, Problem{where + ".subject", "missing"})
case e.Subject == stream && e.Family == "":
ps = append(ps, Problem{where + ".subject", "a subject of the holder's own stream, and no family names that stream"})
case e.Subject == own && e.Family != "":
ps = append(ps, Problem{where + ".subject", "a family names the holder's own stream, so the event is under " + stream})
case e.Subject != own && e.Subject != stream:
ps = append(ps, Problem{where + ".subject", fmt.Sprintf("not %s, nor %s for an event kept in the holder's own stream", own, stream)})
}
}
ps = append(ps, checkStates(d.States)...)
ps = append(ps, checkActs(d.Acts)...)
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
}
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{}
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
ps = append(ps, checkCallers(where+".callers", v.Callers)...)
if v.Class != "" && !slices.Contains(Classes, v.Class) {
ps = append(ps, Problem{where + ".class", fmt.Sprintf("%q is not a class (%s)", v.Class, strings.Join(Classes, ", "))})
}
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)
named := wordName
if field == "events" {
named = eventName
}
if !named.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
mode := m.legacyMode()
switch {
case m.Keep != nil && field != "events":
ps = append(ps, Problem{where + ".keep", "only an event is kept"})
case m.Keep != nil && m.Keep.Object:
ps = append(ps, checkKeepObject(where+".keep", *m.Keep)...)
if m.MaxAgeDays != 0 || m.MaxMiB != 0 {
ps = append(ps, Problem{where + ".max-age-days", "an event whose keep is an object gives its max-age-days and max-mib there"})
}
if m.Keep.LastPerSubject && len(m.Parts()) == 0 && !m.ByCaller {
ps = append(ps, Problem{where + ".keep", "an event of one subject kept last per subject is one message: give it a subject with parts"})
}
case mode != "" && mode != KeepLastPerSubject && mode != KeepStream:
ps = append(ps, Problem{where + ".keep", fmt.Sprintf("%q is not %s or %s", mode, KeepLastPerSubject, KeepStream)})
case mode == KeepLastPerSubject && len(m.Parts()) == 0 && !m.ByCaller:
ps = append(ps, Problem{where + ".keep", "an event of one subject kept last per subject is one message: give it a subject with parts"})
case mode == KeepStream && (m.MaxAgeDays <= 0 || m.MaxMiB <= 0):
ps = append(ps, Problem{where + ".keep", "an event kept in a stream of its own gives its max-age-days and max-mib"})
case mode != KeepStream && (m.MaxAgeDays != 0 || m.MaxMiB != 0):
ps = append(ps, Problem{where + ".max-age-days", "only an event kept as a stream of its own gives max-age-days and max-mib"})
}
if m.Callers != nil && field != "accepts" {
ps = append(ps, Problem{where + ".callers", "only an accepted kind carries callers"})
}
ps = append(ps, checkCallers(where+".callers", m.Callers)...)
if m.Subject != "" && field != "events" {
ps = append(ps, Problem{where + ".subject", "only an event carries a subject of its own"})
} else if why := checkSubject(m); why != "" {
ps = append(ps, Problem{where + ".subject", why})
}
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
}