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:
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user