Files
go-omni/cmd/go-omni/scaffold/addModule.go

110 lines
3.2 KiB
Go

package scaffold
import (
"bytes"
"errors"
"fmt"
"go/format"
"go/token"
"os"
"path/filepath"
"regexp"
"strings"
"text/template"
"golang.org/x/mod/modfile"
)
var moduleName = regexp.MustCompile(`^[a-z][a-zA-Z0-9]{0,47}$`)
// AddModule creates a complete, injectable memory-backed CRUD module. It never
// edits existing routes, go.mod, migrations or files in another module.
func AddModule(directory, name string) (err error) {
if strings.TrimSpace(directory) == "" || !moduleName.MatchString(name) || token.Lookup(name).IsKeyword() || name == "main" || name == "init" || name == "internal" {
return errors.New("module name must be a non-keyword lowerCamelCase Go identifier (1..48 characters)")
}
root, err := filepath.Abs(directory)
if err != nil {
return err
}
root, err = filepath.EvalSymlinks(root)
if err != nil {
return err
}
data, err := os.ReadFile(filepath.Join(root, "go.mod"))
if err != nil {
return fmt.Errorf("read project go.mod: %w", err)
}
manifest, err := modfile.Parse("go.mod", data, nil)
if err != nil || manifest.Module == nil {
return errors.New("project go.mod must declare a valid module")
}
found := false
for _, dependency := range manifest.Require {
if dependency.Mod.Path == frameworkModule {
found = true
}
}
if !found {
return errors.New("project must already require go-omni; create it with go-omni new")
}
module := manifest.Module.Mod.Path
if strings.ContainsAny(module, "\"\\\r\n") {
return errors.New("invalid project module path")
}
paths := []string{"module.go", "model/model.go", "repository/repository.go", "service/service.go", "handler/handler.go", "README.md"}
rendered := make(map[string][]byte, len(paths))
for _, path := range paths {
source, readErr := templateFiles.ReadFile("moduleTemplates/" + strings.ReplaceAll(path, "/", "-") + ".tmpl")
if readErr != nil {
return readErr
}
tmpl, parseErr := template.New(path).Parse(string(source))
if parseErr != nil {
return parseErr
}
var output bytes.Buffer
if err := tmpl.Execute(&output, struct{ Module, Name string }{module, name}); err != nil {
return err
}
content := output.Bytes()
if strings.HasSuffix(path, ".go") {
content, err = format.Source(content)
if err != nil {
return err
}
}
rendered[path] = content
}
parent := filepath.Join(root, "internal")
if info, statErr := os.Lstat(parent); statErr == nil {
if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
return errors.New("internal must be a real directory, not a symlink")
}
} else if !errors.Is(statErr, os.ErrNotExist) {
return statErr
} else if err := os.Mkdir(parent, 0755); err != nil {
return err
}
target := filepath.Join(parent, name)
// Exclusive creation protects existing files, including empty directories.
if err := os.Mkdir(target, 0755); err != nil {
return fmt.Errorf("create module (existing modules are never overwritten): %w", err)
}
defer func() {
if err != nil {
_ = os.RemoveAll(target)
}
}()
for _, path := range paths {
file := filepath.Join(target, filepath.FromSlash(path))
if err = os.MkdirAll(filepath.Dir(file), 0755); err != nil {
return err
}
if err = os.WriteFile(file, rendered[path], 0644); err != nil {
return err
}
}
return nil
}