// 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 (`/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.json`, `seats//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:] }