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

185 lines
4.6 KiB
Go

package scaffold
import (
"embed"
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"sort"
"strings"
"text/template"
)
//go:embed templates
var templateFiles embed.FS
// Options controls project generation.
type Options struct {
Dir string
Module string
FrameworkDir string
Force bool
}
var files = map[string]string{
"go.mod": "templates/go.mod.tmpl",
"README.md": "templates/README.md.tmpl",
"cmd/server/main.go": "templates/cmd-server-main.go.tmpl",
"internal/model/user.go": "templates/internal-model-user.go.tmpl",
"internal/repository/memory.go": "templates/internal-repository-memory.go.tmpl",
"internal/servicecontext/context.go": "templates/internal-servicecontext-context.go.tmpl",
"internal/service/user.go": "templates/internal-service-user.go.tmpl",
"internal/handler/user.go": "templates/internal-handler-user.go.tmpl",
}
// Generate creates a GoOmni service skeleton.
func Generate(options Options) error {
if strings.TrimSpace(options.Dir) == "" {
return errors.New("project directory is required")
}
if strings.TrimSpace(options.Module) == "" {
return errors.New("module path is required")
}
directory, err := filepath.Abs(options.Dir)
if err != nil {
return fmt.Errorf("resolve project directory: %w", err)
}
if err := checkTarget(directory, options.Force); err != nil {
return err
}
if err := os.MkdirAll(directory, 0o755); err != nil {
return fmt.Errorf("create project directory: %w", err)
}
data := templateData{
Module: options.Module,
FrameworkReplace: frameworkReplace(directory, options.FrameworkDir),
}
paths := make([]string, 0, len(files))
for path := range files {
paths = append(paths, path)
}
sort.Strings(paths)
for _, path := range paths {
content, err := render(files[path], data)
if err != nil {
return fmt.Errorf("render %s: %w", path, err)
}
target := filepath.Join(directory, filepath.FromSlash(path))
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
return fmt.Errorf("create %s: %w", filepath.Dir(path), err)
}
if !options.Force {
if _, err := os.Stat(target); err == nil {
return fmt.Errorf("file already exists: %s", target)
} else if !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("check %s: %w", target, err)
}
}
if err := os.WriteFile(target, []byte(content), 0o644); err != nil {
return fmt.Errorf("write %s: %w", target, err)
}
}
return nil
}
type templateData struct {
Module string
FrameworkReplace string
}
func render(name string, data templateData) (string, error) {
content, err := fs.ReadFile(templateFiles, name)
if err != nil {
return "", err
}
tmpl, err := newTemplate(name, string(content))
if err != nil {
return "", err
}
var output strings.Builder
if err := tmpl.Execute(&output, data); err != nil {
return "", err
}
return output.String(), nil
}
func newTemplate(name, content string) (*template.Template, error) {
return template.New(name).Parse(content)
}
func checkTarget(directory string, force bool) error {
info, err := os.Stat(directory)
if errors.Is(err, os.ErrNotExist) {
return nil
}
if err != nil {
return fmt.Errorf("check project directory: %w", err)
}
if !info.IsDir() {
return fmt.Errorf("project path is not a directory: %s", directory)
}
if force {
return nil
}
entries, err := os.ReadDir(directory)
if err != nil {
return fmt.Errorf("read project directory: %w", err)
}
if len(entries) > 0 {
return fmt.Errorf("project directory is not empty: %s (use --force to overwrite generated files)", directory)
}
return nil
}
func frameworkReplace(projectDir, configured string) string {
root := strings.TrimSpace(configured)
if root == "" {
root = findFrameworkRoot()
}
if root == "" {
return ""
}
projectDir = canonicalPath(projectDir)
absoluteRoot := canonicalPath(root)
relative, err := filepath.Rel(projectDir, absoluteRoot)
if err != nil {
return ""
}
return filepath.ToSlash(relative)
}
func canonicalPath(path string) string {
abs, err := filepath.Abs(path)
if err != nil {
return path
}
resolved, err := filepath.EvalSymlinks(abs)
if err == nil {
return resolved
}
return abs
}
func findFrameworkRoot() string {
directory, err := os.Getwd()
if err != nil {
return ""
}
for {
moduleFile := filepath.Join(directory, "go.mod")
content, readErr := os.ReadFile(moduleFile)
if readErr == nil && strings.Contains(string(content), "module git.yuntuo.hk/k2safe/go-omni") {
return directory
}
parent := filepath.Dir(directory)
if parent == directory {
return ""
}
directory = parent
}
}