GEN4: the seat generator emits the types of bodies, resolves #/bodies/ refs and names an accepted kind by its stream

Builds: design 47 G-9 §452, §454, §455 (§391: no verb is refused for a missing class)

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
2026-11-11 11:27:17 +01:00
parent 4ad5f40817
commit b5aebbbe91
5 changed files with 83 additions and 10 deletions
@@ -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 `<stream>.<kind>` (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 {
@@ -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 `<stream>.<kind>` (`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)
}
}
@@ -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.<milestone>","body":{"type":"object"},"keep":{"max-age-days":90,"max-mib":16,"last-per-subject":true}},
@@ -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))
@@ -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/<name>", r)
return nil, fmt.Errorf("$ref %q is not #/defs/<name> or #/bodies/<name>", r)
}
return &typ{kind: "ref", ref: n}, nil
}