Files
mesh-catalog/modules/mesh-build-go/buildstep/seatgen/gogen.go
T

463 lines
15 KiB
Go

package seatgen
import (
"bytes"
"encoding/json"
"fmt"
"go/format"
"sort"
"strconv"
"strings"
"git.novox.be/novox/mesh-sdk/go/seatdef"
)
const header = "// Code generated by seatgen from the seat definitions; DO NOT EDIT.\n\n"
func goFiles(m *Module, out map[string][]byte) error {
// Each seat defined whole is a Go module of its own, so a change of one seat moves only its importers, whose
// fingerprint counts a Go module's go.mod and go.sum (ADR 0625 §1, §2). A seat's module imports nothing: the parts it
// includes are written into it whole. There is no seat list and no root Go module (ADR 0635 §1): the list is read
// from the bus.
for _, name := range m.Order {
d := m.Seats[name]
if d.Grain != seatdef.Whole {
continue
}
src, test, err := goWhole(m, d)
if err != nil {
return fmt.Errorf("seat %s: %w", name, err)
}
dir := "go/seats/" + pkgName(name) + "/"
out[dir+"go.mod"] = goMod(SeatGoModule(m.GoModule, name))
out[dir+"contract.go"] = src
if test != nil {
out[dir+"contract_test.go"] = test
}
}
return nil
}
// SeatGoModule is the Go module of a seat defined whole, beside the seat set's.
func SeatGoModule(root, seat string) string { return root + "/seats/" + pkgName(seat) }
func goMod(path string) []byte { return []byte("module " + path + "\n\ngo 1.26.0\n") }
func sortedKeys[V any](m map[string]V) []string {
out := make([]string, 0, len(m))
for k := range m {
out = append(out, k)
}
sort.Strings(out)
return out
}
func gofmt(b *bytes.Buffer) ([]byte, error) {
src, err := format.Source(b.Bytes())
if err != nil {
return nil, fmt.Errorf("the generated Go does not parse: %w\n%s", err, b.String())
}
return src, nil
}
// declareAll declares a definition's types: its own named types, then each verb's input and answer, each accept,
// event, holder event, record and provision; a name given by a part (in fromPart) is an alias of the part's.
func declareAll(d seatdef.Definition, fromPart map[string]string) (*typer, error) {
tp := newTyper()
decl := func(name, desc string, s map[string]any) error {
if s == nil {
return nil
}
if _, ok := fromPart[name]; ok {
tp.taken[name] = true
return nil
}
return tp.declare(name, desc, s)
}
for _, n := range d.DefNames() {
if err := decl(n, "", d.Defs[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
}
if err := decl(ident(v.Name)+"Answer", "is what "+v.Name+" answers.", v.Answer); err != nil {
return nil, err
}
}
for _, a := range d.Accepts {
if err := decl(ident(a.Name)+"Accept", "is the body of "+a.Name+".", a.Body); err != nil {
return nil, err
}
}
for _, e := range d.Events {
if err := decl(ident(e.Name)+"Event", "is the body of the event "+e.Name+".", e.Body); err != nil {
return nil, err
}
}
for _, e := range d.HolderEvents {
if err := decl(ident(e.Name)+"HolderEvent", "is the body of the holder's event "+e.Name+".", e.Body); err != nil {
return nil, err
}
}
for _, st := range d.States {
for _, kf := range st.KeyForms {
vn, err := stateValueName(st.Name, kf)
if err != nil {
return nil, fmt.Errorf("state %s: %w", st.Name, err)
}
if err := decl(vn, "is one value of the state "+st.Name+" under the key "+kf.Key+".", kf.Value); err != nil {
return nil, err
}
}
}
for _, p := range d.Provisions {
if err := decl(ident(p.Name)+"Form", "is the form of the provision "+p.Name+".", p.Form); err != nil {
return nil, err
}
}
return tp, nil
}
func oneLine(s string) string {
s = strings.TrimSpace(s)
if s == "" {
return "no description."
}
return strings.ReplaceAll(s, "\n", " ")
}
func writeNames(b *bytes.Buffer, prefix string, names []string) {
if len(names) == 0 {
return
}
b.WriteString("const (\n")
for _, n := range names {
fmt.Fprintf(b, "\t%s%s = %q\n", prefix, ident(n), n)
}
b.WriteString(")\n\n")
}
func writeList(b *bytes.Buffer, fn, prefix, what string, names []string) {
if len(names) == 0 {
return
}
fmt.Fprintf(b, "// %s is %s: a new copy each call.\nfunc %s() []string {\n\treturn []string{", fn, what, fn)
for i, n := range names {
if i > 0 {
b.WriteString(", ")
}
b.WriteString(prefix + ident(n))
}
b.WriteString("}\n}\n\n")
}
// goWhole is a whole seat's package: its names, subjects and types, and the test of its fixtures.
func goWhole(m *Module, d seatdef.Definition) ([]byte, []byte, error) {
fromPart := map[string]string{}
tp, err := declareAll(d, fromPart)
if err != nil {
return nil, nil, err
}
pkg := pkgName(d.Seat)
var b bytes.Buffer
b.WriteString(header)
fmt.Fprintf(&b, "// Package %s is the contract of the seat %q, defined whole (seats/%s/seat.json): %s\npackage %s\n\n",
pkg, d.Seat, d.Seat, oneLine(d.Description), pkg)
if len(d.States) > 0 {
b.WriteString(goStatesImports(d.States))
}
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, "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))
if f := d.Family; f != nil {
fmt.Fprintf(&b, "// Family is where the seat's events are kept: the shared events stream, filtered to the seat's subjects.\nconst (\n\tFamily = %q\n\tFamilyMaxAgeDays = %d\n)\n\n", f.Name, f.MaxAgeDays)
}
for _, e := range d.Events {
subj := seatdef.EventSubject(d.Seat, e.Name)
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+".")
} else {
fmt.Fprintf(&b, "// Subject%s is where %s is published.\nconst Subject%s = %q\n\n", ident(e.Name), e.Name, ident(e.Name), subj)
}
}
for _, a := range d.Accepts {
subj := seatdef.AcceptSubject(d.Seat, a.Name)
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+".")
} 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)
}
}
if len(d.HolderEvents) > 0 {
b.WriteString("// The holder's own events, listed beside the seat and published under the holder's subjects (ADR 0620 §2).\nconst (\n")
for _, e := range d.HolderEvents {
fmt.Fprintf(&b, "\tHolderEvent%s = %q\n", ident(e.Name), e.Name)
}
b.WriteString(")\n\n")
for _, e := range d.HolderEvents {
before, after, _ := strings.Cut(e.Subject, "<holder>")
fmt.Fprintf(&b, "// HolderSubject%s is where the holder publishes %s", ident(e.Name), e.Name)
if e.Family != "" {
fmt.Fprintf(&b, ", kept in %s", e.Family)
}
fmt.Fprintf(&b, ".\nfunc HolderSubject%s(holder string) string { return %q + holder + %q }\n\n", ident(e.Name), before, after)
}
}
if len(d.States) > 0 {
if err := goStates(&b, d.States); err != nil {
return nil, nil, err
}
}
if err := goTypes(&b, tp, map[string]*typ{}); err != nil {
return nil, nil, err
}
if bytes.Contains(b.Bytes(), []byte("time.Time")) {
// A moment is the standard library's time (the only import a contract may have besides its states' readers).
raw := b.Bytes()
if bytes.Contains(raw, []byte("\nimport (\n")) {
raw = bytes.Replace(raw, []byte("\nimport (\n"), []byte("\nimport (\n\t\"time\"\n"), 1)
} else {
raw = bytes.Replace(raw, []byte("package "+pkg+"\n\n"), []byte("package "+pkg+"\n\nimport \"time\"\n\n"), 1)
}
b = *bytes.NewBuffer(raw)
}
src, err := gofmt(&b)
if err != nil {
return nil, nil, err
}
test, err := goFixtures(m, d, pkg, tp, fromPart)
return src, test, err
}
// goTypes writes every declared type; also are the kinds of types declared elsewhere (a part's), by name.
func goTypes(b *bytes.Buffer, tp *typer, also map[string]*typ) error {
kinds := map[string]*typ{}
for k, v := range also {
kinds[k] = v
}
for _, t := range tp.types {
kinds[t.name] = t.t
}
for _, t := range tp.types {
desc := strings.TrimSpace(t.desc)
if desc == "" {
desc = "is a type of the contract."
} else if !strings.HasPrefix(desc, "is ") {
desc = "is " + strings.TrimSuffix(desc, ".") + "."
}
fmt.Fprintf(b, "// %s %s\n", t.name, oneLine(desc))
if t.t.kind != "struct" {
fmt.Fprintf(b, "type %s = %s\n\n", t.name, goType(t.t, kinds, true))
goEnum(b, t.name, t.name, t.t)
continue
}
fmt.Fprintf(b, "type %s struct {\n", t.name)
for _, f := range t.t.fields {
if f.desc != "" {
fmt.Fprintf(b, "\t// %s\n", oneLine(f.desc))
}
tag := f.json
if !f.required {
tag += ",omitempty"
}
fmt.Fprintf(b, "\t%s %s `json:%q`\n", ident(f.json), goType(f.t, kinds, f.required), tag)
}
b.WriteString("}\n\n")
for _, f := range t.t.fields {
goEnum(b, t.name+"'s "+f.json, t.name+ident(f.json), f.t)
}
}
return nil
}
// goEnum writes a string's enum as one constant per value, named prefix and the value (RemoveInputReasonRetention), so a
// caller names a value the definition gives and never writes it by hand.
func goEnum(b *bytes.Buffer, what, prefix string, t *typ) {
if t.kind != "string" || len(t.enum) == 0 {
return
}
fmt.Fprintf(b, "// The values of %s.\nconst (\n", what)
for _, v := range t.enum {
fmt.Fprintf(b, "\t%s%s = %q\n", prefix, ident(v), v)
}
b.WriteString(")\n\n")
}
func goType(t *typ, kinds map[string]*typ, required bool) string {
if t.null {
n := *t
n.null = false
return "*" + goType(&n, kinds, true)
}
switch t.kind {
case "string":
return "string"
case "time":
if required {
return "time.Time"
}
return "*time.Time"
case "integer":
return "int64"
case "number":
return "float64"
case "boolean":
return "bool"
case "object":
return "map[string]any"
case "map":
return "map[string]" + goType(t.elem, kinds, true)
case "slice":
return "[]" + goType(t.elem, kinds, true)
case "ref":
if k, ok := kinds[t.ref]; ok && k.kind == "struct" && !required {
return "*" + t.ref
}
return t.ref
}
return "any"
}
// goFixtures is the test that every fixture of the seat decodes into its type and reads back as its wire says, and
// that the subject it names is the one the contract gives.
func goFixtures(m *Module, d seatdef.Definition, pkg string, tp *typer, fromPart map[string]string) ([]byte, error) {
fx := m.Fixtures[d.Seat]
if len(fx) == 0 {
return nil, nil
}
has := map[string]bool{}
for _, t := range tp.types {
has[t.name] = true
}
for n := range fromPart {
has[n] = true
}
var b bytes.Buffer
b.WriteString(header)
fmt.Fprintf(&b, "package %s\n\nimport (\n\t\"encoding/json\"\n\t\"reflect\"\n\t\"regexp\"\n\t\"testing\"\n)\n\n", pkg)
fmt.Fprintf(&b, "// TestFixtures reads every fixture of seats/%s/fixtures: each decodes into its type, a field the type does not\n"+
"// know is dropped (ADR 0608 §6), and it reads back as its wire says; each subject named is the contract's.\n", d.Seat)
b.WriteString("func TestFixtures(t *testing.T) {\n\tcases := []struct {\n\t\tname, subject, given, wire string\n\t\tdecode func([]byte) (any, error)\n\t\tsubjectIs *regexp.Regexp\n\t}{\n")
for _, f := range fx {
kind, name, _ := f.Instance()
typeName, pattern, err := fixtureTarget(d, kind, name)
if err != nil {
return nil, fmt.Errorf("fixture %q: %w", f.Name, err)
}
given, _ := json.Marshal(f.Given)
wire := given
if f.Wire != nil {
wire, _ = json.Marshal(f.Wire)
}
decoder := "func(b []byte) (any, error) { var v map[string]any; err := json.Unmarshal(b, &v); return v, err }"
if has[typeName] {
decoder = fmt.Sprintf("func(b []byte) (any, error) { var v %s; err := json.Unmarshal(b, &v); return v, err }", typeName)
}
subjectIs := "nil"
if pattern != "" {
subjectIs = "regexp.MustCompile(" + strconv.Quote(pattern) + ")"
} else if f.Subject != "" {
return nil, fmt.Errorf("fixture %q names a subject, and a verb's message has none in the contract", f.Name)
}
if pattern != "" && f.Subject == "" {
return nil, fmt.Errorf("fixture %q of %s names no subject (design 26: each event's subject is named)", f.Name, f.Of)
}
fmt.Fprintf(&b, "\t\t{name: %q, subject: %q, given: %s, wire: %s,\n\t\t\tdecode: %s, subjectIs: %s},\n",
f.Name, f.Subject, strconv.Quote(string(given)), strconv.Quote(string(wire)), decoder, subjectIs)
}
b.WriteString(` }
for _, c := range cases {
v, err := c.decode([]byte(c.given))
if err != nil {
t.Errorf("%s: does not decode: %v", c.name, err)
continue
}
back, err := json.Marshal(v)
if err != nil {
t.Errorf("%s: does not encode: %v", c.name, err)
continue
}
var got, want any
if err := json.Unmarshal(back, &got); err != nil {
t.Fatal(err)
}
if err := json.Unmarshal([]byte(c.wire), &want); err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(got, want) {
t.Errorf("%s: reads back as %s, want %s", c.name, back, c.wire)
}
if c.subjectIs != nil && !c.subjectIs.MatchString(c.subject) {
t.Errorf("%s: subject %q is not the contract's %s", c.name, c.subject, c.subjectIs)
}
}
}
`)
return gofmt(&b)
}
// fixtureTarget is the type a fixture decodes into and the pattern its subject must match, if it has one.
func fixtureTarget(d seatdef.Definition, kind, name string) (string, string, error) {
switch kind {
case "verb-input", "verb-answer":
for _, v := range d.Verbs {
if v.Name == name {
if kind == "verb-input" {
return ident(name) + "Input", "", nil
}
return ident(name) + "Answer", "", nil
}
}
case "accept", "event":
ms, subj := d.Accepts, seatdef.AcceptSubject(d.Seat, name)
suffix := "Accept"
if kind == "event" {
ms, subj, suffix = d.Events, seatdef.EventSubject(d.Seat, name), "Event"
}
for _, msg := range ms {
if msg.Name == name {
p := "^" + regexp(subj)
if msg.ByCaller {
p += `\.[A-Za-z0-9_-]+`
}
return ident(name) + suffix, p + "$", nil
}
}
case "state":
if n, ok := stateFormFor(d, name); ok {
return n, "", nil
}
case "holder-event":
for _, e := range d.HolderEvents {
if e.Name == name {
before, after, _ := strings.Cut(e.Subject, "<holder>")
p := "^" + regexp(before)
if strings.Contains(e.Subject, "<holder>") {
p += "[a-z0-9-]+" + regexp(after)
}
return ident(name) + "HolderEvent", p + "$", nil
}
}
}
return "", "", fmt.Errorf("the definition has no %s %q", kind, name)
}
func regexp(s string) string {
var b strings.Builder
for _, r := range s {
if strings.ContainsRune(`\.+*?()|[]{}^$`, r) {
b.WriteRune('\\')
}
b.WriteRune(r)
}
return b.String()
}