SD-M2b sdk: a seat's event family may name its own subject token
The lead's ruling of 12:5x (design 25 § 12.3): mesh-broker's events ride the bus module's own stream, 30 days, on mesh.seat.mesh-broker.said.<event>, outside the shared events stream's mesh.seat.*.event.>, so the bus never refuses the two as overlapping. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
+34
-2
@@ -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.<seat>.accept.<name>`.
|
||||
Accepts []Message `json:"accepts,omitempty"`
|
||||
// Events are what is published on the seat's own subject, `mesh.seat.<seat>.event.<name>`.
|
||||
// Events are what is published on the seat's own subject, `mesh.seat.<seat>.<token>.<name>` (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 }
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user