703 lines
26 KiB
Go
703 lines
26 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"
|
|
)
|
|
|
|
// 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"`
|
|
}
|
|
|
|
// 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: 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)...)
|
|
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
|
|
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
|
|
}
|