diff --git a/modules/mesh-build-go/buildstep/seatgen/gogen.go b/modules/mesh-build-go/buildstep/seatgen/gogen.go index ec0382747..a1179943f 100644 --- a/modules/mesh-build-go/buildstep/seatgen/gogen.go +++ b/modules/mesh-build-go/buildstep/seatgen/gogen.go @@ -79,6 +79,12 @@ func declareAll(d seatdef.Definition, fromPart map[string]string) (*typer, error return nil, err } } + // A body of `bodies` yields its type alone: the sending is mesh-sdk's (design 47 G §452, §454). + for _, n := range sortedKeys(d.Bodies) { + if err := decl(n, "", d.Bodies[n]); err != nil { + return nil, err + } + } for _, v := range d.Verbs { if err := decl(ident(v.Name)+"Input", "is what "+v.Name+" takes.", v.Input); err != nil { return nil, err @@ -88,7 +94,7 @@ func declareAll(d seatdef.Definition, fromPart map[string]string) (*typer, error } } for _, a := range d.Accepts { - if err := decl(ident(a.Name)+"Accept", "is the body of "+a.Name+".", a.Body); err != nil { + if err := decl(ident(acceptName(a))+"Accept", "is the body of "+acceptName(a)+".", a.Body); err != nil { return nil, err } } @@ -171,7 +177,7 @@ func goWhole(m *Module, d seatdef.Definition) ([]byte, []byte, error) { } fmt.Fprintf(&b, "// Seat is the seat's name.\nconst Seat = %q\n\n", d.Seat) writeNames(&b, "Verb", seatdef.VerbNames(d.Verbs)) - writeNames(&b, "Accept", seatdef.MessageNames(d.Accepts)) + writeNames(&b, "Accept", acceptNames(d.Accepts)) writeNames(&b, "Event", seatdef.MessageNames(d.Events)) writeList(&b, "Verbs", "Verb", "every verb the seat serves, in its definition's order", seatdef.VerbNames(d.Verbs)) writeList(&b, "Events", "Event", "every event published on the seat's subject", seatdef.MessageNames(d.Events)) @@ -195,12 +201,12 @@ func goWhole(m *Module, d seatdef.Definition) ([]byte, []byte, error) { } } for _, a := range d.Accepts { - subj := seatdef.AcceptSubject(d.Seat, a.Name) + subj := seatdef.AcceptSubject(d.Seat, acceptName(a)) if a.ByCaller { fmt.Fprintf(&b, "// AcceptSubject%s is where one caller sends %s.\nfunc AcceptSubject%s(caller string) string { return %q + caller }\n\n", - ident(a.Name), a.Name, ident(a.Name), subj+".") + ident(acceptName(a)), acceptName(a), ident(acceptName(a)), subj+".") } else { - fmt.Fprintf(&b, "// AcceptSubject%s is where %s is sent.\nconst AcceptSubject%s = %q\n\n", ident(a.Name), a.Name, ident(a.Name), subj) + fmt.Fprintf(&b, "// AcceptSubject%s is where %s is sent.\nconst AcceptSubject%s = %q\n\n", ident(acceptName(a)), acceptName(a), ident(acceptName(a)), subj) } } if len(d.HolderEvents) > 0 { @@ -430,7 +436,7 @@ func fixtureTarget(d seatdef.Definition, kind, name string) (string, string, err ms, subj, suffix = d.Events, seatdef.EventSubject(d.Seat, name), "Event" } for _, msg := range ms { - if msg.Name == name { + if (kind == "event" && msg.Name == name) || (kind == "accept" && acceptName(msg) == name) { if kind == "event" && len(msg.Parts()) > 0 { return ident(name) + suffix, partsPattern(d.Seat, msg), nil } @@ -460,6 +466,23 @@ func fixtureTarget(d seatdef.Definition, kind, name string) (string, string, err return "", "", fmt.Errorf("the definition has no %s %q", kind, name) } +// acceptName is an accepted kind's name as the generated names and its subject carry it: the plain kind, or for a +// kind on a stream `.` (design 47 G §455), so `step` on `check` and on `delivery` stay two. +func acceptName(m seatdef.Message) string { + if m.Stream != "" { + return m.Stream + "." + m.Name + } + return m.Name +} + +func acceptNames(ms []seatdef.Message) []string { + out := make([]string, len(ms)) + for i, m := range ms { + out[i] = acceptName(m) + } + return out +} + func regexp(s string) string { var b strings.Builder for _, r := range s { diff --git a/modules/mesh-build-go/buildstep/seatgen/layout_test.go b/modules/mesh-build-go/buildstep/seatgen/layout_test.go index 6c714b9a2..ebef2b555 100644 --- a/modules/mesh-build-go/buildstep/seatgen/layout_test.go +++ b/modules/mesh-build-go/buildstep/seatgen/layout_test.go @@ -337,3 +337,43 @@ func TestADefinitionsActsSubjectsKeepAndCallersAreRead(t *testing.T) { } } } + +// TestBodiesAreTypesAndAcceptedKindsRideStreams is design 47 G §452, §454, §455 and §391's check for the generator: a +// definition's `bodies` yield their types in both languages and no sending code, a reference into `bodies` resolves +// as one into `defs`, an accepted kind on a stream has the subject `.` (`step` on both streams stays +// two), and a verb naming no class is refused by nothing. +func TestBodiesAreTypesAndAcceptedKindsRideStreams(t *testing.T) { + m, err := Load("testdata/contracts", "example.org/contracts/go", "@example/contracts") + if err != nil { + t.Fatal(err) + } + files, err := Generate(m) + if err != nil { + t.Fatal(err) + } + src := string(files["go/seats/aregistry/contract.go"]) + for _, want := range []string{ + "type Finding struct", "Related []Finding", "type PeekAnswer = Finding", + `"check.step"`, `"delivery.step"`, `"check.run"`, + `const AcceptSubjectCheckStep = "mesh.seat.a-registry.accept.check.step"`, + `const AcceptSubjectDeliveryStep = "mesh.seat.a-registry.accept.delivery.step"`, + "type CheckStepAccept struct", "type DeliveryStepAccept", + } { + if !strings.Contains(src, want) { + t.Errorf("the Go contract lacks %q:\n%s", want, src) + } + } + if strings.Contains(src, "SendFinding") || strings.Contains(src, "SubjectFinding") { + t.Error("a body yields more than its type") + } + dts := string(files["ts/seats/aregistry/index.d.ts"]) + js := string(files["ts/seats/aregistry/index.js"]) + for _, want := range []string{"export interface Finding", "checkStepSubject", "deliveryStepSubject"} { + if !strings.Contains(dts, want) { + t.Errorf("index.d.ts lacks %q:\n%s", want, dts) + } + } + if !strings.Contains(js, `"mesh.seat.a-registry.accept.delivery.step"`) { + t.Errorf("index.js lacks the delivery stream's subject:\n%s", js) + } +} 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 f992622b6..dbcd3a8a7 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,11 @@ {"format":1,"seat":"a-registry","scope":"mesh","grain":"whole","domain":"Module","decision":"ADR 1","includes":["registry"], - "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"}}], + "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"}}, + {"name":"peek","input":{"type":"object"},"answer":{"$ref":"#/bodies/Finding"}}], + "accepts":[ + {"name":"step","stream":"check","body":{"type":"object","properties":{"id":{"type":"string"}}}}, + {"name":"step","stream":"delivery","body":{"type":"object"}}, + {"name":"run","stream":"check","body":{"type":"object"}}], + "bodies":{"Finding":{"type":"object","properties":{"id":{"type":"string"},"owner":{"type":"string"},"related":{"type":"array","items":{"$ref":"#/bodies/Finding"}}},"required":["id"]}}, "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}}, diff --git a/modules/mesh-build-go/buildstep/seatgen/tsgen.go b/modules/mesh-build-go/buildstep/seatgen/tsgen.go index b1b655b12..352c2cb53 100644 --- a/modules/mesh-build-go/buildstep/seatgen/tsgen.go +++ b/modules/mesh-build-go/buildstep/seatgen/tsgen.go @@ -103,7 +103,7 @@ func tsFiles(m *Module, out map[string][]byte) error { fmt.Fprintf(&o.js, "export const seat = %q;\n\n", d.Seat) fmt.Fprintf(&o.dts, "export declare const seat: %q;\n\n", d.Seat) tsNames(o, "verbs", seatdef.VerbNames(d.Verbs)) - tsNames(o, "accepts", seatdef.MessageNames(d.Accepts)) + tsNames(o, "accepts", acceptNames(d.Accepts)) tsNames(o, "events", seatdef.MessageNames(d.Events)) if f := d.Family; f != nil { fmt.Fprintf(&o.js, "export const family = { name: %q, maxAgeDays: %d };\n\n", f.Name, f.MaxAgeDays) @@ -121,7 +121,7 @@ func tsFiles(m *Module, out map[string][]byte) error { tsSubject(o, tsName(e.Name), seatdef.EventSubject(d.Seat, e.Name), e.ByCaller) } for _, a := range d.Accepts { - tsSubject(o, tsName(a.Name), seatdef.AcceptSubject(d.Seat, a.Name), a.ByCaller) + tsSubject(o, tsName(acceptName(a)), seatdef.AcceptSubject(d.Seat, acceptName(a)), a.ByCaller) } if len(d.HolderEvents) > 0 { tsNames(o, "holderEvents", holderNames(d.HolderEvents)) diff --git a/modules/mesh-build-go/buildstep/seatgen/types.go b/modules/mesh-build-go/buildstep/seatgen/types.go index 4b96bae42..c44cb57ed 100644 --- a/modules/mesh-build-go/buildstep/seatgen/types.go +++ b/modules/mesh-build-go/buildstep/seatgen/types.go @@ -62,9 +62,13 @@ func (tp *typer) read(where string, s map[string]any, top bool) (*typ, error) { return &typ{kind: "any"}, nil } if r, ok := s["$ref"].(string); ok { + // A reference into `bodies` is read as one into `defs` (design 47 G §454). n, found := strings.CutPrefix(r, "#/defs/") + if !found { + n, found = strings.CutPrefix(r, "#/bodies/") + } if !found || n == "" { - return nil, fmt.Errorf("$ref %q is not #/defs/", r) + return nil, fmt.Errorf("$ref %q is not #/defs/ or #/bodies/", r) } return &typ{kind: "ref", ref: n}, nil }