GEN3: the seat generator reads acts, subject, keep and callers and writes each event's subject and stream
An event whose subject carries parts is published by a function taking each part and heard by a filter with a * per part; an event naming keep gets the stream SEAT_<seat>_<event>, in Go and in the TypeScript contract. The reader of the sdk's reader release reads acts (every, a language, kind:image), keep as an object and callers as a list. Builds: design 47 G §89, §203, §238, §260, §324, §331, §396, §397 Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
@@ -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_<seat>_<event>`, 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, `\.`) + "$"
|
||||
}
|
||||
|
||||
@@ -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_<seat>_<event>` 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-1
@@ -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.<milestone>","body":{"type":"object"},"keep":{"max-age-days":90,"max-mib":16,"last-per-subject":true}},
|
||||
{"name":"owed","subject":"owed.<repository>.<number>","body":{"type":"object"},"keep":{"max-age-days":7,"max-mib":64}},
|
||||
{"name":"step-ended","subject":"step-ended.<node>","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",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user