Its version is the build module's own, so a new generator is a new build of mesh-build-go and mesh-build-node and a new fingerprint of what they compare; no repository of its own. Moved from seatgen c1fdd7c unchanged but for the import path. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
218 lines
7.4 KiB
Go
218 lines
7.4 KiB
Go
package seatgen
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/parser"
|
|
"go/token"
|
|
"os"
|
|
"path/filepath"
|
|
re "regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/seatdef"
|
|
)
|
|
|
|
// ClientRoot is where a seat's hand-written client packages live (ADR 0625 §4, §5; ADR 0627): one directory per
|
|
// client, `go/clients/<seat without hyphens>client`, a Go module of its own (ADR 0625 §3), so the seat set's importers
|
|
// never import a client and a client's import of mesh-sdk forms no cycle. Compare leaves the tree to CheckClients.
|
|
const ClientRoot = "go/clients"
|
|
|
|
// RuntimeModule is mesh-sdk's Go module: the one module beside its own seat's contract a client may import (ADR 0625
|
|
// §3, §4).
|
|
const RuntimeModule = "git.novox.be/novox/mesh-sdk/go"
|
|
|
|
// ClientGoModule is the Go module of a seat's client, beside its contract's.
|
|
func ClientGoModule(root, seat string) string { return root + "/clients/" + pkgName(seat) + "client" }
|
|
|
|
// a module's tool address (`<module>.<tool_name>`) or call (`<module>.<tool>` written with an underscore).
|
|
var toolAddress = re.MustCompile(`^[a-z][a-z0-9-]*\.[a-z0-9]+_[a-z0-9_]+$`)
|
|
|
|
// CheckClients is ADR 0627 §3's catalog check of the clients, and ADR 0625 §5's rule for the files beside them: every
|
|
// directory under ClientRoot is the client of one seat defined whole, a Go module of its own at ClientGoModule; it
|
|
// imports only the standard library, its own seat's contract module and mesh-sdk's runtime (never another seat's
|
|
// contract or client); and no string in it names a module's subject (`mesh.mod.`), a module's tool, or another seat
|
|
// (its name, its subjects, its `seat:` call). Each problem is named once, sorted.
|
|
func CheckClients(dir string, m *Module) ([]string, error) {
|
|
var out []string
|
|
entries, err := os.ReadDir(filepath.Join(dir, ClientRoot))
|
|
if os.IsNotExist(err) {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
bySeatPkg := map[string]string{}
|
|
for name, d := range m.Seats {
|
|
if d.Grain == seatdef.Whole {
|
|
bySeatPkg[pkgName(name)+"client"] = name
|
|
}
|
|
}
|
|
for _, e := range entries {
|
|
at := ClientRoot + "/" + e.Name()
|
|
if !e.IsDir() {
|
|
out = append(out, at+": a file under the clients' root, in no client")
|
|
continue
|
|
}
|
|
seat, ok := bySeatPkg[e.Name()]
|
|
if !ok {
|
|
out = append(out, at+": the client of no seat defined whole (a client is `<seat without hyphens>client`)")
|
|
continue
|
|
}
|
|
mod, err := os.ReadFile(filepath.Join(dir, at, "go.mod"))
|
|
want := "module " + ClientGoModule(m.GoModule, seat)
|
|
if err != nil || strings.SplitN(string(mod), "\n", 2)[0] != want {
|
|
out = append(out, at+"/go.mod: not "+strconv.Quote(want)+": a client is a Go module of its own (ADR 0625 §3)")
|
|
}
|
|
problems, err := checkClient(dir, at, seat, m)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, problems...)
|
|
}
|
|
sort.Strings(out)
|
|
return out, nil
|
|
}
|
|
|
|
func checkClient(dir, at, seat string, m *Module) ([]string, error) {
|
|
var out []string
|
|
own := SeatGoModule(m.GoModule, seat)
|
|
others := map[string]bool{}
|
|
for name := range m.Seats {
|
|
if name != seat {
|
|
others[name] = true
|
|
}
|
|
}
|
|
namesOther := func(s string) bool {
|
|
if others[s] {
|
|
return true
|
|
}
|
|
for name := range others {
|
|
if strings.HasPrefix(s, "mesh.seat."+name+".") || strings.HasPrefix(s, "seat:"+name+".") {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
err := filepath.WalkDir(filepath.Join(dir, at), func(p string, e os.DirEntry, err error) error {
|
|
if err != nil || e.IsDir() || !strings.HasSuffix(p, ".go") {
|
|
return err
|
|
}
|
|
r := rel(dir, p)
|
|
f, err := parser.ParseFile(token.NewFileSet(), p, nil, parser.SkipObjectResolution)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, im := range f.Imports {
|
|
path, _ := strconv.Unquote(im.Path.Value)
|
|
switch {
|
|
case !strings.Contains(strings.SplitN(path, "/", 2)[0], "."):
|
|
case path == own:
|
|
case path == RuntimeModule || strings.HasPrefix(path, RuntimeModule+"/"):
|
|
case strings.HasPrefix(path, m.GoModule+"/clients/"):
|
|
out = append(out, r+": imports "+path+", another seat's client (ADR 0625 §4)")
|
|
case path == m.GoModule || strings.HasPrefix(path, m.GoModule+"/"):
|
|
out = append(out, r+": imports "+path+", not its own seat's contract (ADR 0625 §4)")
|
|
default:
|
|
out = append(out, r+": imports "+path+": a client uses only its seat's contract and mesh-sdk's runtime (ADR 0625 §3)")
|
|
}
|
|
}
|
|
ast.Inspect(f, func(n ast.Node) bool {
|
|
lit, ok := n.(*ast.BasicLit)
|
|
if !ok || lit.Kind != token.STRING {
|
|
return true
|
|
}
|
|
v, err := strconv.Unquote(lit.Value)
|
|
if err != nil {
|
|
return true
|
|
}
|
|
switch {
|
|
case strings.HasPrefix(v, "mesh.mod."):
|
|
out = append(out, fmt.Sprintf("%s: names a module's subject %q (ADR 0625 §4)", r, v))
|
|
case toolAddress.MatchString(v):
|
|
out = append(out, fmt.Sprintf("%s: names a module's tool %q (ADR 0625 §4)", r, v))
|
|
case namesOther(v):
|
|
out = append(out, fmt.Sprintf("%s: names another seat %q (ADR 0625 §4)", r, v))
|
|
}
|
|
return true
|
|
})
|
|
return nil
|
|
})
|
|
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,
|
|
// sorted.
|
|
func CheckArtifacts(dir string, m *Module) ([]string, error) {
|
|
raw, err := os.ReadFile(filepath.Join(dir, "module.json"))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var mf struct {
|
|
Build struct {
|
|
Artifacts []struct {
|
|
Name string `json:"name"`
|
|
Kind string `json:"kind"`
|
|
Language string `json:"language"`
|
|
Paths []string `json:"paths"`
|
|
} `json:"artifacts"`
|
|
} `json:"build"`
|
|
}
|
|
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"},
|
|
}
|
|
for name, d := range m.Seats {
|
|
if d.Grain == seatdef.Whole {
|
|
want["go"]["go-"+pkgName(name)] = "go/seats/" + pkgName(name)
|
|
want["typescript"]["ts-"+pkgName(name)] = TSSeatDir(name)
|
|
}
|
|
}
|
|
if entries, err := os.ReadDir(filepath.Join(dir, ClientRoot)); err == nil {
|
|
for _, e := range entries {
|
|
if e.IsDir() {
|
|
want["go"]["go-"+e.Name()] = ClientRoot + "/" + e.Name()
|
|
}
|
|
}
|
|
}
|
|
said := map[string]string{"go": "Go", "typescript": "npm"}
|
|
var out []string
|
|
seen := map[string]bool{}
|
|
for _, a := range mf.Build.Artifacts {
|
|
paths, lang := want[a.Language]
|
|
if a.Kind != "package" || !lang {
|
|
continue
|
|
}
|
|
seen[a.Language+"/"+a.Name] = true
|
|
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")
|
|
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))
|
|
}
|
|
}
|
|
for lang, paths := range want {
|
|
for name, path := range paths {
|
|
if !seen[lang+"/"+name] {
|
|
out = append(out, "module.json: no "+said[lang]+" package artifact "+name+" for "+path)
|
|
}
|
|
}
|
|
}
|
|
sort.Strings(out)
|
|
return out, nil
|
|
}
|