202 lines
6.7 KiB
Go
202 lines
6.7 KiB
Go
// Package seatgen writes a contracts module's Go and TypeScript from its seat definitions (design 26 *The
|
|
// generator*): the seat list every reader compiles, and for each seat defined whole its names, subjects and message
|
|
// types, with a test of its fixtures. The same input gives the same bytes. It is the build modules' logic (the Go
|
|
// build module's for Go, the Node build module's for TypeScript); this copy wrote mesh-contracts' first files and is
|
|
// handed to them as it stands.
|
|
package seatgen
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/seatdef"
|
|
)
|
|
|
|
// Module is a contracts module read from its directory.
|
|
type Module struct {
|
|
// GoModule is the Go module path of the generated Go tree (`<module>/go`).
|
|
GoModule string
|
|
// TSPackage is the npm package's name.
|
|
TSPackage string
|
|
// DomainSDK is the Go module path of the contracts module's domain SDK (ADR 0634 §2), which a seat's client may
|
|
// import beside its contract and mesh-sdk's runtime; empty when the domain has none.
|
|
DomainSDK string
|
|
// Order is every seat's name, sorted: a contracts module has no seat list and no reading order (ADR 0635 §1).
|
|
Order []string
|
|
// Seats are every definition, resolved, by name.
|
|
Seats map[string]seatdef.Definition
|
|
// Raw are every definition as written, its parts not taken in.
|
|
Raw map[string]seatdef.Definition
|
|
// Parts are every part, by name.
|
|
Parts map[string]seatdef.Part
|
|
// Fixtures are each seat's fixtures, by seat, in file order.
|
|
Fixtures map[string][]seatdef.Fixture
|
|
}
|
|
|
|
// Load reads a contracts module from its directory, wherever that is in its repository (ADR 0634 §8):
|
|
// `seats/<seat>/seat.json`, `seats/<seat>/fixtures/*.json` and `parts/*.json`. A definition at the seat-list grain is
|
|
// read and checked like the rest and generates nothing. goModule and tsPackage name the generated trees' roots; a
|
|
// module with no seat defined whole needs neither.
|
|
func Load(dir, goModule, tsPackage string) (*Module, error) {
|
|
m := &Module{GoModule: goModule, TSPackage: tsPackage, Seats: map[string]seatdef.Definition{},
|
|
Raw: map[string]seatdef.Definition{}, Parts: map[string]seatdef.Part{}, Fixtures: map[string][]seatdef.Fixture{}}
|
|
parts, _ := filepath.Glob(filepath.Join(dir, "parts", "*.json"))
|
|
sort.Strings(parts)
|
|
for _, p := range parts {
|
|
raw, err := os.ReadFile(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
part, err := seatdef.ParsePart(raw)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s: %w", rel(dir, p), err)
|
|
}
|
|
if want := strings.TrimSuffix(filepath.Base(p), ".json"); part.Part != want {
|
|
return nil, fmt.Errorf("%s: names part %q", rel(dir, p), part.Part)
|
|
}
|
|
m.Parts[part.Part] = part
|
|
}
|
|
dirs, _ := filepath.Glob(filepath.Join(dir, "seats", "*", "seat.json"))
|
|
for _, p := range dirs {
|
|
name := filepath.Base(filepath.Dir(p))
|
|
raw, err := os.ReadFile(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
d, err := seatdef.Parse(raw)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s: %w", rel(dir, p), err)
|
|
}
|
|
if d.Seat != name {
|
|
return nil, fmt.Errorf("%s: names seat %q", rel(dir, p), d.Seat)
|
|
}
|
|
m.Raw[name] = d
|
|
m.Order = append(m.Order, name)
|
|
resolved, err := seatdef.Resolve(d, m.Parts)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s: %w", rel(dir, p), err)
|
|
}
|
|
m.Seats[name] = resolved
|
|
fx, _ := filepath.Glob(filepath.Join(filepath.Dir(p), "fixtures", "*.json"))
|
|
sort.Strings(fx)
|
|
for _, f := range fx {
|
|
raw, err := os.ReadFile(f)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cases, err := seatdef.ParseFixtures(raw)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s: %w", rel(dir, f), err)
|
|
}
|
|
m.Fixtures[name] = append(m.Fixtures[name], cases...)
|
|
}
|
|
}
|
|
sort.Strings(m.Order)
|
|
return m, nil
|
|
}
|
|
|
|
func rel(dir, p string) string {
|
|
if r, err := filepath.Rel(dir, p); err == nil {
|
|
return r
|
|
}
|
|
return p
|
|
}
|
|
|
|
// Generate is every file the module's generated trees hold, by path from the module's root.
|
|
func Generate(m *Module) (map[string][]byte, error) {
|
|
for _, name := range m.Order {
|
|
if m.Seats[name].Grain == seatdef.Whole && (m.GoModule == "" || m.TSPackage == "") {
|
|
return nil, fmt.Errorf("the seat %s is defined whole and the module names no generated Go module or TypeScript package yet: give them to the generator (seatgen -go-module, -ts-package)", name)
|
|
}
|
|
}
|
|
out := map[string][]byte{}
|
|
if err := goFiles(m, out); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := tsFiles(m, out); err != nil {
|
|
return nil, err
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// GeneratedRoots are the directories every file of which the generator writes: a file there it did not write is
|
|
// out of step (design 26: the compare step).
|
|
var GeneratedRoots = []string{"go", "ts"}
|
|
|
|
// Compare is every path out of step between what the generator writes and the tree in dir: a file that differs, one
|
|
// missing, or one under a generated root the generator did not write, each named once, sorted.
|
|
func Compare(dir string, files map[string][]byte) ([]string, error) {
|
|
var out []string
|
|
for p, want := range files {
|
|
got, err := os.ReadFile(filepath.Join(dir, p))
|
|
switch {
|
|
case os.IsNotExist(err):
|
|
out = append(out, p+": missing")
|
|
case err != nil:
|
|
return nil, err
|
|
case string(got) != string(want):
|
|
out = append(out, p+": differs")
|
|
}
|
|
}
|
|
for _, root := range GeneratedRoots {
|
|
err := filepath.WalkDir(filepath.Join(dir, root), func(p string, e os.DirEntry, err error) error {
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return filepath.SkipDir
|
|
}
|
|
return err
|
|
}
|
|
if e.IsDir() {
|
|
if e.Name() == "node_modules" || e.Name() == "dist" || rel(dir, p) == ClientRoot {
|
|
return filepath.SkipDir
|
|
}
|
|
return nil
|
|
}
|
|
r := rel(dir, p)
|
|
if _, ok := files[r]; !ok {
|
|
out = append(out, r+": not written by the generator")
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
sort.Strings(out)
|
|
return out, nil
|
|
}
|
|
|
|
// pkgName is a seat's or part's Go package: its name without hyphens.
|
|
func pkgName(name string) string { return strings.ReplaceAll(name, "-", "") }
|
|
|
|
// ident is a name in Go's exported form: mesh-broker → MeshBroker, publish-part → PublishPart, preview.written →
|
|
// PreviewWritten, local-name → LocalName, id → ID.
|
|
func ident(name string) string {
|
|
var b strings.Builder
|
|
for _, w := range strings.FieldsFunc(name, func(r rune) bool { return r == '-' || r == '_' || r == '.' || r == ' ' }) {
|
|
switch w {
|
|
case "id":
|
|
b.WriteString("ID")
|
|
case "url":
|
|
b.WriteString("URL")
|
|
case "mib":
|
|
b.WriteString("MiB")
|
|
default:
|
|
b.WriteString(strings.ToUpper(w[:1]) + w[1:])
|
|
}
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
// tsName is a name in TypeScript's camel form for a constant: publish-part → publishPart.
|
|
func tsName(name string) string {
|
|
id := ident(name)
|
|
if strings.HasPrefix(id, "ID") {
|
|
return "id" + id[2:]
|
|
}
|
|
return strings.ToLower(id[:1]) + id[1:]
|
|
}
|