185 lines
4.6 KiB
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
|
|
}
|
|
}
|