diff --git a/modules/mesh-build-go/buildstep/seatgen/gogen.go b/modules/mesh-build-go/buildstep/seatgen/gogen.go index 0c231490d..ec0382747 100644 --- a/modules/mesh-build-go/buildstep/seatgen/gogen.go +++ b/modules/mesh-build-go/buildstep/seatgen/gogen.go @@ -180,6 +180,13 @@ func goWhole(m *Module, d seatdef.Definition) ([]byte, []byte, error) { } for _, e := range d.Events { subj := seatdef.EventSubject(d.Seat, e.Name) + if e.Keep != nil { + fmt.Fprintf(&b, "// Stream%s is the stream %s is kept in.\nconst Stream%s = %q\n\n", ident(e.Name), e.Name, ident(e.Name), keptStream(d.Seat, e.Name)) + } + if parts := e.Parts(); len(parts) > 0 { + goPartsSubject(&b, d.Seat, e, parts) + continue + } if e.ByCaller { fmt.Fprintf(&b, "// Subject%s is where %s is published for one caller.\nfunc Subject%s(caller string) string { return %q + caller }\n\n", ident(e.Name), e.Name, ident(e.Name), subj+".") @@ -424,6 +431,9 @@ func fixtureTarget(d seatdef.Definition, kind, name string) (string, string, err } for _, msg := range ms { if msg.Name == name { + if kind == "event" && len(msg.Parts()) > 0 { + return ident(name) + suffix, partsPattern(d.Seat, msg), nil + } p := "^" + regexp(subj) if msg.ByCaller { p += `\.[A-Za-z0-9_-]+` @@ -460,3 +470,61 @@ func regexp(s string) string { } return b.String() } + +// keptStream is the bus's name of the stream an event naming `keep` is kept in: `SEAT__`, which the bus +// takes (no dot) and the grants part makes (design 47 G §260, §396). +func keptStream(seat, event string) string { return "SEAT_" + seat + "_" + event } + +// subjectExpr is the Go or JavaScript string expression of an event's subject with its parts as arguments. +func subjectExpr(seat string, e seatdef.Message) []string { + var expr []string + lit := "mesh.seat." + seat + ".event." + for i, tok := range strings.Split(e.SubjectPattern(), ".") { + if i > 0 { + lit += "." + } + if strings.HasPrefix(tok, "<") { + expr = append(expr, fmt.Sprintf("%q", lit), goParam(tok[1:len(tok)-1])) + lit = "" + continue + } + lit += tok + } + if lit != "" { + expr = append(expr, fmt.Sprintf("%q", lit)) + } + return expr +} + +// goPartsSubject writes the subject of an event whose subject carries parts (design 47 G §260, §397): a function taking +// each part in order, and the filter that hears every instance of it. +func goPartsSubject(b *bytes.Buffer, seat string, e seatdef.Message, parts []string) { + args := make([]string, len(parts)) + for i, p := range parts { + args[i] = goParam(p) + } + fmt.Fprintf(b, "// Subject%s is where %s is published, on %s.\nfunc Subject%s(%s string) string { return %s }\n\n", + ident(e.Name), e.Name, "mesh.seat."+seat+".event."+e.SubjectPattern(), ident(e.Name), strings.Join(args, ", "), + strings.Join(subjectExpr(seat, e), " + ")) + fmt.Fprintf(b, "// Subject%sAll hears every %s.\nconst Subject%sAll = %q\n\n", ident(e.Name), e.Name, ident(e.Name), + "mesh.seat."+seat+".event."+e.Local()) +} + +// goParam is a part's name as a parameter: its words joined, the first lower-case. +func goParam(part string) string { + id := ident(part) + return strings.ToLower(id[:1]) + id[1:] +} + +// partsPattern is the pattern a fixture's subject of an event with parts must match: a part is one token. +func partsPattern(seat string, e seatdef.Message) string { + toks := strings.Split(e.SubjectPattern(), ".") + for i, tok := range toks { + if strings.HasPrefix(tok, "<") { + toks[i] = `[^.*> ]+` + } else { + toks[i] = regexp(tok) + } + } + return "^" + regexp("mesh.seat."+seat+".event.") + strings.Join(toks, `\.`) + "$" +} diff --git a/modules/mesh-build-go/buildstep/seatgen/layout_test.go b/modules/mesh-build-go/buildstep/seatgen/layout_test.go index 4f097012c..6c714b9a2 100644 --- a/modules/mesh-build-go/buildstep/seatgen/layout_test.go +++ b/modules/mesh-build-go/buildstep/seatgen/layout_test.go @@ -291,3 +291,49 @@ func TestATypeScriptContractIsJavaScriptWithItsDeclarations(t *testing.T) { t.Error("the declarations do not import the part") } } + +// TestADefinitionsActsSubjectsKeepAndCallersAreRead is design 47 G §89, §203, §260, §324, §331's check for the +// generator: it reads `acts` (a step on every, a language and `kind: image`), `subject`, `keep` and `callers`, and +// writes each event's subject and the stream `SEAT__` of one that names `keep`. +func TestADefinitionsActsSubjectsKeepAndCallersAreRead(t *testing.T) { + m, err := Load("testdata/contracts", "example.org/contracts/go", "@example/contracts") + if err != nil { + t.Fatal(err) + } + if acts := m.Seats["a-registry"].Acts; len(acts) != 3 || acts[0].On != "kind:image" || acts[1].On != "every" || acts[2].On != "go" { + t.Errorf("acts read as %+v", acts) + } + files, err := Generate(m) + if err != nil { + t.Fatal(err) + } + src := string(files["go/seats/aregistry/contract.go"]) + for _, want := range []string{ + `const StreamMilestoneAsked = "SEAT_a-registry_milestone-asked"`, + `const StreamOwed = "SEAT_a-registry_owed"`, + "func SubjectMilestoneAsked(milestone string) string {\n\treturn \"mesh.seat.a-registry.event.milestone-asked.\" + milestone\n}", + `SubjectMilestoneAskedAll = "mesh.seat.a-registry.event.milestone-asked.*"`, + `func SubjectOwed(repository, number string) string {`, + "func SubjectStepEnded(node string) string { return \"mesh.seat.a-registry.event.step-ended.\" + node }", + } { + if !strings.Contains(src, want) { + t.Errorf("the Go contract lacks %q:\n%s", want, src) + } + } + if strings.Contains(src, "StreamStepEnded") { + t.Error("an event naming no keep has a stream of its own") + } + js := string(files["ts/seats/aregistry/index.js"]) + dts := string(files["ts/seats/aregistry/index.d.ts"]) + for _, want := range []string{`export const milestoneAskedStream = "SEAT_a-registry_milestone-asked";`, + `export function stepEndedSubject(node) {`, `export const milestoneAskedSubjectAll = "mesh.seat.a-registry.event.milestone-asked.*";`} { + if !strings.Contains(js, want) { + t.Errorf("index.js lacks %q:\n%s", want, js) + } + } + for _, want := range []string{`export declare const owedStream: "SEAT_a-registry_owed";`, `export declare function owedSubject(repository: string, number: string): string;`} { + if !strings.Contains(dts, want) { + t.Errorf("index.d.ts lacks %q:\n%s", want, dts) + } + } +} diff --git a/modules/mesh-build-go/buildstep/seatgen/testdata/contracts/seats/a-registry/seat.json b/modules/mesh-build-go/buildstep/seatgen/testdata/contracts/seats/a-registry/seat.json index 42259601e..f992622b6 100644 --- a/modules/mesh-build-go/buildstep/seatgen/testdata/contracts/seats/a-registry/seat.json +++ b/modules/mesh-build-go/buildstep/seatgen/testdata/contracts/seats/a-registry/seat.json @@ -1,5 +1,10 @@ {"format":1,"seat":"a-registry","scope":"mesh","grain":"whole","domain":"Module","decision":"ADR 1","includes":["registry"], - "verbs":[{"name":"hold","input":{"type":"object"},"answer":{"type":"object"}}], + "verbs":[{"name":"hold","class":"read","callers":["invokers","persons","sessions","seat:b-registry","module:gitea","operator","healers","holder:node-backup@same-node"],"input":{"type":"object"},"answer":{"type":"object"}}], + "acts":[{"step":"build","on":"kind:image","bound":"60m"},{"step":"check","on":"every"},{"step":"build","on":"go"}], + "events":[ + {"name":"milestone-asked","subject":"milestone-asked.","body":{"type":"object"},"keep":{"max-age-days":90,"max-mib":16,"last-per-subject":true}}, + {"name":"owed","subject":"owed..","body":{"type":"object"},"keep":{"max-age-days":7,"max-mib":64}}, + {"name":"step-ended","subject":"step-ended.","body":{"type":"object"}}], "family":{"name":"a-registry.events","max-age-days":7}, "states":[ {"name":"asks","template":"state","description":"the asks held, one key per asker and ask", diff --git a/modules/mesh-build-go/buildstep/seatgen/tsgen.go b/modules/mesh-build-go/buildstep/seatgen/tsgen.go index a4476b22c..b1b655b12 100644 --- a/modules/mesh-build-go/buildstep/seatgen/tsgen.go +++ b/modules/mesh-build-go/buildstep/seatgen/tsgen.go @@ -110,6 +110,14 @@ func tsFiles(m *Module, out map[string][]byte) error { fmt.Fprintf(&o.dts, "export declare const family: { readonly name: %q; readonly maxAgeDays: %d };\n\n", f.Name, f.MaxAgeDays) } for _, e := range d.Events { + if e.Keep != nil { + fmt.Fprintf(&o.js, "export const %sStream = %q;\n\n", tsName(e.Name), keptStream(d.Seat, e.Name)) + fmt.Fprintf(&o.dts, "export declare const %sStream: %q;\n\n", tsName(e.Name), keptStream(d.Seat, e.Name)) + } + if parts := e.Parts(); len(parts) > 0 { + tsPartsSubject(o, d.Seat, e, parts) + continue + } tsSubject(o, tsName(e.Name), seatdef.EventSubject(d.Seat, e.Name), e.ByCaller) } for _, a := range d.Accepts { @@ -235,3 +243,18 @@ func tsType(t *typ) string { } return "unknown" } + +// tsPartsSubject writes the subject of an event whose subject carries parts, as goPartsSubject does. +func tsPartsSubject(o *tsOut, seat string, e seatdef.Message, parts []string) { + args := make([]string, len(parts)) + names := make([]string, len(parts)) + for i, p := range parts { + names[i] = goParam(p) + args[i] = names[i] + ": string" + } + all := "mesh.seat." + seat + ".event." + e.Local() + fmt.Fprintf(&o.js, "export function %sSubject(%s) {\n return %s;\n}\n\n", tsName(e.Name), strings.Join(names, ", "), strings.Join(subjectExpr(seat, e), " + ")) + fmt.Fprintf(&o.js, "export const %sSubjectAll = %q;\n\n", tsName(e.Name), all) + fmt.Fprintf(&o.dts, "export declare function %sSubject(%s): string;\n\n", tsName(e.Name), strings.Join(args, ", ")) + fmt.Fprintf(&o.dts, "export declare const %sSubjectAll: %q;\n\n", tsName(e.Name), all) +}