Files
go-omni/cmd/go-omni/apigen/workspace.go

145 lines
3.7 KiB
Go

package apigen
import (
"errors"
"fmt"
apierrors "git.yuntuo.hk/k2safe/go-omni/errors"
"io"
"os"
"path/filepath"
"strings"
)
type WorkspaceService struct {
Dir string `json:"dir"`
API string `json:"api,omitempty"`
Module string `json:"module,omitempty"`
}
type WorkspaceConfig struct {
Services []WorkspaceService `json:"services"`
ErrorCatalogs []WorkspaceCatalog `json:"errorCatalogs,omitempty"`
}
// Workspace operates independent Go modules listed in an explicit local file.
// All projects are preflighted before generate/format writes. Each generator
// transaction is atomic within its project; no cross-module transaction is promised.
func Workspace(path, command string, w io.Writer) error {
if command != "generate" && command != "check" && command != "format" {
return errors.New("workspace command must be generate, check or format")
}
if w == nil {
w = io.Discard
}
real, err := filepath.Abs(path)
if err != nil {
return err
}
stat, err := os.Lstat(real)
if err != nil {
return err
}
if !stat.Mode().IsRegular() || stat.Size() > 65536 {
return errors.New("workspace configuration must be a regular file of at most 64 KiB")
}
raw, err := os.ReadFile(real)
if err != nil {
return err
}
var cfg WorkspaceConfig
if err = apierrors.DecodeCatalogJSON(raw, &cfg); err != nil {
return err
}
if len(cfg.Services) == 0 || len(cfg.Services) > 64 {
return errors.New("workspace must list 1..64 services")
}
root := canonical(filepath.Dir(real))
var options []Options
seen := map[string]bool{}
for _, service := range cfg.Services {
if service.Dir == "" || service.Dir == "." {
return errors.New("workspace requires distinct relative service directories")
}
if err = safePath(root, service.Dir+"/.goOmniWorkspaceCheck"); err != nil {
return err
}
dir := filepath.Join(root, filepath.FromSlash(service.Dir))
key := strings.ToLower(dir)
for prior := range seen {
if prior == key || strings.HasPrefix(key, prior+string(filepath.Separator)) || strings.HasPrefix(prior, key+string(filepath.Separator)) {
return errors.New("workspace service directories overlap")
}
}
seen[key] = true
entry := service.API
if entry == "" {
entry, err = ProjectEntry(dir)
} else {
if err = safePath(dir, entry); err != nil {
return err
}
entry = filepath.Join(dir, entry)
}
if err != nil {
return err
}
options = append(options, Options{API: entry, Dir: dir, Module: service.Module, DryRun: true})
}
var catalogs map[string][]byte
if command != "format" {
catalogs, err = prepareWorkspaceCatalogs(root, cfg.ErrorCatalogs, command == "check")
if err != nil {
return err
}
for i := range options {
options[i].documentCatalogs = catalogs
}
}
var failures error
for _, o := range options {
if command == "format" {
spec, e := Parse(o.API)
if e == nil {
_, e = Compile(spec)
}
err = e
} else {
_, err = Generate(o)
}
if err != nil {
failures = errors.Join(failures, fmt.Errorf("%s: %w", o.Dir, err))
}
}
if failures != nil {
return failures
}
if command == "generate" {
for path, data := range catalogs {
if err = writeWorkspaceCatalog(path, data); err != nil {
return err
}
}
}
for _, o := range options {
switch command {
case "format":
_, err = FormatProject(o.API, false)
case "generate":
o.DryRun = false
_, err = Generate(o)
case "check":
_, fmtErr := FormatProject(o.API, true)
o.DryRun = false
o.Check = true
_, genErr := Generate(o)
err = errors.Join(fmtErr, genErr)
}
if err != nil {
failures = errors.Join(failures, fmt.Errorf("%s: %w", o.Dir, err))
fmt.Fprintf(w, "FAIL %s\n", o.Dir)
} else {
fmt.Fprintf(w, "OK %s\n", o.Dir)
}
}
return failures
}