diff --git a/go/buscompose/names_test.go b/go/buscompose/names_test.go index 2f0b5cc..25934e2 100644 --- a/go/buscompose/names_test.go +++ b/go/buscompose/names_test.go @@ -1,8 +1,10 @@ package buscompose import ( + "strings" "testing" + "git.novox.be/novox/mesh-contracts/go/seats/meshbroker" "git.novox.be/novox/mesh-sdk/go/membership" ) @@ -32,4 +34,27 @@ func TestTheBusNamesAgree(t *testing.T) { if err := ledger.Valid(); err != nil || ledger.OwnSecret() != "ledgers" { t.Fatalf("a wanted ledger account: %v, held in %q", err, ledger.OwnSecret()) } + // mesh-broker's events are the bus module's own stream (design 25 § 12.3, 30 days): no subject of the shared + // streams may take them, or the bus refuses the two streams as overlapping. + for _, st := range MeshStreams() { + for _, pattern := range st.Subjects { + if subjectTakes(pattern, meshbroker.SubjectCreated) { + t.Errorf("the stream %s takes mesh-broker's %s by %s", st.Name, meshbroker.SubjectCreated, pattern) + } + } + } +} + +// subjectTakes says whether a subject pattern (`*` one token, `>` the rest) takes a subject. +func subjectTakes(pattern, subject string) bool { + p, s := strings.Split(pattern, "."), strings.Split(subject, ".") + for i, tok := range p { + switch { + case tok == ">": + return len(s) > i + case i >= len(s) || (tok != "*" && tok != s[i]): + return false + } + } + return len(p) == len(s) } diff --git a/go/seatdef/seatdef.go b/go/seatdef/seatdef.go index c915c93..c0b37ae 100644 --- a/go/seatdef/seatdef.go +++ b/go/seatdef/seatdef.go @@ -67,7 +67,8 @@ type Definition struct { Verbs []Verb `json:"verbs,omitempty"` // Accepts are what may be sent to the seat's holder on `mesh.seat..accept.`. Accepts []Message `json:"accepts,omitempty"` - // Events are what is published on the seat's own subject, `mesh.seat..event.`. + // Events are what is published on the seat's own subject, `mesh.seat...` (EventSubject): the + // token is `event`, unless the family names its own. Events []Message `json:"events,omitempty"` // Family is where the seat's events are kept, when the definition fixes it. Family *Family `json:"family,omitempty"` @@ -122,6 +123,18 @@ type Family struct { Name string `json:"name"` MaxAgeDays int `json:"max-age-days"` MaxMiB int `json:"max-mib,omitempty"` + // Token is the subject's word after the seat's name, `event` when absent. A family kept in a stream of its own + // names another, so its subjects stay outside the shared events stream's `mesh.seat.*.event.>`: the bus refuses + // two streams whose subjects overlap (mesh-broker's `said`, design 25 § 12.3). + Token string `json:"token,omitempty"` +} + +// EventToken is the token after the seat's name on the subjects of its events. +func (f *Family) EventToken() string { + if f == nil || f.Token == "" { + return "event" + } + return f.Token } // Record is one state the holder keeps for the seat: its key's form and its value's schema. @@ -377,6 +390,14 @@ func (d Definition) check() Problems { if d.Family.MaxAgeDays < 1 { ps = append(ps, Problem{"family.max-age-days", "missing, or under one day"}) } + switch t := d.Family.Token; { + case t == "": + case !wordName.MatchString(t): + ps = append(ps, Problem{"family.token", fmt.Sprintf("%q is not a lower-case word", t)}) + case sharedTokens[t]: + ps = append(ps, Problem{"family.token", fmt.Sprintf("%q is a token every seat shares; "+ + "a family of its own names another, or none", t)}) + } } for i, e := range d.HolderEvents { where := fmt.Sprintf("holder-events[%d]", i) @@ -496,9 +517,20 @@ func Resolve(d Definition, parts map[string]Part) (Definition, error) { return out, nil } -// EventSubject is where the seat's event is published; by a caller, the caller's name follows. +// EventSubject is where the seat's event is published on the shared events stream's token; by a caller, the +// caller's name follows. A definition whose family names a token of its own is asked with Definition.EventSubject. func EventSubject(seat, event string) string { return "mesh.seat." + seat + ".event." + event } +// EventSubject is where this seat's event is published, under its family's token; by a caller, the caller's name +// follows. +func (d Definition) EventSubject(event string) string { + return "mesh.seat." + d.Seat + "." + d.Family.EventToken() + "." + event +} + +// sharedTokens are the words after a seat's name that every seat's subjects share (its verbs, its work queue, the +// shared events stream's events): a family's own token is none of them. +var sharedTokens = map[string]bool{"event": true, "accept": true, "tool": true, "call": true, "proof": true} + // AcceptSubject is where a message the seat accepts is sent. func AcceptSubject(seat, accept string) string { return "mesh.seat." + seat + ".accept." + accept } diff --git a/go/seatdef/seatdef_test.go b/go/seatdef/seatdef_test.go index e1ca81e..9c9a52f 100644 --- a/go/seatdef/seatdef_test.go +++ b/go/seatdef/seatdef_test.go @@ -37,6 +37,11 @@ func TestADefinitionIsReadStrictlyNamingWhatIsWrong(t *testing.T) { `"name":"created"`, 1), "events[0].body: missing"}, {"whole events kept nowhere", strings.Replace(whole, `,"family":{"name":"broker.events","max-age-days":30}`, "", 1), "family: missing"}, + {"a family's own token parses", strings.Replace(whole, `"max-age-days":30}`, `"max-age-days":30,"token":"said"}`, 1), ""}, + {"a family's token shared by every seat", strings.Replace(whole, `"max-age-days":30}`, + `"max-age-days":30,"token":"event"}`, 1), "family.token: \"event\" is a token every seat shares"}, + {"a family's token not a word", strings.Replace(whole, `"max-age-days":30}`, + `"max-age-days":30,"token":"sa.id"}`, 1), "family.token"}, {"a verb twice", strings.Replace(listGrain, `]}`, `,{"name":"add"}]}`, 1), `"add" twice`}, {"records at the seat list's grain", strings.Replace(listGrain, `"decision":"ADR 1",`, `"decision":"ADR 1","records":[{"name":"asks"}],`, 1), "grain: seat-list holds"}, @@ -89,6 +94,18 @@ func TestADefinitionIsReadStrictlyNamingWhatIsWrong(t *testing.T) { t.Errorf("a type defined twice: %v", err) } + // An event's subject takes its family's token: the shared events stream's `event`, or the family's own. + for _, c := range []struct{ token, want string }{{"", "mesh.seat.mesh-broker.event.created"}, + {"said", "mesh.seat.mesh-broker.said.created"}} { + d, err := Parse([]byte(strings.Replace(whole, `"max-age-days":30}`, `"max-age-days":30,"token":"`+c.token+`"}`, 1))) + if err != nil { + t.Fatal(err) + } + if got := d.EventSubject("created"); got != c.want { + t.Errorf("token %q: subject %q, want %q", c.token, got, c.want) + } + } + // Fixtures name what they are an instance of. if _, err := ParseFixtures([]byte(`[{"name":"a","of":"verb:list.input","given":{}},{"name":"b","of":"event:created", "subject":"mesh.seat.mesh-broker.event.created","given":{}}]`)); err != nil {