seatgen: no seat list and no root package, definitions in any folder, seat-list-grain files generate nothing, packages with a dependency-free lock (ADR 0635 §1, ADR 0634 §8, F7; issue 1966)
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
@@ -34,8 +34,7 @@ var goModuleLine = regexp.MustCompile(`(?m)^module\s+(\S+)`)
|
||||
|
||||
// OutOfStep is every file under root, a path from the module's directory, that the generator writes otherwise than the
|
||||
// module holds it, for each `generated-from` its package artifacts name, sorted and each once; none for a module naming
|
||||
// none. The generated Go tree's module path is read from <from>/go/go.mod and the TypeScript package's name from
|
||||
// <from>/ts/package.json, as the contracts module states them.
|
||||
// none. The generated trees' names are read from the seats already generated in <from> (generatedNames).
|
||||
func OutOfStep(moduleDir, root string) ([]string, error) {
|
||||
raw, err := os.ReadFile(filepath.Join(moduleDir, "module.json"))
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
@@ -93,23 +92,32 @@ func OutOfStep(moduleDir, root string) ([]string, error) {
|
||||
return paths, nil
|
||||
}
|
||||
|
||||
// generatedNames reads the names the generated trees are rooted at from the seats already generated in dir: a seat's Go
|
||||
// module is `<root>/seats/<seat without hyphens>` and its npm package `<root>-<seat>` (a contracts module has no root
|
||||
// package of either kind, ADR 0635 §1). A module with no seat generated yet gives none, and the generator says so only
|
||||
// if a seat is defined whole.
|
||||
func generatedNames(dir string) (goModule, tsPackage string, err error) {
|
||||
if raw, err := os.ReadFile(filepath.Join(dir, GeneratedRootGo, "go.mod")); err == nil {
|
||||
if m := goModuleLine.FindSubmatch(raw); m != nil {
|
||||
goModule = string(m[1])
|
||||
names, _ := filepath.Glob(filepath.Join(dir, "seats", "*", "seat.json"))
|
||||
for _, p := range names {
|
||||
seat := filepath.Base(filepath.Dir(p))
|
||||
pkg := strings.ReplaceAll(seat, "-", "")
|
||||
if goModule == "" {
|
||||
if raw, err := os.ReadFile(filepath.Join(dir, GeneratedRootGo, "seats", pkg, "go.mod")); err == nil {
|
||||
if m := goModuleLine.FindSubmatch(raw); m != nil {
|
||||
goModule = strings.TrimSuffix(string(m[1]), "/seats/"+pkg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if raw, err := os.ReadFile(filepath.Join(dir, GeneratedRootTS, "package.json")); err == nil {
|
||||
var p struct {
|
||||
Name string `json:"name"`
|
||||
if tsPackage == "" {
|
||||
if raw, err := os.ReadFile(filepath.Join(dir, GeneratedRootTS, "seats", pkg, "package.json")); err == nil {
|
||||
var p struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
if json.Unmarshal(raw, &p) == nil {
|
||||
tsPackage = strings.TrimSuffix(p.Name, "-"+seat)
|
||||
}
|
||||
}
|
||||
}
|
||||
if json.Unmarshal(raw, &p) == nil {
|
||||
tsPackage = p.Name
|
||||
}
|
||||
}
|
||||
if goModule == "" || tsPackage == "" {
|
||||
return "", "", errors.New("a contracts module names its generated Go module in go/go.mod and its TypeScript package in ts/package.json; " +
|
||||
filepath.Base(dir) + " names " + map[bool]string{true: "not the TypeScript package", false: "not the Go module"}[goModule != ""])
|
||||
}
|
||||
return goModule, tsPackage, nil
|
||||
}
|
||||
|
||||
@@ -21,8 +21,9 @@ func contracts(t *testing.T, manifest string) (string, []string) {
|
||||
dir := t.TempDir()
|
||||
for p, text := range map[string]string{
|
||||
"module.json": manifest,
|
||||
"seats/index.json": `{"format":1,"seats":["one-store"]}`,
|
||||
"seats/one-store/seat.json": `{"format":1,"seat":"one-store","scope":"mesh","grain":"seat-list","decision":"ADR 1","verbs":[{"name":"query","input":{"type":"object"}}]}`,
|
||||
"seats/one-store/seat.json": `{"format":1,"seat":"one-store","scope":"mesh","grain":"whole","decision":"ADR 1","verbs":[{"name":"query","input":{"type":"object"},"answer":{"type":"object"}}]}`,
|
||||
// A definition at the seat-list grain is read and generates nothing (ADR 0635 §1, §2).
|
||||
"seats/other/seat.json": `{"format":1,"seat":"other","scope":"mesh","grain":"seat-list","decision":"ADR 1","verbs":[{"name":"ask","input":{"type":"object"}}]}`,
|
||||
} {
|
||||
writeContract(t, filepath.Join(dir, p), text)
|
||||
}
|
||||
@@ -39,13 +40,6 @@ func contracts(t *testing.T, manifest string) (string, []string) {
|
||||
writeContract(t, filepath.Join(dir, p), string(b))
|
||||
paths = append(paths, p)
|
||||
}
|
||||
// The names the compare reads, where the contracts module states them; a generated go.mod or package.json is kept.
|
||||
if _, ok := files["go/go.mod"]; !ok {
|
||||
writeContract(t, filepath.Join(dir, "go/go.mod"), "module example.org/c/go\n")
|
||||
}
|
||||
if _, ok := files["ts/package.json"]; !ok {
|
||||
writeContract(t, filepath.Join(dir, "ts/package.json"), `{"name":"@example/c"}`)
|
||||
}
|
||||
return dir, paths
|
||||
}
|
||||
|
||||
@@ -74,6 +68,11 @@ func TestAContractsModulesGeneratedFilesAreComparedWithTheGenerator(t *testing.T
|
||||
tsFile = p
|
||||
}
|
||||
}
|
||||
for _, p := range paths {
|
||||
if strings.Contains(p, "other") || p == "go/go.mod" || p == "ts/package.json" {
|
||||
t.Errorf("%s is generated: a seat-list definition generates nothing and a contracts module has no root package", p)
|
||||
}
|
||||
}
|
||||
if goFile == "" || tsFile == "" {
|
||||
t.Fatalf("the generator wrote no Go or no TypeScript file: %v", paths)
|
||||
}
|
||||
|
||||
@@ -144,14 +144,10 @@ func checkClient(dir, at, seat string, m *Module) ([]string, error) {
|
||||
return out, err
|
||||
}
|
||||
|
||||
// TSSetArtifact is the seat set's npm package artifact, at ts/.
|
||||
const TSSetArtifact = "lib"
|
||||
|
||||
// CheckArtifacts is the check of module.json's package artifacts against the tree (ADR 0625 §1, §3), per language.
|
||||
// Go: the root module is the artifact `go` at `go`, each seat defined whole the artifact `go-<seat without hyphens>`
|
||||
// at its module's directory, each client present the artifact `go-<seat without hyphens>client` at its directory.
|
||||
// npm: the seat set's package is the artifact `lib` at `ts`, each seat defined whole the artifact
|
||||
// `ts-<seat without hyphens>` at TSSeatDir. No other package artifact of either language. Each problem is named once,
|
||||
// Go: each seat defined whole is the artifact `go-<seat without hyphens>` at its module's directory, each client
|
||||
// present the artifact `go-<seat without hyphens>client` at its directory. npm: each seat defined whole is the artifact
|
||||
// `ts-<seat without hyphens>` at TSSeatDir. There is no root package of either kind (ADR 0635 §1). No other package artifact of either language. Each problem is named once,
|
||||
// sorted.
|
||||
func CheckArtifacts(dir string, m *Module) ([]string, error) {
|
||||
raw, err := os.ReadFile(filepath.Join(dir, "module.json"))
|
||||
@@ -171,10 +167,7 @@ func CheckArtifacts(dir string, m *Module) ([]string, error) {
|
||||
if err := json.Unmarshal(raw, &mf); err != nil {
|
||||
return nil, fmt.Errorf("module.json: %w", err)
|
||||
}
|
||||
want := map[string]map[string]string{
|
||||
"go": {"go": "go"},
|
||||
"typescript": {TSSetArtifact: "ts"},
|
||||
}
|
||||
want := map[string]map[string]string{"go": {}, "typescript": {}}
|
||||
for name, d := range m.Seats {
|
||||
if d.Grain == seatdef.Whole {
|
||||
want["go"]["go-"+pkgName(name)] = "go/seats/" + pkgName(name)
|
||||
@@ -200,7 +193,7 @@ func CheckArtifacts(dir string, m *Module) ([]string, error) {
|
||||
path, ok := paths[a.Name]
|
||||
switch {
|
||||
case !ok:
|
||||
out = append(out, "module.json: the "+said[a.Language]+" package artifact "+a.Name+" is no seat's module, no client's and not the seat set's")
|
||||
out = append(out, "module.json: the "+said[a.Language]+" package artifact "+a.Name+" is no seat's module and no client's")
|
||||
case len(a.Paths) != 1 || a.Paths[0] != path:
|
||||
out = append(out, fmt.Sprintf("module.json: the artifact %s's paths are %v, not [%s]", a.Name, a.Paths, path))
|
||||
}
|
||||
|
||||
@@ -29,12 +29,10 @@ import (
|
||||
|
||||
func Publish(ctx context.Context) string { _ = parts.MaxChunk; return "seat:" + aregistry.Seat + ".publish" }
|
||||
`
|
||||
const ts = `,{"name":"lib","kind":"package","language":"typescript","paths":["ts"]},
|
||||
{"name":"ts-aregistry","kind":"package","language":"typescript","paths":["ts/seats/aregistry"]},
|
||||
const ts = `,{"name":"ts-aregistry","kind":"package","language":"typescript","paths":["ts/seats/aregistry"]},
|
||||
{"name":"ts-bregistry","kind":"package","language":"typescript","paths":["ts/seats/bregistry"]}`
|
||||
artifacts := func(extra string) string {
|
||||
return `{"build":{"artifacts":[{"name":"go","kind":"package","language":"go","paths":["go"]},
|
||||
{"name":"go-aregistry","kind":"package","language":"go","paths":["go/seats/aregistry"]},
|
||||
return `{"build":{"artifacts":[{"name":"go-aregistry","kind":"package","language":"go","paths":["go/seats/aregistry"]},
|
||||
{"name":"go-bregistry","kind":"package","language":"go","paths":["go/seats/bregistry"]}` + ts + extra + `]}}`
|
||||
}
|
||||
clientArtifact := `,{"name":"go-aregistryclient","kind":"package","language":"go","paths":["go/clients/aregistryclient"]}`
|
||||
@@ -55,10 +53,8 @@ func Publish(ctx context.Context) string { _ = parts.MaxChunk; return "seat:" +
|
||||
}, artifacts(`,{"name":"go-extra","kind":"package","language":"go","paths":["go/extra"]}`),
|
||||
[]string{"module.json: no Go package artifact go-aregistryclient", "module.json: the Go package artifact go-extra is no seat's"}},
|
||||
{"a seat's npm package with no artifact, one at another path, and one of no package", nil,
|
||||
`{"build":{"artifacts":[{"name":"go","kind":"package","language":"go","paths":["go"]},
|
||||
{"name":"go-aregistry","kind":"package","language":"go","paths":["go/seats/aregistry"]},
|
||||
`{"build":{"artifacts":[{"name":"go-aregistry","kind":"package","language":"go","paths":["go/seats/aregistry"]},
|
||||
{"name":"go-bregistry","kind":"package","language":"go","paths":["go/seats/bregistry"]},
|
||||
{"name":"lib","kind":"package","language":"typescript","paths":["ts"]},
|
||||
{"name":"ts-aregistry","kind":"package","language":"typescript","paths":["ts"]},
|
||||
{"name":"ts-onestore","kind":"package","language":"typescript","paths":["ts/seats/onestore"]}]}}`,
|
||||
[]string{"module.json: no npm package artifact ts-bregistry", "module.json: the artifact ts-aregistry's paths are [ts], not [ts/seats/aregistry]",
|
||||
|
||||
@@ -15,15 +15,10 @@ import (
|
||||
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
|
||||
// Each seat defined whole is a Go module of its own, 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. There is no seat list and no root Go module (ADR 0635 §1): the list is read
|
||||
// from the bus.
|
||||
for _, name := range m.Order {
|
||||
d := m.Seats[name]
|
||||
if d.Grain != seatdef.Whole {
|
||||
@@ -65,254 +60,6 @@ func gofmt(b *bytes.Buffer) ([]byte, error) {
|
||||
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) {
|
||||
|
||||
@@ -11,9 +11,10 @@ import (
|
||||
"git.novox.be/novox/mesh-sdk/go/seatdef"
|
||||
)
|
||||
|
||||
// TestEachSeatDefinedWholeIsAGoModuleOfItsOwn is ADR 0625's generator check (§1, §2, §3): the seat set is one package in
|
||||
// the root Go module, each seat defined whole is a Go module of its own holding its package alone, the parts it
|
||||
// includes are written into it, and no generated Go file imports anything outside the standard library.
|
||||
// TestEachSeatDefinedWholeIsAGoModuleOfItsOwn is ADR 0625's generator check (§1, §2, §3): there is no seat set and no
|
||||
// root Go module (ADR 0635 §1), each seat defined whole is a Go module of its own holding its package alone, the
|
||||
// parts it includes are written into it, a definition at the seat-list grain generates nothing, and no generated Go file
|
||||
// imports anything outside the standard library.
|
||||
func TestEachSeatDefinedWholeIsAGoModuleOfItsOwn(t *testing.T) {
|
||||
m, err := Load("testdata/contracts", "example.org/contracts/go", "@example/contracts")
|
||||
if err != nil {
|
||||
@@ -30,7 +31,6 @@ func TestEachSeatDefinedWholeIsAGoModuleOfItsOwn(t *testing.T) {
|
||||
}
|
||||
}
|
||||
want := map[string]string{
|
||||
"go/": "module example.org/contracts/go",
|
||||
"go/seats/aregistry/": "module example.org/contracts/go/seats/aregistry",
|
||||
"go/seats/bregistry/": "module example.org/contracts/go/seats/bregistry",
|
||||
}
|
||||
@@ -54,8 +54,8 @@ func TestEachSeatDefinedWholeIsAGoModuleOfItsOwn(t *testing.T) {
|
||||
continue
|
||||
}
|
||||
dir := p[:strings.LastIndex(p, "/")+1]
|
||||
if dir != "go/seats/" && mods[dir] == "" {
|
||||
t.Errorf("%s is in no seat's own Go module, nor the seat set's package", p)
|
||||
if mods[dir] == "" {
|
||||
t.Errorf("%s is in no seat's own Go module", p)
|
||||
}
|
||||
f, err := parser.ParseFile(token.NewFileSet(), p, b, parser.ImportsOnly)
|
||||
if err != nil {
|
||||
@@ -77,13 +77,15 @@ func TestEachSeatDefinedWholeIsAGoModuleOfItsOwn(t *testing.T) {
|
||||
t.Errorf("%s does not name the part's enum values:\n%s", seat, src)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(string(files["go/seats/seats.go"]), "OneStore") {
|
||||
t.Error("the seat set does not name the seat at the seat list's grain")
|
||||
for p := range files {
|
||||
if strings.Contains(p, "onestore") || p == "go/seats/seats.go" || p == "go/go.mod" || p == "ts/package.json" || p == "ts/src/seats.ts" {
|
||||
t.Errorf("%s is written: a seat list is not generated, and a definition at its grain generates nothing", p)
|
||||
}
|
||||
}
|
||||
|
||||
// The npm side (ADR 0625 §1): the indexer counts an npm package a lock resolves, so each seat defined whole is an
|
||||
// npm package of its own (`<package>-<seat>` at ts/seats/<seat without hyphens>), holding its parts' types whole and
|
||||
// importing nothing outside itself; the seat set's package (ts/) holds the seat list alone.
|
||||
// importing nothing outside itself; there is no package above them.
|
||||
pkgs := map[string]string{}
|
||||
for p, b := range files {
|
||||
if strings.HasPrefix(p, "ts/") && strings.HasSuffix(p, "package.json") {
|
||||
@@ -97,7 +99,6 @@ func TestEachSeatDefinedWholeIsAGoModuleOfItsOwn(t *testing.T) {
|
||||
}
|
||||
}
|
||||
wantPkgs := map[string]string{
|
||||
"ts/": "@example/contracts",
|
||||
"ts/seats/aregistry/": "@example/contracts-a-registry",
|
||||
"ts/seats/bregistry/": "@example/contracts-b-registry",
|
||||
}
|
||||
@@ -132,13 +133,29 @@ func TestEachSeatDefinedWholeIsAGoModuleOfItsOwn(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// F7: a published package is built from its own lock (mesh-build-node refuses one with none), and a generated one has
|
||||
// no dependency to lock: its lock names the package alone and its package.json has no script and no dependency.
|
||||
for _, seat := range []string{"aregistry", "bregistry"} {
|
||||
var pj, lock map[string]any
|
||||
if err := json.Unmarshal(files["ts/seats/"+seat+"/package.json"], &pj); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := json.Unmarshal(files["ts/seats/"+seat+"/package-lock.json"], &lock); err != nil {
|
||||
t.Fatalf("%s has no readable package-lock.json: %v", seat, err)
|
||||
}
|
||||
for _, k := range []string{"scripts", "dependencies", "devDependencies"} {
|
||||
if _, ok := pj[k]; ok {
|
||||
t.Errorf("%s's package.json has %s: a generated package needs no compiler or dependency", seat, k)
|
||||
}
|
||||
}
|
||||
if pk, _ := lock["packages"].(map[string]any); len(pk) != 1 || lock["name"] != pj["name"] {
|
||||
t.Errorf("%s's lock %v does not name its package alone", seat, lock)
|
||||
}
|
||||
}
|
||||
for _, seat := range []string{"aregistry", "bregistry"} {
|
||||
if !strings.Contains(string(files["ts/seats/"+seat+"/src/index.ts"]), "export type PublishedEvent = registry.PublishedEvent") ||
|
||||
!strings.Contains(string(files["ts/seats/"+seat+"/src/parts/registry.ts"]), "export type PublishedEvent") {
|
||||
t.Errorf("the npm package of %s does not hold the part's types whole", seat)
|
||||
}
|
||||
}
|
||||
if ix := string(files["ts/src/index.ts"]); strings.Contains(ix, "Registry") || !strings.Contains(ix, "seats") {
|
||||
t.Errorf("the seat set's npm package exports a seat's contract or not the seat list:\n%s", ix)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ type Module struct {
|
||||
GoModule string
|
||||
// TSPackage is the npm package's name.
|
||||
TSPackage string
|
||||
// Order is the seat list's reading order (seats/index.json).
|
||||
// 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
|
||||
@@ -33,20 +33,13 @@ type Module struct {
|
||||
Fixtures map[string][]seatdef.Fixture
|
||||
}
|
||||
|
||||
// Load reads a contracts module: `seats/index.json`, `seats/<seat>/seat.json`, `seats/<seat>/fixtures/*.json`
|
||||
// and `parts/*.json`. A seat with a directory and not in the index, or in the index with no directory, is refused.
|
||||
// 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{}}
|
||||
raw, err := os.ReadFile(filepath.Join(dir, "seats", "index.json"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ix, err := seatdef.ParseIndex(raw)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("seats/index.json: %w", err)
|
||||
}
|
||||
m.Order = ix.Seats
|
||||
parts, _ := filepath.Glob(filepath.Join(dir, "parts", "*.json"))
|
||||
sort.Strings(parts)
|
||||
for _, p := range parts {
|
||||
@@ -64,15 +57,8 @@ func Load(dir, goModule, tsPackage string) (*Module, error) {
|
||||
m.Parts[part.Part] = part
|
||||
}
|
||||
dirs, _ := filepath.Glob(filepath.Join(dir, "seats", "*", "seat.json"))
|
||||
inIndex := map[string]bool{}
|
||||
for _, s := range m.Order {
|
||||
inIndex[s] = true
|
||||
}
|
||||
for _, p := range dirs {
|
||||
name := filepath.Base(filepath.Dir(p))
|
||||
if !inIndex[name] {
|
||||
return nil, fmt.Errorf("%s: seat %q is not in seats/index.json", rel(dir, p), name)
|
||||
}
|
||||
raw, err := os.ReadFile(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -85,6 +71,7 @@ func Load(dir, goModule, tsPackage string) (*Module, error) {
|
||||
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)
|
||||
@@ -104,11 +91,7 @@ func Load(dir, goModule, tsPackage string) (*Module, error) {
|
||||
m.Fixtures[name] = append(m.Fixtures[name], cases...)
|
||||
}
|
||||
}
|
||||
for _, s := range m.Order {
|
||||
if _, ok := m.Seats[s]; !ok {
|
||||
return nil, fmt.Errorf("seats/index.json names %q, which has no seats/%s/seat.json", s, s)
|
||||
}
|
||||
}
|
||||
sort.Strings(m.Order)
|
||||
return m, nil
|
||||
}
|
||||
|
||||
@@ -121,6 +104,11 @@ func rel(dir, p string) string {
|
||||
|
||||
// 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
|
||||
|
||||
@@ -24,7 +24,7 @@ func TestAStateHasItsNamesKeyFormsAndStrictValues(t *testing.T) {
|
||||
}
|
||||
src := string(files["go/seats/aregistry/contract.go"])
|
||||
for _, want := range []string{
|
||||
"func AsksByAskerIDKey(asker, id string) string", "func ParseAsksByAskerIDKey(key string) (string, string, bool)",
|
||||
"func AsksByAskerIDKey(asker, id string) string", "func ParseAsksByAskerIDKey(key string) (string, string, bool)",
|
||||
"func DecodeAsksByAskerIDValue(raw []byte)", "type AsksByAskerIDValue struct",
|
||||
"func AsksByAskerIDDoneKey(", "func AsksByDefaultsKey() string",
|
||||
"func LocalNameBySeenIDOfAskKey(idOfAsk string) string",
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
{"format":1,"seats":["one-store","a-registry","b-registry"]}
|
||||
@@ -19,14 +19,15 @@ 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",
|
||||
"declaration": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
@@ -35,27 +36,28 @@ const tsConfig = `{
|
||||
}
|
||||
`
|
||||
|
||||
// tsPackage writes one npm package's package.json and tsconfig.json at dir.
|
||||
// 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{".": "./dist/index.js"},
|
||||
"files": []any{"dist"},
|
||||
"scripts": map[string]any{"build": "tsc", "prepare": "tsc"},
|
||||
"devDependencies": map[string]any{"typescript": "^5.6.0"},
|
||||
"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: the seat set's package at ts/ (the seat list alone, ADR 0625 §2) and one package per
|
||||
// seat defined whole at TSSeatDir (ADR 0625 §1), holding the parts it includes whole and importing nothing outside it.
|
||||
// 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 {
|
||||
tsPackage(out, "ts", m.TSPackage, "The mesh's seat set, generated from every seat's definition.")
|
||||
out["ts/src/index.ts"] = []byte(tsHeader + "export * as seats from \"./seats.js\";\n")
|
||||
out["ts/src/seats.ts"] = tsSeatList(m)
|
||||
partTypes := map[string]*typer{}
|
||||
partFiles := map[string][]byte{}
|
||||
for _, name := range sortedKeys(m.Parts) {
|
||||
@@ -221,98 +223,3 @@ func tsType(t *typ) string {
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// tsSeatList is ts/src/seats.ts: the seat list as data, and every seat's name.
|
||||
func tsSeatList(m *Module) []byte {
|
||||
var b bytes.Buffer
|
||||
b.WriteString(tsHeader)
|
||||
b.WriteString(`// The mesh's seat list, generated from every seat's definition (design 26).
|
||||
|
||||
export interface Verb {
|
||||
name: string;
|
||||
description: string;
|
||||
input?: Record<string, unknown>;
|
||||
output?: Record<string, unknown>;
|
||||
optional?: boolean;
|
||||
replaces?: string[];
|
||||
}
|
||||
|
||||
export interface Seat {
|
||||
name: string;
|
||||
scope: "mesh" | "node";
|
||||
grain: "whole" | "seat-list";
|
||||
decision: string;
|
||||
delivers?: string;
|
||||
replicated?: boolean;
|
||||
accepts?: string[];
|
||||
emits?: string[];
|
||||
pulledByKind?: boolean;
|
||||
yields?: boolean;
|
||||
heldByTheEngine?: boolean;
|
||||
serves?: Verb[];
|
||||
}
|
||||
|
||||
`)
|
||||
b.WriteString("export const names = {\n")
|
||||
for _, n := range m.Order {
|
||||
fmt.Fprintf(&b, " %s: %q,\n", tsName(n), n)
|
||||
}
|
||||
b.WriteString("} as const;\n\nexport const list: readonly Seat[] = ")
|
||||
var list []map[string]any
|
||||
for _, n := range m.Order {
|
||||
d := m.Seats[n]
|
||||
if d.HolderDeclares {
|
||||
continue
|
||||
}
|
||||
e := map[string]any{"name": d.Seat, "scope": d.Scope, "grain": d.Grain, "decision": d.Decision}
|
||||
if d.Delivers != "" {
|
||||
e["delivers"] = d.Delivers
|
||||
}
|
||||
if d.Replicated {
|
||||
e["replicated"] = true
|
||||
}
|
||||
if len(d.Accepts) > 0 {
|
||||
e["accepts"] = seatdef.MessageNames(d.Accepts)
|
||||
}
|
||||
if len(d.Events) > 0 {
|
||||
e["emits"] = seatdef.MessageNames(d.Events)
|
||||
}
|
||||
if d.PulledByKind {
|
||||
e["pulledByKind"] = true
|
||||
}
|
||||
if d.Yields {
|
||||
e["yields"] = true
|
||||
}
|
||||
if d.HeldByTheEngine {
|
||||
e["heldByTheEngine"] = true
|
||||
}
|
||||
var verbs []map[string]any
|
||||
for _, v := range d.Verbs {
|
||||
vm := map[string]any{"name": v.Name, "description": v.Description}
|
||||
if v.Input != nil {
|
||||
vm["input"] = resolveRefs(v.Input, d.Defs)
|
||||
}
|
||||
if v.Answer != nil {
|
||||
vm["output"] = resolveRefs(v.Answer, d.Defs)
|
||||
}
|
||||
if v.Optional {
|
||||
vm["optional"] = true
|
||||
}
|
||||
if len(v.Replaces) > 0 {
|
||||
vm["replaces"] = v.Replaces
|
||||
}
|
||||
verbs = append(verbs, vm)
|
||||
}
|
||||
if len(verbs) > 0 {
|
||||
e["serves"] = verbs
|
||||
}
|
||||
list = append(list, e)
|
||||
}
|
||||
enc := json.NewEncoder(&b)
|
||||
enc.SetEscapeHTML(false)
|
||||
enc.SetIndent("", " ")
|
||||
_ = enc.Encode(list)
|
||||
b.Truncate(b.Len() - 1)
|
||||
b.WriteString(";\n")
|
||||
return b.Bytes()
|
||||
}
|
||||
|
||||
@@ -146,40 +146,3 @@ func (tp *typer) inline(where string, s map[string]any) (*typ, error) {
|
||||
}
|
||||
return tp.read(where, s, false)
|
||||
}
|
||||
|
||||
// resolveRefs is a schema with every reference to a definition's own type replaced by that type, for the seat list
|
||||
// a reader shows: a caller reading a verb's input there never meets a reference it cannot follow.
|
||||
func resolveRefs(s map[string]any, defs map[string]map[string]any) map[string]any {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
v := resolveValue(s, defs, 0)
|
||||
m, _ := v.(map[string]any)
|
||||
return m
|
||||
}
|
||||
|
||||
func resolveValue(v any, defs map[string]map[string]any, depth int) any {
|
||||
if depth > 32 {
|
||||
return v
|
||||
}
|
||||
switch x := v.(type) {
|
||||
case map[string]any:
|
||||
if r, ok := x["$ref"].(string); ok {
|
||||
if d, ok := defs[strings.TrimPrefix(r, "#/defs/")]; ok {
|
||||
return resolveValue(d, defs, depth+1)
|
||||
}
|
||||
}
|
||||
out := make(map[string]any, len(x))
|
||||
for k, e := range x {
|
||||
out[k] = resolveValue(e, defs, depth+1)
|
||||
}
|
||||
return out
|
||||
case []any:
|
||||
out := make([]any, len(x))
|
||||
for i, e := range x {
|
||||
out[i] = resolveValue(e, defs, depth+1)
|
||||
}
|
||||
return out
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
@@ -85,7 +85,6 @@ func TestAPackageWithNeitherScriptNorTypeScriptHasNoTests(t *testing.T) {
|
||||
m, _, work = newMachine(t)
|
||||
workspaceWith(t, work, map[string]string{
|
||||
"module.json": `{"module":"c","build":{"artifacts":[{"name":"ts","kind":"package","language":"typescript","paths":["ts"],"generated-from":"."}]}}`,
|
||||
"seats/index.json": `{"format":1,"seats":["one-store"]}`,
|
||||
"seats/one-store/seat.json": `{"format":1,"seat":"one-store","scope":"mesh","grain":"seat-list","decision":"ADR 1","verbs":[{"name":"query","input":{"type":"object"}}]}`,
|
||||
"go/go.mod": "module example.org/c/go\n", "go/stray.go": "package stray\n",
|
||||
"ts/package.json": `{"name":"@example/c"}`, "ts/stray.ts.txt": "not written by the generator\n",
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep/seatgen"
|
||||
)
|
||||
|
||||
// F7 (ADR 0625 §5, ADR 0634 §8): a package the seat generator wrote, with its dependency-free lock and no script, is
|
||||
// installed from that lock (npm ci) and packed (npm pack) with no build script run, as any package artifact is.
|
||||
func TestAGeneratedSeatPackageIsInstalledFromItsLockAndPackedWithoutABuild(t *testing.T) {
|
||||
m, f, work := newMachine(t)
|
||||
seat := `{"format":1,"seat":"one-store","scope":"mesh","grain":"whole","decision":"ADR 1","verbs":[{"name":"query","input":{"type":"object"},"answer":{"type":"object"}}]}`
|
||||
src := t.TempDir()
|
||||
write(t, filepath.Join(src, "seats", "one-store", "seat.json"), seat, 0o644)
|
||||
g, err := seatgen.Load(src, "example.org/c/go", "@example/c")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
files, err := seatgen.Generate(g)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dir := filepath.Join(work, "w1", "tree", "ts", "seats", "onestore")
|
||||
for _, name := range []string{"package.json", "package-lock.json"} {
|
||||
body, ok := files["ts/seats/onestore/"+name]
|
||||
if !ok {
|
||||
t.Fatalf("the generator wrote no %s for the seat's package", name)
|
||||
}
|
||||
write(t, filepath.Join(dir, name), string(body), 0o644)
|
||||
}
|
||||
f.effect = func(c call) {
|
||||
if c.name == "npm" && len(c.args) > 0 && c.args[0] == "pack" {
|
||||
dest := c.args[slices.Index(c.args, "--pack-destination")+1]
|
||||
write(t, filepath.Join(dest, "example-c-one-store-0.1.0.tgz"), "tgz", 0o644)
|
||||
}
|
||||
}
|
||||
ctx := context.Background()
|
||||
if err := m.installDev(ctx, work, dir, "ts/seats/onestore"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
p, err := m.buildAndPack(ctx, work, dir, "ts/seats/onestore", false, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if p.Name != "@example/c-one-store" || p.Version != "0.1.0" {
|
||||
t.Errorf("packed %s@%s", p.Name, p.Version)
|
||||
}
|
||||
var ran []string
|
||||
for _, c := range f.calls {
|
||||
if c.name == "npm" {
|
||||
ran = append(ran, c.args[0])
|
||||
}
|
||||
}
|
||||
if !slices.Equal(ran, []string{"ci", "pack"}) {
|
||||
t.Errorf("npm ran %v, want ci then pack and no build script", ran)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user