diff --git a/modules/mesh-grants/internal/grants/inputs.go b/modules/mesh-grants/internal/grants/inputs.go index 0308ae7a..1db94a80 100644 --- a/modules/mesh-grants/internal/grants/inputs.go +++ b/modules/mesh-grants/internal/grants/inputs.go @@ -192,6 +192,8 @@ type Inputs struct { EventSubjects map[string]map[string]string // AcceptCallers are, by pulled-by-kind seat and queue, the seats that may ask it (acceptCallersOf). AcceptCallers map[string]map[string][]string + // VerbCallers are, by seat and verb, the callers each verb of the definitions admits (verbCallersOf). + VerbCallers map[string]map[string][]string // Defined are the events a seat definition of the seat list defines, by local name, and mesh-sdk's finding events: the // events a `*` emitter before them reaches each declaring module's own stream for (design 47 G §386, §474, §644). Defined map[string]bool @@ -373,6 +375,7 @@ func ReadInputs(r Reader, cache *Manifests) Inputs { in.Chunked = buscompose.ChunkVerbsOf(d.Definitions()) in.EventSubjects = eventSubjectsOf(d.Definitions()) in.AcceptCallers = acceptCallersOf(d.Definitions()) + in.VerbCallers = verbCallersOf(d.Definitions()) in.Defined = definedEventsOf(d.Definitions()) } nodes := map[string]bool{} diff --git a/modules/mesh-grants/internal/grants/records.go b/modules/mesh-grants/internal/grants/records.go index 5fce46a5..2fa9228b 100644 --- a/modules/mesh-grants/internal/grants/records.go +++ b/modules/mesh-grants/internal/grants/records.go @@ -102,6 +102,9 @@ func records(in Inputs, union bool) (buscompose.Records, []Gap) { d.Watches, d.Consumes, d.Hears, keptHeard = keptOutOfHearing(in.Kept, d.Watches, d.Consumes, d.Hears) d.HearsAStream = d.HearsAStream || keptHeard d.Reads, d.ReadsObjects, d.ReadsRefused = readsOfObjects(m.Module, node, d.Reads, d.ReadsRefused, declared, values) + // The sent stores of the modules it is for (design 47 G §59, §124), read from the catalog graph's manifests and the + // definitions' callers. + d.ReadsObjects = append(d.ReadsObjects, sentStoresRead(m, in.Manifests, in.VerbCallers)...) // Its bindings as kept (bindings.go): the same source its membership's `bindings` is written from in this pass, // so a grant and its binding are written together, never one before the other (issue 1966, the kind of issue 249). d.Bindings = in.Bindings[m.Module] diff --git a/modules/mesh-grants/internal/grants/sentobjects.go b/modules/mesh-grants/internal/grants/sentobjects.go new file mode 100644 index 00000000..fc0082f1 --- /dev/null +++ b/modules/mesh-grants/internal/grants/sentobjects.go @@ -0,0 +1,86 @@ +package grants + +import ( + "slices" + "strings" + + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/seatdef" +) + +// **A module's sent store is read by the modules it is for** (design 47 G §59, §124): the grants part derives, from the +// catalog graph and the seat definitions, never from a manifest field, a standing read of `_sent-objects` for the +// callers the module's seat definition names for the verbs it serves, and for the consumers whose `consumes` declare its +// events. The grant follows the catalog graph, so it is composed with each module's declaration at every pass. + +// verbCallersOf is, by seat and verb, the callers each verb of the definitions admits (a verb naming none takes its class's +// default). +func verbCallersOf(defs map[string]seatdef.Definition) map[string]map[string][]string { + out := map[string]map[string][]string{} + for _, d := range defs { + for _, v := range d.Verbs { + if out[d.Seat] == nil { + out[d.Seat] = map[string][]string{} + } + out[d.Seat][v.Name] = v.EffectiveCallers() + } + } + return out +} + +// admits says whether a verb's callers name the module: `invokers` (every module whose manifest invokes the verb), the module +// itself, or a seat it claims. +func admits(callers []string, m catalogue.Manifest) bool { + for _, c := range callers { + switch { + case c == seatdef.CallerInvokers, c == seatdef.CallerModulePrefix+m.Module: + return true + } + if seat, ok := strings.CutPrefix(c, seatdef.CallerSeatPrefix); ok && slices.ContainsFunc(m.Claims, func(x catalogue.Claim) bool { return x.Name == seat }) { + return true + } + } + return false +} + +// sentStoresRead are the bus names of the sent stores the module m reads: of each module whose event it consumes (the module's +// own, or the seat a module claims), and of each module claiming a seat whose verb m invokes where the verb's callers admit m. +// Never m's own store, which its binding grants it. +func sentStoresRead(m catalogue.Manifest, all map[string]catalogue.Manifest, callers map[string]map[string][]string) []string { + claimants := func(name string) []string { + var out []string + if _, ok := all[name]; ok { + out = append(out, name) + } + for module, x := range all { + if slices.ContainsFunc(x.Claims, func(c catalogue.Claim) bool { return c.Name == name }) { + out = append(out, module) + } + } + return out + } + var stores []string + for _, c := range m.Consumes { + name, _, ok := strings.Cut(strings.TrimPrefix(c, "seat:"), ".") + if !ok || name == "*" { + continue + } + for _, module := range claimants(name) { + stores = append(stores, catalogue.SentObjectsName(module)) + } + } + for _, in := range m.Invokes { + seat, verb, ok := catalogue.SeatInvoke(in) + if !ok || !admits(callers[seat][verb], m) { + continue + } + for module, x := range all { + if slices.ContainsFunc(x.Claims, func(c catalogue.Claim) bool { return c.Name == seat }) { + stores = append(stores, catalogue.SentObjectsName(module)) + } + } + } + stores = slices.DeleteFunc(stores, func(s string) bool { return s == catalogue.SentObjectsName(m.Module) }) + slices.Sort(stores) + return slices.Compact(stores) +} diff --git a/modules/mesh-grants/internal/grants/sentobjects_test.go b/modules/mesh-grants/internal/grants/sentobjects_test.go new file mode 100644 index 00000000..28e7c6cd --- /dev/null +++ b/modules/mesh-grants/internal/grants/sentobjects_test.go @@ -0,0 +1,38 @@ +package grants + +import ( + "slices" + "testing" + + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/seatdef" +) + +// The sent stores a module reads follow the catalog graph (design 47 G §59, §124): the consumers of a module's events and the +// callers a seat's verb admits, and no other module. +func TestSentStoresRead(t *testing.T) { + holder := catalogue.Manifest{Module: "shop", Claims: []catalogue.Claim{{Name: "orders-seat"}}} + all := map[string]catalogue.Manifest{"shop": holder, "bank": {Module: "bank"}, "plain": {Module: "plain"}} + callers := map[string]map[string][]string{"orders-seat": { + "place": {"seat:billing", "module:plain"}, "list": seatdef.DefaultCallers(seatdef.ClassRead), "wipe": seatdef.DefaultCallers(seatdef.ClassAct)}} + for _, c := range []struct { + name string + m catalogue.Manifest + want []string + }{ + {"consumes a module's event", catalogue.Manifest{Module: "bank", Consumes: []string{"shop.sold"}}, []string{"shop_sent-objects"}}, + {"consumes a seat's event", catalogue.Manifest{Module: "bank", Consumes: []string{"orders-seat.sold"}}, []string{"shop_sent-objects"}}, + {"invokes a verb that names its seat", catalogue.Manifest{Module: "bank", Claims: []catalogue.Claim{{Name: "billing"}}, + Invokes: []string{"seat:orders-seat.place"}}, []string{"shop_sent-objects"}}, + {"invokes a verb that names it", catalogue.Manifest{Module: "plain", Invokes: []string{"seat:orders-seat.place"}}, []string{"shop_sent-objects"}}, + {"invokes a read, its callers include invokers", catalogue.Manifest{Module: "bank", Invokes: []string{"seat:orders-seat.list"}}, []string{"shop_sent-objects"}}, + {"invokes an act no one names it for", catalogue.Manifest{Module: "bank", Invokes: []string{"seat:orders-seat.wipe"}}, nil}, + {"invokes a verb that names others", catalogue.Manifest{Module: "bank", Invokes: []string{"seat:orders-seat.place"}}, nil}, + {"declares neither", catalogue.Manifest{Module: "bank"}, nil}, + {"its own store is its binding's", holder, nil}, + } { + if got := sentStoresRead(c.m, all, callers); !slices.Equal(got, c.want) { + t.Errorf("%s: %v, want %v", c.name, got, c.want) + } + } +}