110 lines
3.2 KiB
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
|
|
}
|