215 lines
7.6 KiB
Go
215 lines
7.6 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, mesh-sdk's runtime and its domain's SDK (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 m.DomainSDK != "" && (path == m.DomainSDK || strings.HasPrefix(path, m.DomainSDK+"/")):
|
|
// the domain's SDK (ADR 0634 §2)
|
|
case path == RuntimeModule || strings.HasPrefix(path, RuntimeModule+"/"):
|
|
case path == ClientGoModule(m.GoModule, seat) || strings.HasPrefix(path, ClientGoModule(m.GoModule, seat)+"/"):
|
|
// the client's own packages: a client may be several packages of its module
|
|
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
|
|
}
|
|
|
|
// CheckArtifacts is the check of module.json's package artifacts against the tree (ADR 0625 §1, §3), per language.
|
|
// 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"))
|
|
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": {}, "typescript": {}}
|
|
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 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))
|
|
}
|
|
}
|
|
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
|
|
}
|