Merge pull request 'Reader release: the seat-definition and manifest readers read the words milestone G writes (design 47 G)' (#186) from fix/47-G-reader-release into main

This commit was merged in pull request #186.
This commit is contained in:
2026-11-11 10:22:19 +00:00
7 changed files with 342 additions and 23 deletions
+10
View File
@@ -465,6 +465,11 @@ type Manifest struct {
// choosing `kc` for keycloak keeps the identity legible where a hash would not. // choosing `kc` for keycloak keeps the identity legible where a hash would not.
Slug string `json:"slug,omitempty"` Slug string `json:"slug,omitempty"`
// NeedsApproval marks tools the module serves (`<tool>`), seat verbs it serves (`<seat>.<verb>`) and events it
// emits that need the operator's approval first (design 46 *The approval mark* §1; design 34 every-event §32). Absent
// is no mark.
NeedsApproval []string `json:"needs-approval,omitempty"`
// Provides are the names other modules may require. A module always provides its own name; // Provides are the names other modules may require. A module always provides its own name;
// this is for the rest — `zsh` provides `shell`, `xorg` provides `display-server`. // this is for the rest — `zsh` provides `shell`, `xorg` provides `display-server`.
Provides []Offer `json:"provides,omitempty"` Provides []Offer `json:"provides,omitempty"`
@@ -1236,6 +1241,11 @@ type Artifact struct {
// DefaultSeries (SeriesOf). // DefaultSeries (SeriesOf).
Series string `json:"series,omitempty"` Series string `json:"series,omitempty"`
// Needs are the artifact references, `<module>:<artifact>`, of the catalog graph's edges this artifact has where no
// other line of the manifest or of a seat's definition says them (design 47 G §47, §432). Read here so a reader
// release keeps a manifest that names them whole (design 47 G §340, §409); the manifest validator checks their form.
Needs []string `json:"needs,omitempty"`
// GeneratedFrom marks a package of a contracts module (novox/hq ADR 0608 §5, design 26 *Contracts modules*): the // GeneratedFrom marks a package of a contracts module (novox/hq ADR 0608 §5, design 26 *Contracts modules*): the
// directory, relative to the module's, of the seat definitions every file of the package is generated from. A // directory, relative to the module's, of the seat definitions every file of the package is generated from. A
// module carrying it holds contracts only and claims no seat, which the judge refuses naming the seat; the build // module carrying it holds contracts only and claims no seat, which the judge refuses naming the seat; the build
+23
View File
@@ -0,0 +1,23 @@
package catalogue
import "testing"
// A manifest carrying the words design 47 G writes is read whole by the reader release (design 47 G §340, §409):
// `version` as MAJOR.MINOR (§368), a `*` emitter in `consumes` (§386), an observer's finding stream (§387) and an
// artifact's `needs` (§47, §432).
func TestTheReaderReleaseReadsTheManifestWordsGWrites(t *testing.T) {
m, err := decodeForTest([]byte(`{"module":"mesh-watcher","version":"1.4","needs-approval":["restart","node-engine.composition","finding-written"],` +
`"consumes":["*.finding-written","mesh-conditions.findings-asked"],` +
`"streams":[{"name":"mesh-watcher-findings","events":["finding-written","finding-cleared","findings-stated"],"max-age-days":7,"max-mib":16}],` +
`"build":{"artifacts":[{"name":"program","kind":"bundle","language":"go","needs":["mesh-grants:program","node-tools:runtime"]}]}}`))
if err != nil {
t.Fatalf("a manifest with G's words was refused: %v", err)
}
if len(m.UnknownField()) != 0 || len(m.NeedsApproval) != 3 {
t.Fatalf("a word was set aside: %v (needs-approval %v)", m.UnknownField(), m.NeedsApproval)
}
if m.Version != "1.4" || m.Consumes[0] != "*.finding-written" || m.Streams[0].Name != "mesh-watcher-findings" ||
len(m.Build.Artifacts[0].Needs) != 2 || m.Build.Artifacts[0].Needs[0] != "mesh-grants:program" {
t.Fatalf("read as %+v", m)
}
}
+167
View File
@@ -0,0 +1,167 @@
package seatdef
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"strings"
)
// Callers says who may call a verb or send an accepted kind. It is read as text (the form every definition on main
// has) or as a list (the form design 47 G §336 writes); the reader release carries both so that a definition in
// either form is read (design 47 G §340), and G's own reader refuses the text.
type Callers struct {
// Text is the callers in words, when the definition gave them so.
Text string
// List is the callers as a list of entries, when the definition gave them so.
List []string
}
// IsText says whether the definition gave its callers in words.
func (c Callers) IsText() bool { return c.List == nil }
// UnmarshalJSON reads a string or a list of strings.
func (c *Callers) UnmarshalJSON(raw []byte) error {
raw = bytes.TrimSpace(raw)
switch {
case len(raw) > 0 && raw[0] == '"':
return json.Unmarshal(raw, &c.Text)
case len(raw) > 0 && raw[0] == '[':
c.List = []string{}
return json.Unmarshal(raw, &c.List)
}
return errors.New("callers is a text or a list of entries")
}
// MarshalJSON writes the form the definition was read in.
func (c Callers) MarshalJSON() ([]byte, error) {
if c.IsText() {
return json.Marshal(c.Text)
}
return json.Marshal(c.List)
}
func checkCallers(where string, c *Callers) Problems {
if c == nil {
return nil
}
if c.IsText() {
if c.Text == "" {
return Problems{{where, "is empty"}}
}
return nil
}
var ps Problems
for i, e := range c.List {
if why := CallerEntryProblem(e); why != "" {
ps = append(ps, Problem{fmt.Sprintf("%s[%d]", where, i), why})
}
}
return ps
}
// The entries of a list of callers that are not a name (design 47 G §336).
const (
CallerOperator = "operator"
CallerHealers = "healers"
CallerPersons = "persons"
CallerSessions = "sessions"
CallerInvokers = "invokers"
)
// The prefixes of the entries that name a seat's holder or a module (design 47 G §392, §336).
const (
CallerSeatPrefix = "seat:"
CallerModulePrefix = "module:"
CallerHolderPrefix = "holder:"
CallerSameNode = "@same-node"
)
// CallerEntryProblem says why an entry of a list of callers is none of the forms of design 47 G §336, §392; empty
// when it is one: `operator`, `healers`, `persons`, `sessions`, `invokers`, `seat:<seat>`, `module:<module>` and
// `holder:<seat>@same-node`. A bare name is refused, since a name such as `mesh-grants` is both a seat and a module.
func CallerEntryProblem(e string) string {
switch e {
case CallerOperator, CallerHealers, CallerPersons, CallerSessions, CallerInvokers:
return ""
}
if n, ok := strings.CutPrefix(e, CallerSeatPrefix); ok && wordName.MatchString(n) {
return ""
}
if n, ok := strings.CutPrefix(e, CallerModulePrefix); ok && wordName.MatchString(n) {
return ""
}
if n, ok := strings.CutPrefix(e, CallerHolderPrefix); ok {
if seat, ok := strings.CutSuffix(n, CallerSameNode); ok && wordName.MatchString(seat) {
return ""
}
}
return fmt.Sprintf("%q is none of operator, healers, persons, sessions, invokers, seat:<seat>, module:<module> and "+
"holder:<seat>@same-node", e)
}
// The classes of a verb (design 47 G §391).
const (
ClassRead = "read"
ClassAct = "act"
ClassWarrant = "warrant"
)
// Classes are the classes a verb's `class` may name.
var Classes = []string{ClassRead, ClassAct, ClassWarrant}
// Keep is an event's `keep`: the string of today's definitions (KeepLastPerSubject or KeepStream, with the message's
// max-age-days and max-mib beside it) or the object of design 47 G §396 (`max-age-days`, `max-mib`,
// `last-per-subject`). The reader release reads both (design 47 G §340, §409).
type Keep struct {
// Mode is the string form; empty when the object form was given.
Mode string
// MaxAgeDays, MaxMiB and LastPerSubject are the object form's fields; MaxAgeDays and MaxMiB are nil when unsaid.
MaxAgeDays *int
MaxMiB *int
LastPerSubject bool
// Object says the object form was given.
Object bool
}
type keepObject struct {
MaxAgeDays *int `json:"max-age-days"`
MaxMiB *int `json:"max-mib"`
LastPerSubject bool `json:"last-per-subject"`
}
// UnmarshalJSON reads the string or the object, the object strictly.
func (k *Keep) UnmarshalJSON(raw []byte) error {
raw = bytes.TrimSpace(raw)
if len(raw) > 0 && raw[0] == '"' {
return json.Unmarshal(raw, &k.Mode)
}
var o keepObject
dec := json.NewDecoder(bytes.NewReader(raw))
dec.DisallowUnknownFields()
if err := dec.Decode(&o); err != nil {
return fmt.Errorf("keep is a string or an object of max-age-days, max-mib and last-per-subject: %w", err)
}
*k = Keep{MaxAgeDays: o.MaxAgeDays, MaxMiB: o.MaxMiB, LastPerSubject: o.LastPerSubject, Object: true}
return nil
}
// MarshalJSON writes the form the definition was read in.
func (k Keep) MarshalJSON() ([]byte, error) {
if !k.Object {
return json.Marshal(k.Mode)
}
return json.Marshal(keepObject{k.MaxAgeDays, k.MaxMiB, k.LastPerSubject})
}
func checkKeepObject(where string, k Keep) Problems {
var ps Problems
if k.MaxAgeDays != nil && *k.MaxAgeDays < 0 {
ps = append(ps, Problem{where + ".max-age-days", "is negative (0 is no age limit)"})
}
if k.MaxMiB != nil && *k.MaxMiB <= 0 {
ps = append(ps, Problem{where + ".max-mib", "is not above zero"})
}
return ps
}
+15 -5
View File
@@ -1,18 +1,28 @@
package seatdef package seatdef
import "strings"
// ResolveRefs is a schema (its all-string lists as []string, StringLists) with every `{"$ref": "#/defs/<name>"}` replaced by the definition's named type, as the seat // ResolveRefs is a schema (its all-string lists as []string, StringLists) with every `{"$ref": "#/defs/<name>"}` replaced by the definition's named type, as the seat
// list's readers need it (a verb's input and answer are one schema each, not a schema and its types). A reference to a // list's readers need it (a verb's input and answer are one schema each, not a schema and its types). A reference to a
// name the definition does not define stays as it is; nesting deeper than 32 levels stops there. // name the definition does not define stays as it is; nesting deeper than 32 levels stops there.
func ResolveRefs(schema map[string]any, defs map[string]map[string]any) map[string]any { //
// A reference `#/bodies/<name>` resolves against the definition's bodies, given after defs (design 47 G §439).
func ResolveRefs(schema map[string]any, defs map[string]map[string]any, bodies ...map[string]map[string]any) map[string]any {
if schema == nil { if schema == nil {
return nil return nil
} }
m, _ := StringLists(resolveValue(schema, defs, 0)).(map[string]any) all := make(map[string]map[string]any, len(defs))
for n, d := range defs {
all["#/defs/"+n] = d
}
for _, b := range bodies {
for n, d := range b {
all["#/bodies/"+n] = d
}
}
m, _ := StringLists(resolveValue(schema, all, 0)).(map[string]any)
return m return m
} }
// resolveValue resolves against refs, each reference as written (`#/defs/<name>`, `#/bodies/<name>`).
func resolveValue(v any, defs map[string]map[string]any, depth int) any { func resolveValue(v any, defs map[string]map[string]any, depth int) any {
if depth > 32 { if depth > 32 {
return v return v
@@ -20,7 +30,7 @@ func resolveValue(v any, defs map[string]map[string]any, depth int) any {
switch x := v.(type) { switch x := v.(type) {
case map[string]any: case map[string]any:
if r, ok := x["$ref"].(string); ok { if r, ok := x["$ref"].(string); ok {
if d, ok := defs[strings.TrimPrefix(r, "#/defs/")]; ok { if d, ok := defs[r]; ok {
return resolveValue(d, defs, depth+1) return resolveValue(d, defs, depth+1)
} }
} }
+6 -1
View File
@@ -11,6 +11,7 @@ func TestResolveRefsPutsNamedTypesInPlace(t *testing.T) {
"id": {"type": "string"}, "id": {"type": "string"},
"row": {"type": "object", "properties": map[string]any{"id": map[string]any{"$ref": "#/defs/id"}}}, "row": {"type": "object", "properties": map[string]any{"id": map[string]any{"$ref": "#/defs/id"}}},
} }
bodies := map[string]map[string]any{"Finding": {"type": "object", "properties": map[string]any{"id": map[string]any{"$ref": "#/defs/id"}}}}
cases := []struct { cases := []struct {
name string name string
schema, want map[string]any schema, want map[string]any
@@ -20,11 +21,15 @@ func TestResolveRefsPutsNamedTypesInPlace(t *testing.T) {
map[string]any{"type": "array", "items": map[string]any{"type": "object", map[string]any{"type": "array", "items": map[string]any{"type": "object",
"properties": map[string]any{"id": map[string]any{"type": "string"}}}}}, "properties": map[string]any{"id": map[string]any{"type": "string"}}}}},
{"a name it does not define stays", map[string]any{"$ref": "#/defs/gone"}, map[string]any{"$ref": "#/defs/gone"}}, {"a name it does not define stays", map[string]any{"$ref": "#/defs/gone"}, map[string]any{"$ref": "#/defs/gone"}},
{"a body", map[string]any{"type": "array", "items": map[string]any{"$ref": "#/bodies/Finding"}},
map[string]any{"type": "array", "items": map[string]any{"type": "object",
"properties": map[string]any{"id": map[string]any{"type": "string"}}}}},
{"a body it does not define stays", map[string]any{"$ref": "#/bodies/Gone"}, map[string]any{"$ref": "#/bodies/Gone"}},
{"a list of schemas", map[string]any{"oneOf": []any{map[string]any{"$ref": "#/defs/id"}}}, {"a list of schemas", map[string]any{"oneOf": []any{map[string]any{"$ref": "#/defs/id"}}},
map[string]any{"oneOf": []any{map[string]any{"type": "string"}}}}, map[string]any{"oneOf": []any{map[string]any{"type": "string"}}}},
} }
for _, c := range cases { for _, c := range cases {
if got := ResolveRefs(c.schema, defs); !reflect.DeepEqual(got, c.want) { if got := ResolveRefs(c.schema, defs, bodies); !reflect.DeepEqual(got, c.want) {
t.Errorf("%s: %v, want %v", c.name, got, c.want) t.Errorf("%s: %v, want %v", c.name, got, c.want)
} }
} }
+76 -16
View File
@@ -93,6 +93,9 @@ type Definition struct {
Paths []Path `json:"paths,omitempty"` Paths []Path `json:"paths,omitempty"`
// Defs are named types a schema refers to as `{"$ref": "#/defs/<name>"}`. // Defs are named types a schema refers to as `{"$ref": "#/defs/<name>"}`.
Defs map[string]map[string]any `json:"defs,omitempty"` Defs map[string]map[string]any `json:"defs,omitempty"`
// Bodies are the bodies other modules say on their own streams, named: each a JSON Schema, referred to as
// `{"$ref": "#/bodies/<name>"}`. The grants part makes no seat event or stream from them (design 47 G §439, §409).
Bodies map[string]map[string]any `json:"bodies,omitempty"`
// Acts are the seat's acting rules (novox/hq ADR 0656 §1; design 47 *What ADR 0656 leaves to this design*): each step // 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 // 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. // version. At either grain: a seat-list definition names its holder's step too.
@@ -118,14 +121,19 @@ const (
StepCompose = "compose" StepCompose = "compose"
StepGrant = "grant" StepGrant = "grant"
StepPlace = "place" 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. // Steps are the steps an acting rule names, in a delivery's order.
var Steps = []string{StepTakeIn, StepValidate, StepCheck, StepBuild, StepCompose, StepGrant, StepPlace} 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. // OnEvery is an acting rule's `on` for every module; an absent `on` reads as it.
const OnEvery = "every" 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 // 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 // (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. // tier's), 10 to grant and to place; the indexer's take-in and validate have no wait, so zero.
@@ -139,6 +147,8 @@ func DefaultBound(step string) time.Duration {
return 20 * time.Minute return 20 * time.Minute
case StepGrant, StepPlace: case StepGrant, StepPlace:
return 10 * time.Minute return 10 * time.Minute
case StepPipeline:
return 30 * time.Minute
} }
return 0 return 0
} }
@@ -177,8 +187,8 @@ func checkActs(rs []ActingRule) Problems {
case !slices.Contains(Steps, r.Step): 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, ", "))}) 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 && !wordName.MatchString(r.On) { 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 nor a language's lower-case name", 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 r.Bound != "" {
if d, err := time.ParseDuration(r.Bound); err != nil || d <= 0 { if d, err := time.ParseDuration(r.Bound); err != nil || d <= 0 {
@@ -204,10 +214,21 @@ type Verb struct {
Replaces []string `json:"replaces,omitempty"` Replaces []string `json:"replaces,omitempty"`
Input map[string]any `json:"input,omitempty"` Input map[string]any `json:"input,omitempty"`
Answer map[string]any `json:"answer,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 says who may call it, as a list (design 47 G §336) or, in the definitions before G, in words.
Callers string `json:"callers,omitempty"` Callers *Callers `json:"callers,omitempty"`
// Class is the verb's class, ClassRead, ClassAct or ClassWarrant (design 47 G §391).
Class string `json:"class,omitempty"`
// NeedsApproval marks the verb for every holder: the operator approves a call of it first (design 46 *The approval
// mark* §2).
NeedsApproval bool `json:"needs-approval,omitempty"`
} }
// The streams an accepted kind may ride (design 47 G §415).
const (
StreamCheck = "check"
StreamDelivery = "delivery"
)
// Message is one accepted message or one event of the seat, its body a JSON Schema. // Message is one accepted message or one event of the seat, its body a JSON Schema.
type Message struct { 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 // Name is the message's name; an event's may be lower-case words joined by dots (pull.updated), as a module's
@@ -227,7 +248,12 @@ type Message struct {
// running.<node>.<module>, so the last build of every module on every node stays on the bus however long ago it // 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, // was said, and a durable consumer starting from the last message per subject holds them all); or KeepStream,
// every message within MaxAgeDays and MaxMiB. // every message within MaxAgeDays and MaxMiB.
Keep string `json:"keep,omitempty"` 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"`
// Stream is the stream an accepted kind rides, StreamCheck or StreamDelivery; its subject is `<stream>.<kind>`, and
// the same kind name may ride more than one stream (design 47 G §415).
Stream string `json:"stream,omitempty"`
// MaxAgeDays and MaxMiB bound an event kept KeepStream: how long a message is kept and how much the stream holds // 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 // (design 47 *What the gate owes the forge*: the stream `owed`, 7 days and at most 64 MiB). Given with KeepStream
// alone. // alone.
@@ -243,8 +269,19 @@ const (
KeepStream = "stream" KeepStream = "stream"
) )
// KeptStream is the stream an event with Keep is kept in: `<seat>.<event>`, as a family is `<seat>.events`. // KeptStream is the stream an event with Keep is kept in: `SEAT_<seat>_<event>` (design 47 G §260, §396).
func KeptStream(seat string, m Message) string { return seat + "." + m.Name } func KeptStream(seat string, m Message) string { return KeptStreamPrefix + seat + "_" + m.Name }
// KeptStreamPrefix begins the name of every kept event's stream.
const KeptStreamPrefix = "SEAT_"
// 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 // 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. // name itself when the definition gives no subject.
@@ -761,6 +798,10 @@ func checkVerbs(vs []Verb, whole bool) Problems {
ps = append(ps, Problem{where + ".name", fmt.Sprintf("%q twice", v.Name)}) ps = append(ps, Problem{where + ".name", fmt.Sprintf("%q twice", v.Name)})
} }
seen[v.Name] = true 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 { if whole && v.Input == nil {
ps = append(ps, Problem{where + ".input", "missing: a whole definition gives every verb's input"}) ps = append(ps, Problem{where + ".input", "missing: a whole definition gives every verb's input"})
} }
@@ -782,22 +823,41 @@ func checkMessages(field string, ms []Message, whole bool) Problems {
} }
if !named.MatchString(m.Name) { if !named.MatchString(m.Name) {
ps = append(ps, Problem{where + ".name", "missing, or not a lower-case name"}) ps = append(ps, Problem{where + ".name", "missing, or not a lower-case name"})
} else if seen[m.Name] { } else if key := m.Stream + "/" + m.Name; seen[key] {
ps = append(ps, Problem{where + ".name", fmt.Sprintf("%q twice", m.Name)}) ps = append(ps, Problem{where + ".name", fmt.Sprintf("%q twice", m.Name)})
} }
seen[m.Name] = true seen[m.Stream+"/"+m.Name] = true
switch { switch {
case m.Keep != "" && field != "events": case m.Stream != "" && field != "accepts":
ps = append(ps, Problem{where + ".stream", "only an accepted kind rides a stream"})
case m.Stream != "" && m.Stream != StreamCheck && m.Stream != StreamDelivery:
ps = append(ps, Problem{where + ".stream", fmt.Sprintf("%q is not %s or %s", m.Stream, StreamCheck, StreamDelivery)})
}
mode := m.legacyMode()
switch {
case m.Keep != nil && field != "events":
ps = append(ps, Problem{where + ".keep", "only an event is kept"}) ps = append(ps, Problem{where + ".keep", "only an event is kept"})
case m.Keep != "" && m.Keep != KeepLastPerSubject && m.Keep != KeepStream: case m.Keep != nil && m.Keep.Object:
ps = append(ps, Problem{where + ".keep", fmt.Sprintf("%q is not %s or %s", m.Keep, KeepLastPerSubject, KeepStream)}) ps = append(ps, checkKeepObject(where+".keep", *m.Keep)...)
case m.Keep == KeepLastPerSubject && len(m.Parts()) == 0 && !m.ByCaller: 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"}) 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 m.Keep == KeepStream && (m.MaxAgeDays <= 0 || m.MaxMiB <= 0): 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"}) ps = append(ps, Problem{where + ".keep", "an event kept in a stream of its own gives its max-age-days and max-mib"})
case m.Keep != KeepStream && (m.MaxAgeDays != 0 || m.MaxMiB != 0): 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"}) 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" { if m.Subject != "" && field != "events" {
ps = append(ps, Problem{where + ".subject", "only an event carries a subject of its own"}) ps = append(ps, Problem{where + ".subject", "only an event carries a subject of its own"})
} else if why := checkSubject(m); why != "" { } else if why := checkSubject(m); why != "" {
+45 -1
View File
@@ -105,7 +105,7 @@ func TestADefinitionIsReadStrictlyNamingWhatIsWrong(t *testing.T) {
{"an acting rule at the seat list's grain", strings.Replace(listGrain, `"verbs":`, `"acts":[{"step":"compose","bound":"30m"}],"verbs":`, 1), ""}, {"an acting rule at the seat list's grain", strings.Replace(listGrain, `"verbs":`, `"acts":[{"step":"compose","bound":"30m"}],"verbs":`, 1), ""},
{"an acting rule with no step", strings.Replace(listGrain, `"verbs":`, `"acts":[{"on":"go"}],"verbs":`, 1), "acts[0].step: missing"}, {"an acting rule with no step", strings.Replace(listGrain, `"verbs":`, `"acts":[{"on":"go"}],"verbs":`, 1), "acts[0].step: missing"},
{"an acting rule of an unknown step", strings.Replace(listGrain, `"verbs":`, `"acts":[{"step":"deploy"}],"verbs":`, 1), {"an acting rule of an unknown step", strings.Replace(listGrain, `"verbs":`, `"acts":[{"step":"deploy"}],"verbs":`, 1),
`acts[0].step: "deploy" is not a step (take-in, validate, check, build, compose, grant, place)`}, `acts[0].step: "deploy" is not a step (take-in, validate, check, build, compose, grant, place, pipeline)`},
{"an acting rule on no language", strings.Replace(listGrain, `"verbs":`, `"acts":[{"step":"build","on":"Go Lang"}],"verbs":`, 1), {"an acting rule on no language", strings.Replace(listGrain, `"verbs":`, `"acts":[{"step":"build","on":"Go Lang"}],"verbs":`, 1),
"acts[0].on"}, "acts[0].on"},
{"an acting rule's bound that is no duration", strings.Replace(listGrain, `"verbs":`, `"acts":[{"step":"build","bound":"an hour"}],"verbs":`, 1), {"an acting rule's bound that is no duration", strings.Replace(listGrain, `"verbs":`, `"acts":[{"step":"build","bound":"an hour"}],"verbs":`, 1),
@@ -114,6 +114,42 @@ func TestADefinitionIsReadStrictlyNamingWhatIsWrong(t *testing.T) {
"when: a field the format does not know"}, "when: a field the format does not know"},
{"one step on one language twice", strings.Replace(listGrain, `"verbs":`, `"acts":[{"step":"grant"},{"step":"grant","on":"every"}],"verbs":`, 1), {"one step on one language twice", strings.Replace(listGrain, `"verbs":`, `"acts":[{"step":"grant"},{"step":"grant","on":"every"}],"verbs":`, 1),
"acts[1]: the step grant on every twice"}, "acts[1]: the step grant on every twice"},
// The reader release (design 47 G §340, §409): the forms G writes beside the ones on main.
{"callers as text", strings.Replace(whole, `"name":"list",`, `"name":"list","callers":"a person",`, 1), ""},
{"callers as a list", strings.Replace(whole, `"name":"list",`, `"name":"list","callers":["operator","seat:mesh-grants","invokers"],"class":"read",`, 1), ""},
{"callers as neither", strings.Replace(whole, `"name":"list",`, `"name":"list","callers":7,`, 1), "callers is a text or a list"},
{"a list with an empty entry", strings.Replace(whole, `"name":"list",`, `"name":"list","callers":["operator",""],`, 1), "verbs[0].callers[1]"},
{"a bare name among callers", strings.Replace(whole, `"name":"list",`, `"name":"list","callers":["mesh-grants"],`, 1), `"mesh-grants" is none of operator`},
{"a class of the old branches", strings.Replace(whole, `"name":"list",`, `"name":"list","callers":["holder","invokes","reviewers"],`, 1), `"holder" is none of operator`},
{"every form among callers", strings.Replace(whole, `"name":"list",`,
`"name":"list","callers":["operator","healers","persons","sessions","invokers","seat:mesh-grants","module:mesh-grants","holder:node-backup@same-node"],`, 1), ""},
{"a holder not on the same node", strings.Replace(whole, `"name":"list",`, `"name":"list","callers":["holder:node-backup"],`, 1), "none of operator"},
{"a class that is none", strings.Replace(whole, `"name":"list",`, `"name":"list","class":"free",`, 1), `verbs[0].class: "free" is not a class (read, act, warrant)`},
{"an accepted kind's callers", strings.Replace(whole, `"events":`, `"accepts":[{"name":"step","body":{},"callers":["seat:mesh-pipeline-runner"]}],"events":`, 1), ""},
{"an event's callers", strings.Replace(whole, `"name":"created"`, `"name":"created","callers":["operator"]`, 1), "only an accepted kind carries callers"},
{"keep as an object", strings.Replace(whole, `"name":"created"`,
`"name":"owed","keep":{"max-age-days":7,"max-mib":64}`, 1), ""},
{"keep as an object with no age limit", strings.Replace(whole, `"name":"created"`,
`"name":"owed","keep":{"max-age-days":0,"last-per-subject":true},"subject":"owed.<repository>"`, 1), ""},
{"keep as an object, last per subject, one subject", strings.Replace(whole, `"name":"created"`,
`"name":"owed","keep":{"last-per-subject":true}`, 1), "give it a subject with parts"},
{"keep as an object with a field it does not know", strings.Replace(whole, `"name":"created"`,
`"name":"owed","keep":{"days":7}`, 1), "keep is a string or an object"},
{"keep as an object with a negative age", strings.Replace(whole, `"name":"created"`,
`"name":"owed","keep":{"max-age-days":-1}`, 1), "events[0].keep.max-age-days: is negative"},
{"the step pipeline and an image rule", strings.Replace(listGrain, `"verbs":`,
`"acts":[{"step":"pipeline"},{"step":"build","on":"kind:image"},{"step":"check"}],"verbs":`, 1), ""},
{"an acting rule on another kind", strings.Replace(listGrain, `"verbs":`, `"acts":[{"step":"build","on":"kind:Image"}],"verbs":`, 1), "acts[0].on"},
{"a verb marked for approval", strings.Replace(whole, `"name":"list",`, `"name":"list","needs-approval":true,`, 1), ""},
{"a verb marked with a word that is no boolean", strings.Replace(whole, `"name":"list",`, `"name":"list","needs-approval":"yes",`, 1), "not a seat definition's JSON"},
{"a bodies section", strings.Replace(whole, `"family":`, `"bodies":{"Finding":{"type":"object"}},"family":`, 1), ""},
{"one kind on two streams", strings.Replace(whole, `"events":`,
`"accepts":[{"name":"step","body":{},"stream":"check"},{"name":"step","body":{},"stream":"delivery"}],"events":`, 1), ""},
{"one kind twice on a stream", strings.Replace(whole, `"events":`,
`"accepts":[{"name":"step","body":{},"stream":"check"},{"name":"step","body":{},"stream":"check"}],"events":`, 1), `"step" twice`},
{"a stream that is none", strings.Replace(whole, `"events":`, `"accepts":[{"name":"step","body":{},"stream":"mail"}],"events":`, 1),
`accepts[0].stream: "mail" is not check or delivery`},
{"an event on a stream", strings.Replace(whole, `"name":"created"`, `"name":"created","stream":"check"`, 1), "only an accepted kind rides a stream"},
{"over 256 KiB", listGrain[:len(listGrain)-1] + `,"description":"` + strings.Repeat("x", MaxSize) + `"}`, {"over 256 KiB", listGrain[:len(listGrain)-1] + `,"description":"` + strings.Repeat("x", MaxSize) + `"}`,
ErrTooLarge.Error()}, ErrTooLarge.Error()},
} }
@@ -230,6 +266,7 @@ func TestAnActingRuleReadsItsStepsDefaults(t *testing.T) {
{ActingRule{Step: StepGrant, Bound: "5m"}, "go", true, "5m0s"}, {ActingRule{Step: StepGrant, Bound: "5m"}, "go", true, "5m0s"},
{ActingRule{Step: StepPlace}, "go", true, "10m0s"}, {ActingRule{Step: StepPlace}, "go", true, "10m0s"},
{ActingRule{Step: StepTakeIn}, "go", true, "0s"}, {ActingRule{Step: StepTakeIn}, "go", true, "0s"},
{ActingRule{Step: StepPipeline}, "go", true, "30m0s"},
} { } {
if got := c.rule.ActsOn(c.language); got != c.acts { if got := c.rule.ActsOn(c.language); got != c.acts {
t.Errorf("%+v acts on %q: %v, want %v", c.rule, c.language, got, c.acts) t.Errorf("%+v acts on %q: %v, want %v", c.rule, c.language, got, c.acts)
@@ -239,3 +276,10 @@ func TestAnActingRuleReadsItsStepsDefaults(t *testing.T) {
} }
} }
} }
// Design 47 G §260, §396: a kept event's stream is SEAT_<seat>_<event>.
func TestAKeptEventsStreamIsNamedFromTheSeatAndTheEvent(t *testing.T) {
if got := KeptStream("mesh-delivery", Message{Name: "owed"}); got != "SEAT_mesh-delivery_owed" {
t.Errorf("the stream is %s", got)
}
}