Issue 127 stood because nothing compared the two halves. Every manifest was well-formed on its own and every derivation correct on its own, and no cross-module subscription in the mesh matched anything — a subscription that matches nothing is not an error, it is silence. Two checks, because the mistake is possible at two scales. Per manifest: an event is a local name, and `module.` is refused with the name to write instead. A module emitting under what reads as another module's name is refused too, pointing at the seat, where a name outlives whoever holds it. Across the catalogue: where a consumed event's emitter is present, it must emit that event. It cannot demand a live emitter for everything — a module lives in its own repository and may be installed long before the one whose events it wants — so the rule is narrower and still catches this. It found two real dangling subscriptions the moment it ran. Wildcards were undecided and two manifests needed them: `*` is one name and `**` is the rest, spelled the mesh's way and derived to `>` here and `#` on the old bus. A manifest naming either would stop being true when the wire changed, which is the whole reason names are local. And the field documentation taught the old form, examples included — which is why the drift was uniform across 37 manifests rather than scattered. Nobody was guessing; everybody followed the comment.
126 lines
3.8 KiB
Go
126 lines
3.8 KiB
Go
package broker
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// Do the emitters and the consumers of a catalogue agree?
|
|
//
|
|
// **The check that was missing** (novox/hq 04-ISSUES/127). Every manifest was individually
|
|
// well-formed and every derivation individually correct, and no cross-module subscription in the
|
|
// mesh matched anything: a consumer's declaration derived into a namespace nobody publishes to.
|
|
// Nothing failed, because a subscription that matches nothing is not an error — it is silence.
|
|
//
|
|
// The comparison has to be over the whole catalogue, because the two halves live in different
|
|
// manifests, and it cannot simply demand that every consumed event have a live emitter: a module
|
|
// may be installed long before the one whose events it wants. So the rule is narrower and still
|
|
// catches this: **where the emitter is present, it must emit what the consumer asked for.**
|
|
|
|
// AConsumer is one module's interest in another's events, as this check needs it.
|
|
type AConsumer struct {
|
|
Module string
|
|
Consumes []string
|
|
}
|
|
|
|
// AnEmitter is one module's events.
|
|
type AnEmitter struct {
|
|
Module string
|
|
Emits []string
|
|
}
|
|
|
|
// Disagreements are the consumed events whose emitter is in the catalogue and does not emit them.
|
|
//
|
|
// Returned as sentences rather than as structs: every one of them is read by a person deciding
|
|
// whether a manifest or a catalogue is wrong, and a pair of names without the reason is a puzzle.
|
|
func Disagreements(emitters []AnEmitter, consumers []AConsumer, seats []DeclaredSeat) []string {
|
|
emits := map[string]map[string]bool{}
|
|
for _, e := range emitters {
|
|
if emits[e.Module] == nil {
|
|
emits[e.Module] = map[string]bool{}
|
|
}
|
|
for _, name := range e.Emits {
|
|
emits[e.Module][name] = true
|
|
}
|
|
}
|
|
// A seat's events are published by its holder under the seat's name, so a consumer naming the
|
|
// seat is naming something real even though no module declares it as its own.
|
|
for _, s := range seats {
|
|
if len(s.Emits) == 0 {
|
|
continue
|
|
}
|
|
if emits[s.Name] == nil {
|
|
emits[s.Name] = map[string]bool{}
|
|
}
|
|
for _, name := range s.Emits {
|
|
emits[s.Name][name] = true
|
|
}
|
|
}
|
|
|
|
var out []string
|
|
for _, c := range consumers {
|
|
for _, pattern := range c.Consumes {
|
|
emitter, event, named := strings.Cut(pattern, ".")
|
|
// Every event from everyone, or every event from one module: both are deliberate and
|
|
// neither names a particular event to check.
|
|
if !named || emitter == "*" || emitter == catalogueTheRest || event == catalogueTheRest {
|
|
continue
|
|
}
|
|
known, present := emits[emitter]
|
|
if !present {
|
|
// Not installed here, which is ordinary: a module lives in its own repository and
|
|
// may be registered later. Nothing to compare, so nothing to say.
|
|
continue
|
|
}
|
|
if matchesAny(event, known) {
|
|
continue
|
|
}
|
|
out = append(out, fmt.Sprintf(
|
|
"%s consumes %q and %s emits %s — so that subscription would match nothing, and "+
|
|
"nothing would report it",
|
|
c.Module, pattern, emitter, listOf(known)))
|
|
}
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
// matchesAny says whether one of an emitter's event names satisfies a consumer's pattern.
|
|
func matchesAny(pattern string, emitted map[string]bool) bool {
|
|
want := strings.Split(pattern, ".")
|
|
for name := range emitted {
|
|
if matches(want, strings.Split(name, ".")) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func matches(pattern, name []string) bool {
|
|
for i, part := range pattern {
|
|
if part == catalogueTheRest {
|
|
return i < len(name)
|
|
}
|
|
if i >= len(name) {
|
|
return false
|
|
}
|
|
if part != "*" && part != name[i] {
|
|
return false
|
|
}
|
|
}
|
|
return len(pattern) == len(name)
|
|
}
|
|
|
|
func listOf(names map[string]bool) string {
|
|
if len(names) == 0 {
|
|
return "nothing"
|
|
}
|
|
out := make([]string, 0, len(names))
|
|
for n := range names {
|
|
out = append(out, n)
|
|
}
|
|
sort.Strings(out)
|
|
return strings.Join(out, ", ")
|
|
}
|