A caller names a value the seat definition gives, such as the removal reason retention, and never writes it by hand. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
689 lines
20 KiB
Go
689 lines
20 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 {
|
|
// The seat set is the contracts module's root Go module; each seat defined whole is a Go module of its own beside
|
|
// it, 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.
|
|
out["go/go.mod"] = goMod(m.GoModule)
|
|
set, err := goSeatList(m)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
out["go/seats/seats.go"] = set
|
|
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
|
|
}
|
|
|
|
// goSeatList is go/seats/seats.go: the seat list's types, every seat's name and the compiled list in reading order.
|
|
func goSeatList(m *Module) ([]byte, error) {
|
|
var b bytes.Buffer
|
|
b.WriteString(header)
|
|
b.WriteString(`// Package seats is the mesh's seat list, generated from every seat's definition (design 26 *The seat set has one
|
|
// source*): what each seat is, where it is held, the provision it delivers, its verbs with their inputs, what it
|
|
// accepts and emits, what it receives and the paths it owns. mesh-sdk's runtime reads it; a seat its holder declares
|
|
// itself has a name here and no entry in the list.
|
|
package seats
|
|
|
|
// Seat is one role the mesh defines.
|
|
type Seat struct {
|
|
Name string
|
|
Scope string
|
|
Grain string
|
|
Replicated bool
|
|
Delivers string
|
|
Accepts []string
|
|
Emits []string
|
|
PulledByKind bool
|
|
Yields bool
|
|
Serves []Verb
|
|
Receives []Receivable
|
|
Paths []Path
|
|
HeldByTheEngine bool
|
|
Decision string
|
|
}
|
|
|
|
// Verb is one verb a seat serves: its input and answer as JSON Schema.
|
|
type Verb struct {
|
|
Name string
|
|
Description string
|
|
Input map[string]any
|
|
Output map[string]any
|
|
Optional bool
|
|
Replaces []string
|
|
}
|
|
|
|
// Receivable is one kind of contribution a seat receives.
|
|
type Receivable struct {
|
|
Kind string
|
|
Comment string
|
|
Dirs bool
|
|
Shape *Shape
|
|
Offered bool
|
|
}
|
|
|
|
// Shape is a kind given as data. Its checks are the runtime's.
|
|
type Shape struct {
|
|
Places []PlaceField
|
|
Order string
|
|
Examples []map[string]any
|
|
Text bool
|
|
}
|
|
|
|
// PlaceField is one field a holder places by, and the values it takes.
|
|
type PlaceField struct {
|
|
Field string
|
|
Values []string
|
|
}
|
|
|
|
// Path is one path on its machine a seat's holder owns.
|
|
type Path struct {
|
|
Path string
|
|
StepsBackFor string
|
|
}
|
|
|
|
// The name of every seat defined.
|
|
const (
|
|
`)
|
|
for _, name := range m.Order {
|
|
fmt.Fprintf(&b, "\t%s = %q\n", ident(name), name)
|
|
}
|
|
b.WriteString(")\n\n// Names is every seat defined, in reading order, the ones their holders declare included.\nfunc Names() []string {\n\treturn []string{")
|
|
for _, name := range m.Order {
|
|
b.WriteString(ident(name) + ", ")
|
|
}
|
|
b.WriteString("}\n}\n\n// List is the seat list, in reading order: a new copy each call.\nfunc List() []Seat {\n\treturn []Seat{\n")
|
|
for _, name := range m.Order {
|
|
d := m.Seats[name]
|
|
if d.HolderDeclares {
|
|
continue
|
|
}
|
|
b.WriteString(goSeat(d))
|
|
}
|
|
b.WriteString("\t}\n}\n")
|
|
return gofmt(&b)
|
|
}
|
|
|
|
func goSeat(d seatdef.Definition) string {
|
|
var b strings.Builder
|
|
fmt.Fprintf(&b, "{Name: %s, Scope: %q, Grain: %q, Decision: %q", ident(d.Seat), d.Scope, d.Grain, d.Decision)
|
|
if d.Replicated {
|
|
b.WriteString(", Replicated: true")
|
|
}
|
|
if d.Delivers != "" {
|
|
fmt.Fprintf(&b, ", Delivers: %q", d.Delivers)
|
|
}
|
|
if len(d.Accepts) > 0 {
|
|
b.WriteString(", Accepts: " + goLit(anyStrings(seatdef.MessageNames(d.Accepts))))
|
|
}
|
|
if len(d.Events) > 0 {
|
|
b.WriteString(", Emits: " + goLit(anyStrings(seatdef.MessageNames(d.Events))))
|
|
}
|
|
if d.PulledByKind {
|
|
b.WriteString(", PulledByKind: true")
|
|
}
|
|
if d.Yields {
|
|
b.WriteString(", Yields: true")
|
|
}
|
|
if d.HeldByTheEngine {
|
|
b.WriteString(", HeldByTheEngine: true")
|
|
}
|
|
if len(d.Verbs) > 0 {
|
|
b.WriteString(",\n\t\tServes: []Verb{\n")
|
|
for _, v := range d.Verbs {
|
|
fmt.Fprintf(&b, "\t\t\t{Name: %q, Description: %q", v.Name, v.Description)
|
|
if v.Optional {
|
|
b.WriteString(", Optional: true")
|
|
}
|
|
if v.Input != nil {
|
|
b.WriteString(",\n\t\t\t\tInput: " + goLit(resolveRefs(v.Input, d.Defs)))
|
|
}
|
|
if v.Answer != nil {
|
|
b.WriteString(",\n\t\t\t\tOutput: " + goLit(resolveRefs(v.Answer, d.Defs)))
|
|
}
|
|
if len(v.Replaces) > 0 {
|
|
b.WriteString(", Replaces: " + goLit(anyStrings(v.Replaces)))
|
|
}
|
|
b.WriteString("},\n")
|
|
}
|
|
b.WriteString("\t\t}")
|
|
}
|
|
if len(d.Receives) > 0 {
|
|
b.WriteString(",\n\t\tReceives: []Receivable{")
|
|
for _, r := range d.Receives {
|
|
fmt.Fprintf(&b, "{Kind: %q, Comment: %q", r.Kind, r.Comment)
|
|
if r.Dirs {
|
|
b.WriteString(", Dirs: true")
|
|
}
|
|
if r.Offered {
|
|
b.WriteString(", Offered: true")
|
|
}
|
|
if s := r.Shape; s != nil {
|
|
b.WriteString(", Shape: &Shape{")
|
|
if len(s.Places) > 0 {
|
|
b.WriteString("Places: []PlaceField{")
|
|
for _, p := range s.Places {
|
|
fmt.Fprintf(&b, "{Field: %q, Values: %s}, ", p.Field, goLit(anyStrings(p.Values)))
|
|
}
|
|
b.WriteString("}, ")
|
|
}
|
|
if s.Order != "" {
|
|
fmt.Fprintf(&b, "Order: %q, ", s.Order)
|
|
}
|
|
if len(s.Examples) > 0 {
|
|
b.WriteString("Examples: []map[string]any{")
|
|
for _, e := range s.Examples {
|
|
b.WriteString(goLit(e) + ", ")
|
|
}
|
|
b.WriteString("}, ")
|
|
}
|
|
if s.Text {
|
|
b.WriteString("Text: true")
|
|
}
|
|
b.WriteString("}")
|
|
}
|
|
b.WriteString("}, ")
|
|
}
|
|
b.WriteString("}")
|
|
}
|
|
if len(d.Paths) > 0 {
|
|
b.WriteString(", Paths: []Path{")
|
|
for _, p := range d.Paths {
|
|
fmt.Fprintf(&b, "{Path: %q, StepsBackFor: %q}, ", p.Path, p.StepsBackFor)
|
|
}
|
|
b.WriteString("}")
|
|
}
|
|
b.WriteString("},\n")
|
|
return b.String()
|
|
}
|
|
|
|
func anyStrings(s []string) []any {
|
|
out := make([]any, len(s))
|
|
for i, v := range s {
|
|
out[i] = v
|
|
}
|
|
return out
|
|
}
|
|
|
|
// goLit is a JSON value as a Go literal, in the form encoding/json decodes it, but for a list of strings alone,
|
|
// which is a []string as the seat list wrote it: a map's keys sorted, a number a float64.
|
|
func goLit(v any) string {
|
|
switch x := v.(type) {
|
|
case nil:
|
|
return "nil"
|
|
case string:
|
|
return strconv.Quote(x)
|
|
case bool:
|
|
return strconv.FormatBool(x)
|
|
case float64:
|
|
return "float64(" + strconv.FormatFloat(x, 'g', -1, 64) + ")"
|
|
case int:
|
|
return "float64(" + strconv.Itoa(x) + ")"
|
|
case json.Number:
|
|
return "float64(" + x.String() + ")"
|
|
case []string:
|
|
return goLit(anyStrings(x))
|
|
case []any:
|
|
allStrings := len(x) > 0
|
|
for _, e := range x {
|
|
if _, ok := e.(string); !ok {
|
|
allStrings = false
|
|
}
|
|
}
|
|
var b strings.Builder
|
|
if allStrings {
|
|
b.WriteString("[]string{")
|
|
} else {
|
|
b.WriteString("[]any{")
|
|
}
|
|
for i, e := range x {
|
|
if i > 0 {
|
|
b.WriteString(", ")
|
|
}
|
|
b.WriteString(goLit(e))
|
|
}
|
|
b.WriteString("}")
|
|
return b.String()
|
|
case map[string]any:
|
|
var b strings.Builder
|
|
b.WriteString("map[string]any{")
|
|
for i, k := range sortedKeys(x) {
|
|
if i > 0 {
|
|
b.WriteString(", ")
|
|
}
|
|
b.WriteString(strconv.Quote(k) + ": " + goLit(x[k]))
|
|
}
|
|
b.WriteString("}")
|
|
return b.String()
|
|
}
|
|
// Any other value (a typed list from a definition read in Go) goes through JSON once.
|
|
raw, _ := json.Marshal(v)
|
|
var back any
|
|
_ = json.Unmarshal(raw, &back)
|
|
return goLit(back)
|
|
}
|
|
|
|
// 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 _, r := range d.Records {
|
|
if err := decl(ident(r.Name)+"Record", "is one value of the record "+r.Name+".", r.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)
|
|
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.Records) > 0 {
|
|
writeNames(&b, "Record", recordNames(d.Records))
|
|
}
|
|
if err := goTypes(&b, tp, map[string]*typ{}); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
src, err := gofmt(&b)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
test, err := goFixtures(m, d, pkg, tp, fromPart)
|
|
return src, test, err
|
|
}
|
|
|
|
func recordNames(rs []seatdef.Record) []string {
|
|
out := make([]string, len(rs))
|
|
for i, r := range rs {
|
|
out[i] = r.Name
|
|
}
|
|
return out
|
|
}
|
|
|
|
// 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 {
|
|
switch t.kind {
|
|
case "string":
|
|
return "string"
|
|
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 "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()
|
|
}
|