Grant a seat's traffic by caller and by kind, so an ask's asker and a channel's kind are facts the bus enforces (hq ADR 0259)
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
package catalogue
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// The router of novox/hq ADR 0259: it declares the operator's seat and the two kinded benches.
|
||||
func router() Manifest {
|
||||
return Manifest{Module: "messenger", Tools: []string{"open", "history", "notify"},
|
||||
State: []StateDeclaration{{Name: "asks"}},
|
||||
DefinesSeats: []SeatDeclaration{
|
||||
{Name: "operator-channel", Scope: ScopeMesh, Accepts: []string{"ask", "cancel"}, Emits: []string{"decided"},
|
||||
ByCaller: []string{"ask", "cancel", "decided"}, Records: []string{"asks"},
|
||||
Serves: []Verb{{Name: "open"}, {Name: "history"}, {Name: "notify"}}},
|
||||
{Name: "channel", Scope: ScopeMesh, Kinded: true, Accepts: []string{"show", "edit", "send"}},
|
||||
{Name: "intake", Scope: ScopeMesh, Kinded: true, Emits: []string{"choice", "link"}, Proofs: []string{"code"}},
|
||||
},
|
||||
Claims: []Claim{{Name: "operator-channel", Scope: ScopeMesh}},
|
||||
Uses: []string{"channel"}}
|
||||
}
|
||||
|
||||
func aChannel(module, kind string) Manifest {
|
||||
return Manifest{Module: module, Claims: []Claim{
|
||||
{Name: "channel", Scope: ScopeMesh, Kind: kind}, {Name: "intake", Scope: ScopeMesh, Kind: kind}}}
|
||||
}
|
||||
|
||||
func TestTwoChannelsOfDifferentKindsHoldTheBenches(t *testing.T) {
|
||||
shelf := Shelf{"messenger": router(), "telegram": aChannel("telegram", "telegram"),
|
||||
"desk-channel": aChannel("desk-channel", "desktop")}
|
||||
if got := problemsFor(t, shelf); got != "" {
|
||||
t.Fatalf("two kinds were refused: %s", got)
|
||||
}
|
||||
for _, m := range shelf {
|
||||
if got := declaredSeatProblems(m); len(got) > 0 {
|
||||
t.Fatalf("%s: %v", m.Module, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestASecondClaimOfOneKindIsRefused(t *testing.T) {
|
||||
got := problemsFor(t, Shelf{"messenger": router(), "telegram": aChannel("telegram", "telegram"),
|
||||
"telegram-two": aChannel("telegram-two", "telegram")})
|
||||
if !strings.Contains(got, `of kind "telegram", which telegram already claims`) {
|
||||
t.Fatalf("a second holder of one kind stood: %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAKindedBenchNeedsAKindAndNoOtherSeatTakesOne(t *testing.T) {
|
||||
got := problemsFor(t, Shelf{"messenger": router(), "nameless": aChannel("nameless", "")})
|
||||
if !strings.Contains(got, "claims the kinded bench channel and names no kind") {
|
||||
t.Fatalf("a claim without a kind stood: %s", got)
|
||||
}
|
||||
odd := Manifest{Module: "odd", Claims: []Claim{{Name: "operator-channel", Scope: ScopeMesh, Kind: "telegram"}}}
|
||||
got = problemsFor(t, Shelf{"messenger": router(), "odd": odd})
|
||||
if !strings.Contains(got, "only a kinded bench takes a kind") {
|
||||
t.Fatalf("a kind on a seat that is not kinded stood: %s", got)
|
||||
}
|
||||
dotted := problemsFor(t, Shelf{"messenger": router(), "dotted": aChannel("dotted", "a.b")})
|
||||
if !strings.Contains(dotted, "not a usable name") {
|
||||
t.Fatalf("a kind that would widen a subject stood: %s", dotted)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOnlyChannelAndIntakeAreKinded(t *testing.T) {
|
||||
m := Manifest{Module: "x", DefinesSeats: []SeatDeclaration{{Name: "pager", Kinded: true, Accepts: []string{"page"}}}}
|
||||
if got := strings.Join(declaredSeatProblems(m), "; "); !strings.Contains(got, "only channel and intake are kinded") {
|
||||
t.Fatalf("another kinded bench was declared: %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTheNewRulesAreHeldToWhatTheSeatSays(t *testing.T) {
|
||||
m := Manifest{Module: "x", DefinesSeats: []SeatDeclaration{{Name: "thing", Accepts: []string{"do"},
|
||||
ByCaller: []string{"undo"}, Proofs: []string{"code"}, Records: []string{"nothing"}}}}
|
||||
got := strings.Join(declaredSeatProblems(m), "; ")
|
||||
for _, want := range []string{"names thing.undo by its caller, which the seat neither accepts nor emits",
|
||||
"declares proofs on thing, which is not kinded", `read its records "nothing", which it keeps no state of`} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Errorf("not refused: %q in %s", want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Two kinds on one machine are two holders, and one kind on two machines is a second claimant.
|
||||
func TestEachKindIsItsOwnHolderWhenResolved(t *testing.T) {
|
||||
modules := []Manifest{aChannel("telegram", "telegram"), aChannel("desk-channel", "desktop")}
|
||||
held, problems := checkClaims(modules, Node{Name: "anchor"}, nil, nil)
|
||||
if len(problems) > 0 || len(held) != 4 {
|
||||
t.Fatalf("two kinds on one machine: held %v, problems %v", held, problems)
|
||||
}
|
||||
_, problems = checkClaims([]Manifest{aChannel("telegram", "telegram")}, Node{Name: "home"}, held, nil)
|
||||
if len(problems) == 0 {
|
||||
t.Fatal("one kind held on two machines was not refused")
|
||||
}
|
||||
}
|
||||
@@ -64,6 +64,9 @@ type Claim struct {
|
||||
// text/template over one piece — its fields and `module` — in the tool's own grammar (novox/hq ADR
|
||||
// 0255). The data is the mesh's, the format the holder's, as a module's facts template is.
|
||||
Renders map[string]string `json:"renders,omitempty"`
|
||||
// Kind is the kind this module holds a kinded bench as (novox/hq ADR 0234 §2, ADR 0259): `telegram`,
|
||||
// `desktop`. Refused on any other seat, and a second claim of one kind is refused.
|
||||
Kind string `json:"kind,omitempty"`
|
||||
}
|
||||
|
||||
// ServesFor is what this claim offers a seat's protocol: the verbs it names, else the module's
|
||||
|
||||
@@ -120,6 +120,16 @@ type Held struct {
|
||||
Node string
|
||||
Module string
|
||||
Site string
|
||||
// Kind is the kind a kinded bench is held as (novox/hq ADR 0234 §2): each kind is its own holder.
|
||||
Kind string
|
||||
}
|
||||
|
||||
// heldKey is what one holder holds: the seat, and its kind on a kinded bench.
|
||||
func heldKey(claim, kind string) string {
|
||||
if kind == "" {
|
||||
return canonicalSeat(claim)
|
||||
}
|
||||
return canonicalSeat(claim) + "/" + kind
|
||||
}
|
||||
|
||||
// Resolution is what a node should run, and why.
|
||||
@@ -874,7 +884,7 @@ func checkClaims(modules []Manifest, node Node, elsewhere []Held, holdings []Hel
|
||||
// **One seat under either of its names** (novox/hq ADR 0122): a manifest registered before
|
||||
// a rename claims the former name, and one written after it the current — two claimants of
|
||||
// one seat, compared by the seat they resolve to and not by how each spelled it.
|
||||
seat := canonicalSeat(c.Name)
|
||||
seat := heldKey(c.Name, c.Kind)
|
||||
if other, taken := byScope[scope][seat]; taken {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s and %s both claim %q, and only one thing may hold it per %s",
|
||||
@@ -883,14 +893,14 @@ func checkClaims(modules []Manifest, node Node, elsewhere []Held, holdings []Hel
|
||||
}
|
||||
byScope[scope][seat] = m.Module
|
||||
held = append(held, Held{Claim: c.Name, Scope: scope, Node: node.Name,
|
||||
Module: m.Module, Site: node.Site})
|
||||
Module: m.Module, Site: node.Site, Kind: c.Kind})
|
||||
}
|
||||
}
|
||||
|
||||
// And against the rest of the mesh, for the scopes that reach past this machine.
|
||||
for _, h := range held {
|
||||
for _, e := range elsewhere {
|
||||
if e.Node == node.Name || canonicalSeat(e.Claim) != canonicalSeat(h.Claim) || e.Scope != h.Scope {
|
||||
if e.Node == node.Name || heldKey(e.Claim, e.Kind) != heldKey(h.Claim, h.Kind) || e.Scope != h.Scope {
|
||||
continue
|
||||
}
|
||||
switch h.Scope {
|
||||
|
||||
@@ -51,6 +51,35 @@ type SeatDeclaration struct {
|
||||
// owns its own, which is why a seat is also the answer for a module that needs retention
|
||||
// its events cannot have.
|
||||
RetainSeconds int `json:"retain-seconds,omitempty"`
|
||||
|
||||
// Kinded makes the seat a kinded bench (novox/hq ADR 0234 §2, ADR 0259 §3): its holders are different
|
||||
// modules, each claiming one kind, and each verb's subject carries the kind. Only the benches in
|
||||
// KindedBenches may be kinded; making another is a decision, recorded.
|
||||
Kinded bool `json:"kinded,omitempty"`
|
||||
// ByCaller are accepts and emits whose subject's last token names the calling module (ADR 0259 §3): a
|
||||
// user submits such an accept, and hears such an event, under its own name and no other.
|
||||
ByCaller []string `json:"by-caller,omitempty"`
|
||||
// Proofs are verbs carried as core request and reply, never on a stream: what travels on them (a code
|
||||
// the operator typed) is never kept (ADR 0259 §3). On a kinded bench a holder asks with its own kind and
|
||||
// the modules watching the seat answer.
|
||||
Proofs []string `json:"proofs,omitempty"`
|
||||
// Records are state buckets of the declaring module that each user reads under its own name — the
|
||||
// keys `<user>.…` and no other (ADR 0259 §3).
|
||||
Records []string `json:"records,omitempty"`
|
||||
}
|
||||
|
||||
// KindedBenches are the seats that may be kinded (novox/hq ADR 0234 §2): `channel` sends to the operator,
|
||||
// `intake` takes what the operator answers. Another is a decision, recorded, as ADR 0223 asks of a bench.
|
||||
var KindedBenches = map[string]bool{"channel": true, "intake": true}
|
||||
|
||||
// NamedByCaller says whether one of the seat's verbs is named by its caller.
|
||||
func (s SeatDeclaration) NamedByCaller(verb string) bool {
|
||||
for _, v := range s.ByCaller {
|
||||
if v == verb {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// At is this declaration's scope, with the default applied. Mesh by default, because a seat
|
||||
@@ -132,6 +161,7 @@ func declaredSeatProblems(m Manifest) []string {
|
||||
"%s declares %s.%s, which is not a usable verb", m.Module, s.Name, v))
|
||||
}
|
||||
}
|
||||
problems = append(problems, trafficProblems(m, s)...)
|
||||
}
|
||||
|
||||
for _, u := range m.Uses {
|
||||
@@ -142,6 +172,56 @@ func declaredSeatProblems(m Manifest) []string {
|
||||
return problems
|
||||
}
|
||||
|
||||
// trafficProblems is what one declaration of the rules of ADR 0259 §3 can be judged on alone.
|
||||
func trafficProblems(m Manifest, s SeatDeclaration) []string {
|
||||
var problems []string
|
||||
if s.Kinded && !KindedBenches[s.Name] {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s declares %s as a kinded bench; only channel and intake are kinded, and another is a "+
|
||||
"decision, recorded (novox/hq ADR 0234)", m.Module, s.Name))
|
||||
}
|
||||
if s.Kinded && len(s.ByCaller) > 0 {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s declares %s kinded and names verbs by their caller; a kinded bench's subjects carry the kind",
|
||||
m.Module, s.Name))
|
||||
}
|
||||
for _, v := range s.ByCaller {
|
||||
inAccepts, inEmits := false, false
|
||||
for _, a := range s.Accepts {
|
||||
inAccepts = inAccepts || a == v
|
||||
}
|
||||
for _, e := range s.Emits {
|
||||
inEmits = inEmits || e == v
|
||||
}
|
||||
if !inAccepts && !inEmits {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s names %s.%s by its caller, which the seat neither accepts nor emits", m.Module, s.Name, v))
|
||||
}
|
||||
}
|
||||
for _, v := range s.Proofs {
|
||||
if !name.MatchString(v) || strings.Contains(v, ".") {
|
||||
problems = append(problems, fmt.Sprintf("%s declares the proof %s.%s, which is not a usable verb",
|
||||
m.Module, s.Name, v))
|
||||
}
|
||||
}
|
||||
if len(s.Proofs) > 0 && !s.Kinded {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s declares proofs on %s, which is not kinded; a proof is asked by a holder of a kind",
|
||||
m.Module, s.Name))
|
||||
}
|
||||
for _, r := range s.Records {
|
||||
kept := false
|
||||
for _, st := range m.State {
|
||||
kept = kept || st.Name == r
|
||||
}
|
||||
if !kept {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s says %s's users read its records %q, which it keeps no state of", m.Module, s.Name, r))
|
||||
}
|
||||
}
|
||||
return problems
|
||||
}
|
||||
|
||||
// A Shelf is every manifest the mesh has registered, by module name.
|
||||
type Shelf map[string]Manifest
|
||||
|
||||
@@ -182,8 +262,19 @@ func CatalogueProblems(shelf Shelf) []string {
|
||||
return ok
|
||||
}
|
||||
|
||||
// Who claims each kind of a kinded bench, so a second claim of one kind is refused (ADR 0234 §2).
|
||||
kindsTaken := map[string]string{}
|
||||
|
||||
for _, module := range shelfOrder(shelf) {
|
||||
m := shelf[module]
|
||||
for _, c := range m.Claims {
|
||||
if c.Kind != "" {
|
||||
if _, isModuleSeat := declared[c.Name]; !isModuleSeat {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s claims %s of kind %q, and only a kinded bench takes a kind", module, c.Name, c.Kind))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A `uses` naming nothing is where ADR 0110's guarantee lands under a derived set: the
|
||||
// same refusal, at the same moment, from a set nobody maintains by hand.
|
||||
@@ -214,6 +305,7 @@ func CatalogueProblems(shelf Shelf) []string {
|
||||
}
|
||||
continue
|
||||
}
|
||||
problems = append(problems, kindProblems(module, c, s, kindsTaken)...)
|
||||
if c.At() != s.At() {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s claims %s at scope %q, and %s declares it at %s",
|
||||
@@ -239,6 +331,29 @@ func CatalogueProblems(shelf Shelf) []string {
|
||||
return problems
|
||||
}
|
||||
|
||||
// kindProblems is a claim judged against a declared seat's kind: a kinded bench takes one claim per kind,
|
||||
// a usable name; any other seat takes none.
|
||||
func kindProblems(module string, c Claim, s SeatDeclaration, taken map[string]string) []string {
|
||||
switch {
|
||||
case !s.Kinded && c.Kind != "":
|
||||
return []string{fmt.Sprintf("%s claims %s of kind %q, and only a kinded bench takes a kind",
|
||||
module, c.Name, c.Kind)}
|
||||
case !s.Kinded:
|
||||
return nil
|
||||
case c.Kind == "":
|
||||
return []string{fmt.Sprintf("%s claims the kinded bench %s and names no kind", module, c.Name)}
|
||||
case !name.MatchString(c.Kind) || strings.Contains(c.Kind, "."):
|
||||
return []string{fmt.Sprintf("%s claims %s of kind %q, which is not a usable name", module, c.Name, c.Kind)}
|
||||
}
|
||||
key := c.Name + "/" + c.Kind
|
||||
if first, ok := taken[key]; ok && first != module {
|
||||
return []string{fmt.Sprintf("%s claims %s of kind %q, which %s already claims; a kind has one holder",
|
||||
module, c.Name, c.Kind, first)}
|
||||
}
|
||||
taken[key] = module
|
||||
return nil
|
||||
}
|
||||
|
||||
// unserved is what a seat's protocol promises and the claimant does not answer. Only the tools
|
||||
// are checked: `accepts` and `emits` are wired by the runtime from the declaration, while a tool
|
||||
// is code the module either has or has not written — under the claim's serves, or among its own.
|
||||
|
||||
Reference in New Issue
Block a user