diff --git a/go/catalogue/manifest.go b/go/catalogue/manifest.go index 765bdcf..5d501f9 100644 --- a/go/catalogue/manifest.go +++ b/go/catalogue/manifest.go @@ -465,6 +465,11 @@ type Manifest struct { // choosing `kc` for keycloak keeps the identity legible where a hash would not. Slug string `json:"slug,omitempty"` + // NeedsApproval marks tools the module serves (``), seat verbs it serves (`.`) 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; // this is for the rest — `zsh` provides `shell`, `xorg` provides `display-server`. Provides []Offer `json:"provides,omitempty"` @@ -1236,6 +1241,11 @@ type Artifact struct { // DefaultSeries (SeriesOf). Series string `json:"series,omitempty"` + // Needs are the artifact references, `:`, 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 // 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 diff --git a/go/catalogue/reader_release_test.go b/go/catalogue/reader_release_test.go new file mode 100644 index 0000000..43cdf35 --- /dev/null +++ b/go/catalogue/reader_release_test.go @@ -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) + } +} diff --git a/go/seatdef/callers.go b/go/seatdef/callers.go new file mode 100644 index 0000000..177e4c1 --- /dev/null +++ b/go/seatdef/callers.go @@ -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:`, `module:` and +// `holder:@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:, module: and "+ + "holder:@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 +} diff --git a/go/seatdef/refs.go b/go/seatdef/refs.go index 435005a..202f087 100644 --- a/go/seatdef/refs.go +++ b/go/seatdef/refs.go @@ -1,18 +1,28 @@ package seatdef -import "strings" - // ResolveRefs is a schema (its all-string lists as []string, StringLists) with every `{"$ref": "#/defs/"}` 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 // 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/` 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 { 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 } +// resolveValue resolves against refs, each reference as written (`#/defs/`, `#/bodies/`). func resolveValue(v any, defs map[string]map[string]any, depth int) any { if depth > 32 { return v @@ -20,7 +30,7 @@ func resolveValue(v any, defs map[string]map[string]any, depth int) any { switch x := v.(type) { case map[string]any: 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) } } diff --git a/go/seatdef/refs_test.go b/go/seatdef/refs_test.go index 59576a4..9358da6 100644 --- a/go/seatdef/refs_test.go +++ b/go/seatdef/refs_test.go @@ -11,6 +11,7 @@ func TestResolveRefsPutsNamedTypesInPlace(t *testing.T) { "id": {"type": "string"}, "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 { name string 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", "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 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"}}}, map[string]any{"oneOf": []any{map[string]any{"type": "string"}}}}, } 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) } } diff --git a/go/seatdef/seatdef.go b/go/seatdef/seatdef.go index e77fbbb..d283899 100644 --- a/go/seatdef/seatdef.go +++ b/go/seatdef/seatdef.go @@ -93,6 +93,9 @@ type Definition struct { Paths []Path `json:"paths,omitempty"` // Defs are named types a schema refers to as `{"$ref": "#/defs/"}`. 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/"}`. 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 // 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. @@ -118,14 +121,19 @@ const ( 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} +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. @@ -139,6 +147,8 @@ func DefaultBound(step string) time.Duration { return 20 * time.Minute case StepGrant, StepPlace: return 10 * time.Minute + case StepPipeline: + return 30 * time.Minute } return 0 } @@ -177,8 +187,8 @@ func checkActs(rs []ActingRule) Problems { 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 && !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)}) + 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 { @@ -204,10 +214,21 @@ type Verb struct { 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"` + // 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"` + // 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. 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 @@ -227,7 +248,12 @@ type Message struct { // running.., 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 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 `.`, 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 // (design 47 *What the gate owes the forge*: the stream `owed`, 7 days and at most 64 MiB). Given with KeepStream // alone. @@ -243,8 +269,19 @@ const ( KeepStream = "stream" ) -// KeptStream is the stream an event with Keep is kept in: `.`, as a family is `.events`. -func KeptStream(seat string, m Message) string { return seat + "." + m.Name } +// KeptStream is the stream an event with Keep is kept in: `SEAT__` (design 47 G §260, §396). +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 // 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)}) } 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"}) } @@ -782,22 +823,41 @@ func checkMessages(field string, ms []Message, whole bool) Problems { } if !named.MatchString(m.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)}) } - seen[m.Name] = true + seen[m.Stream+"/"+m.Name] = true 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"}) - case m.Keep != "" && m.Keep != KeepLastPerSubject && m.Keep != KeepStream: - ps = append(ps, Problem{where + ".keep", fmt.Sprintf("%q is not %s or %s", m.Keep, KeepLastPerSubject, KeepStream)}) - case m.Keep == KeepLastPerSubject && len(m.Parts()) == 0 && !m.ByCaller: + 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 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"}) - 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"}) } + 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 != "" { diff --git a/go/seatdef/seatdef_test.go b/go/seatdef/seatdef_test.go index 548207e..abaa972 100644 --- a/go/seatdef/seatdef_test.go +++ b/go/seatdef/seatdef_test.go @@ -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 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), - `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), "acts[0].on"}, {"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"}, {"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"}, + // 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."`, 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) + `"}`, ErrTooLarge.Error()}, } @@ -230,6 +266,7 @@ func TestAnActingRuleReadsItsStepsDefaults(t *testing.T) { {ActingRule{Step: StepGrant, Bound: "5m"}, "go", true, "5m0s"}, {ActingRule{Step: StepPlace}, "go", true, "10m0s"}, {ActingRule{Step: StepTakeIn}, "go", true, "0s"}, + {ActingRule{Step: StepPipeline}, "go", true, "30m0s"}, } { 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) @@ -239,3 +276,10 @@ func TestAnActingRuleReadsItsStepsDefaults(t *testing.T) { } } } + +// Design 47 G §260, §396: a kept event's stream is SEAT__. +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) + } +}