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

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:]
}