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:
@@ -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 (`<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;
|
||||
// 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, `<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
|
||||
// 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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -1,18 +1,28 @@
|
||||
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
|
||||
// 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/<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 {
|
||||
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/<name>`, `#/bodies/<name>`).
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+76
-16
@@ -93,6 +93,9 @@ type Definition struct {
|
||||
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"`
|
||||
// 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
|
||||
// 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.<node>.<module>, so the last build of every module on every node stays on the bus however long ago it
|
||||
// was said, and a durable consumer starting from the last message per subject holds them all); or KeepStream,
|
||||
// every message within MaxAgeDays and MaxMiB.
|
||||
Keep 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
|
||||
// (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: `<seat>.<event>`, as a family is `<seat>.events`.
|
||||
func KeptStream(seat string, m Message) string { return seat + "." + m.Name }
|
||||
// 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 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 != "" {
|
||||
|
||||
@@ -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.<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) + `"}`,
|
||||
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_<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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user