package catalogue import ( "fmt" "regexp" "strings" ) // What a module may call an event, and what a consumer may ask for. // // A module names an event **locally**: `order.placed`, not a subject and not a routing key // (design 29 §1). A consumer names the emitter and the event: `billing.order.placed`. The mesh // derives the subject from those, so reorganising the subject space leaves every manifest correct. // // **Nothing checked this until every manifest in the catalogue was wrong the same way** // (novox/hq 04-ISSUES/127). All thirty-seven kept the old bus's routing key — // `module..` — which the derivation read as "a module called `module`", so every // cross-module subscription in the mesh pointed at a namespace nobody publishes to. Nothing failed: // the services started and none of them reacted. The documentation on these fields taught the old // form too, which is why the drift was uniform rather than scattered. // eventName is one name in a local event: lower-case, and no wildcard. var eventName = regexp.MustCompile(`^[a-z0-9][a-z0-9-]*$`) // The wildcards a consumer may use, spelled the mesh's way and derived to whatever the transport // spells them as. // // **A manifest holds no transport token**, which is the whole point of naming locally: the bus the // mesh runs on today spells these `*` and `#`, and the one being built spells them `*` and `>`. A // manifest that said either would be a manifest that stopped being true when the wire changed. const ( // OneName stands for exactly one name. OneName = "*" // TheRest stands for one or more names, and may only come last. TheRest = "**" ) // EventProblems is what is wrong with a manifest's events. // // Refused at registration, because the alternative is a module that installs, starts, connects and // reacts to nothing — and every log line says it is fine. func EventProblems(m Manifest) []string { var problems []string for _, e := range m.Emits { if was, stale := staleEventForm(e, m.Module); stale { problems = append(problems, fmt.Sprintf( "%s emits %q, which is the old bus's routing key. An event is named locally now, so "+ "write %q — the mesh derives the subject (novox/hq design 29 §1)", m.Module, was, strings.TrimPrefix(was, "module."+m.Module+"."))) continue } if strings.HasPrefix(e, "module.") { problems = append(problems, fmt.Sprintf( "%s emits %q: `module.` is reserved, because it is how the old bus spelled a "+ "routing key and an event named that way derives into a namespace nobody owns", m.Module, e)) continue } if err := localName(e); err != nil { problems = append(problems, fmt.Sprintf("%s emits %q: %v", m.Module, e, err)) continue } // **Its own name, never another's.** The bus enforces that a namespace belongs to the module // it is named for, so an event named for somebody else cannot be published at all. If the // event is about a role rather than about this module, it belongs on the seat: a name that // is stable across whoever fills it (04-ISSUES/127). if first, _, split := strings.Cut(e, "."); split && isAModuleNameOtherThan(first, m.Module) { problems = append(problems, fmt.Sprintf( "%s emits %q, which reads as another module's event. A module publishes under its "+ "own name only. If this is about a role rather than about %s, declare it on that "+ "seat, where the name survives the holder changing", m.Module, e, m.Module)) } } for _, c := range m.Consumes { if strings.HasPrefix(c, "module.") { problems = append(problems, fmt.Sprintf( "%s consumes %q, which is the old bus's pattern. A consumed event names its emitter "+ "and the event: write %q", m.Module, c, strings.TrimPrefix(c, "module."))) continue } if c == "#" { problems = append(problems, fmt.Sprintf( "%s consumes %q, which is the old bus's wildcard for everything. Write %q", m.Module, c, TheRest)) continue } if err := consumePattern(c); err != nil { problems = append(problems, fmt.Sprintf("%s consumes %q: %v", m.Module, c, err)) } } return problems } // staleEventForm says an emitted name is this module's own old routing key, and what it was. func staleEventForm(event, module string) (string, bool) { return event, module != "" && strings.HasPrefix(event, "module."+module+".") } // isAModuleNameOtherThan says a first token names some module of this mesh that is not this one. // // Only the mesh's own seats and the catalogue could answer this properly, and neither is reachable // from a parser given one manifest. So this catches the case that actually happened — a name that // is a *provision* the mesh defines, which is where "another module's event" comes from in practice // — and the whole-catalogue check catches the rest. func isAModuleNameOtherThan(first, module string) bool { if first == module || first == "" { return false } if _, isASeat := SeatNamed(first); isASeat { return true } if _, isASeat := SeatDelivering(first); isASeat { return true } return false } // localName checks one event name: dot-separated names, no wildcards, nothing else. func localName(event string) error { if event == "" { return fmt.Errorf("an event needs a name") } for _, part := range strings.Split(event, ".") { if part == OneName || part == TheRest { return fmt.Errorf("an emitted event names one event, so it carries no wildcard") } if !eventName.MatchString(part) { return fmt.Errorf("%q is not a usable name: lower-case letters, digits and dashes", part) } } return nil } // consumePattern checks a consumed pattern: the emitter, then the event, with wildcards. func consumePattern(pattern string) error { if pattern == "" { return fmt.Errorf("a consumed event needs an emitter and an event") } parts := strings.Split(pattern, ".") for i, part := range parts { switch { case part == TheRest: if i != len(parts)-1 { return fmt.Errorf("%q stands for the rest of a name, so nothing may follow it", TheRest) } case part == OneName: case !eventName.MatchString(part): return fmt.Errorf("%q is not a usable name: lower-case letters, digits and dashes", part) } } // `**` alone is every event from every module, which the audit logger wants and says plainly. if len(parts) == 1 && parts[0] != TheRest { return fmt.Errorf( "%q names an emitter and no event. Write ., or %q for every event", pattern, TheRest) } return nil }