463 lines
15 KiB
Go
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()
|
|
}
|