233 lines
7.2 KiB
Go
233 lines
7.2 KiB
Go
package seatgen
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/seatdef"
|
|
)
|
|
|
|
const tsHeader = "// Code generated by seatgen from the seat definitions; DO NOT EDIT.\n\n"
|
|
|
|
// TSSeatDir is where a seat defined whole has its npm package (ADR 0625 §1): the indexer counts the npm package a
|
|
// lock resolves, so each such seat is a package of its own and a module is rebuilt only for the seats it uses.
|
|
func TSSeatDir(seat string) string { return "ts/seats/" + pkgName(seat) }
|
|
|
|
// TSSeatPackage is the npm package name of a seat defined whole, beside the seat set's package root.
|
|
func TSSeatPackage(root, seat string) string { return root + "-" + seat }
|
|
|
|
// tsConfig type-checks the package's sources and writes nothing: the package ships its TypeScript, which its importers
|
|
// compile or bundle, so it has no build script and needs no compiler of its own (F7: a generated package has no
|
|
// dependency, and its lock, which the build module requires of a package artifact, holds none).
|
|
const tsConfig = `{
|
|
"compilerOptions": {
|
|
"target": "ES2022",
|
|
"module": "NodeNext",
|
|
"moduleResolution": "NodeNext",
|
|
"noEmit": true,
|
|
"strict": true,
|
|
"skipLibCheck": true,
|
|
"forceConsistentCasingInFileNames": true
|
|
},
|
|
"include": ["src/**/*.ts"]
|
|
}
|
|
`
|
|
|
|
// tsPackage writes one npm package's package.json, its dependency-free package-lock.json and its tsconfig.json at dir.
|
|
func tsPackage(out map[string][]byte, dir, name, description string) {
|
|
pkg := map[string]any{
|
|
"name": name, "version": "0.1.0", "type": "module",
|
|
"description": description,
|
|
"exports": map[string]any{".": "./src/index.ts"},
|
|
"types": "./src/index.ts",
|
|
"files": []any{"src"},
|
|
}
|
|
raw, _ := json.MarshalIndent(pkg, "", " ")
|
|
out[dir+"/package.json"] = append(raw, '\n')
|
|
lock := map[string]any{
|
|
"name": name, "version": "0.1.0", "lockfileVersion": 3, "requires": true,
|
|
"packages": map[string]any{"": map[string]any{"name": name, "version": "0.1.0"}},
|
|
}
|
|
raw, _ = json.MarshalIndent(lock, "", " ")
|
|
out[dir+"/package-lock.json"] = append(raw, '\n')
|
|
out[dir+"/tsconfig.json"] = []byte(tsConfig)
|
|
}
|
|
|
|
// tsFiles writes the npm side: one package per seat defined whole at TSSeatDir (ADR 0625 §1), holding the parts it includes whole and importing nothing outside it.
|
|
func tsFiles(m *Module, out map[string][]byte) error {
|
|
partTypes := map[string]*typer{}
|
|
partFiles := map[string][]byte{}
|
|
for _, name := range sortedKeys(m.Parts) {
|
|
p := m.Parts[name]
|
|
d := seatdef.Definition{Grain: seatdef.Whole, Verbs: p.Verbs, Events: p.Events, Defs: p.Defs}
|
|
tp, err := declareAll(d, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
partTypes[name] = tp
|
|
var b bytes.Buffer
|
|
b.WriteString(tsHeader)
|
|
fmt.Fprintf(&b, "// The part %q several seats include whole: %s\n\n", p.Part, oneLine(p.Description))
|
|
tsNames(&b, "verbs", seatdef.VerbNames(p.Verbs))
|
|
tsNames(&b, "events", seatdef.MessageNames(p.Events))
|
|
tsTypes(&b, tp)
|
|
partFiles[name] = b.Bytes()
|
|
}
|
|
for _, name := range m.Order {
|
|
d := m.Seats[name]
|
|
if d.Grain != seatdef.Whole {
|
|
continue
|
|
}
|
|
dir := TSSeatDir(name)
|
|
tsPackage(out, dir, TSSeatPackage(m.TSPackage, name), fmt.Sprintf("The contract of the seat %s, generated from its definition.", name))
|
|
raw := m.Raw[name]
|
|
fromPart := map[string]string{}
|
|
for _, inc := range raw.Includes {
|
|
for _, t := range partTypes[inc].types {
|
|
fromPart[t.name] = inc
|
|
}
|
|
out[dir+"/src/parts/"+inc+".ts"] = partFiles[inc]
|
|
}
|
|
tp, err := declareAll(d, fromPart)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var b bytes.Buffer
|
|
b.WriteString(tsHeader)
|
|
fmt.Fprintf(&b, "// The contract of the seat %q, defined whole (seats/%s/seat.json): %s\n\n", d.Seat, d.Seat, oneLine(d.Description))
|
|
for _, inc := range raw.Includes {
|
|
fmt.Fprintf(&b, "import type * as %s from \"./parts/%s.js\";\n\n", tsName(inc), inc)
|
|
}
|
|
fmt.Fprintf(&b, "export const seat = %q;\n\n", d.Seat)
|
|
tsNames(&b, "verbs", seatdef.VerbNames(d.Verbs))
|
|
tsNames(&b, "accepts", seatdef.MessageNames(d.Accepts))
|
|
tsNames(&b, "events", seatdef.MessageNames(d.Events))
|
|
if f := d.Family; f != nil {
|
|
fmt.Fprintf(&b, "export const family = { name: %q, maxAgeDays: %d", f.Name, f.MaxAgeDays)
|
|
b.WriteString(" } as const;\n\n")
|
|
}
|
|
for _, e := range d.Events {
|
|
subj := seatdef.EventSubject(d.Seat, e.Name)
|
|
if e.ByCaller {
|
|
fmt.Fprintf(&b, "export function %sSubject(caller: string): string {\n return %q + caller;\n}\n\n", tsName(e.Name), subj+".")
|
|
} else {
|
|
fmt.Fprintf(&b, "export const %sSubject = %q;\n\n", tsName(e.Name), subj)
|
|
}
|
|
}
|
|
for _, a := range d.Accepts {
|
|
subj := seatdef.AcceptSubject(d.Seat, a.Name)
|
|
if a.ByCaller {
|
|
fmt.Fprintf(&b, "export function %sSubject(caller: string): string {\n return %q + caller;\n}\n\n", tsName(a.Name), subj+".")
|
|
} else {
|
|
fmt.Fprintf(&b, "export const %sSubject = %q;\n\n", tsName(a.Name), subj)
|
|
}
|
|
}
|
|
if len(d.HolderEvents) > 0 {
|
|
tsNames(&b, "holderEvents", holderNames(d.HolderEvents))
|
|
for _, e := range d.HolderEvents {
|
|
before, after, _ := strings.Cut(e.Subject, "<holder>")
|
|
fmt.Fprintf(&b, "export function %sHolderSubject(holder: string): string {\n return %q + holder + %q;\n}\n\n",
|
|
tsName(e.Name), before, after)
|
|
}
|
|
}
|
|
if len(d.States) > 0 {
|
|
if err := tsStates(&b, d.States); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for _, n := range sortedKeys(fromPart) {
|
|
fmt.Fprintf(&b, "export type %s = %s.%s;\n", n, tsName(fromPart[n]), n)
|
|
}
|
|
if len(fromPart) > 0 {
|
|
b.WriteString("\n")
|
|
}
|
|
tsTypes(&b, tp)
|
|
out[dir+"/src/index.ts"] = b.Bytes()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func holderNames(es []seatdef.HolderEvent) []string {
|
|
out := make([]string, len(es))
|
|
for i, e := range es {
|
|
out[i] = e.Name
|
|
}
|
|
return out
|
|
}
|
|
|
|
func tsNames(b *bytes.Buffer, what string, names []string) {
|
|
if len(names) == 0 {
|
|
return
|
|
}
|
|
fmt.Fprintf(b, "export const %s = {\n", what)
|
|
for _, n := range names {
|
|
fmt.Fprintf(b, " %s: %q,\n", tsName(n), n)
|
|
}
|
|
b.WriteString("} as const;\n\n")
|
|
}
|
|
|
|
func tsTypes(b *bytes.Buffer, tp *typer) {
|
|
for _, t := range tp.types {
|
|
if d := strings.TrimSpace(t.desc); d != "" {
|
|
fmt.Fprintf(b, "/** %s */\n", oneLine(d))
|
|
}
|
|
if t.t.kind != "struct" {
|
|
fmt.Fprintf(b, "export type %s = %s;\n\n", t.name, tsType(t.t))
|
|
continue
|
|
}
|
|
fmt.Fprintf(b, "export interface %s {\n", t.name)
|
|
for _, f := range t.t.fields {
|
|
if f.desc != "" {
|
|
fmt.Fprintf(b, " /** %s */\n", oneLine(f.desc))
|
|
}
|
|
opt := "?"
|
|
if f.required {
|
|
opt = ""
|
|
}
|
|
fmt.Fprintf(b, " %s%s: %s;\n", strconv.Quote(f.json), opt, tsType(f.t))
|
|
}
|
|
b.WriteString("}\n\n")
|
|
}
|
|
}
|
|
|
|
func tsType(t *typ) string {
|
|
if t.null {
|
|
n := *t
|
|
n.null = false
|
|
return tsType(&n) + " | null"
|
|
}
|
|
switch t.kind {
|
|
case "string":
|
|
if len(t.enum) > 0 {
|
|
parts := make([]string, len(t.enum))
|
|
for i, e := range t.enum {
|
|
parts[i] = strconv.Quote(e)
|
|
}
|
|
return strings.Join(parts, " | ")
|
|
}
|
|
return "string"
|
|
case "time":
|
|
return "string"
|
|
case "integer", "number":
|
|
return "number"
|
|
case "boolean":
|
|
return "boolean"
|
|
case "object":
|
|
return "Record<string, unknown>"
|
|
case "map":
|
|
return "Record<string, " + tsType(t.elem) + ">"
|
|
case "slice":
|
|
el := tsType(t.elem)
|
|
if strings.Contains(el, "|") {
|
|
el = "(" + el + ")"
|
|
}
|
|
return el + "[]"
|
|
case "ref":
|
|
return t.ref
|
|
}
|
|
return "unknown"
|
|
}
|