diff --git a/README.md b/README.md index df37c03..e1cc764 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Go High-level Open Service Templater -GHOST creates in working dir fully functional microservice with basic functionality: +GHOST creates in output dir fully functional microservice code with basic functionality: - Ready to use Makefile with help target - Ready to use docker-compose with override template @@ -16,19 +16,22 @@ GHOST creates in working dir fully functional microservice with basic functional ## Usage +> [!NOTE] +> Output directory (relative or absolute). Files will be created at paths relative to this directory. Existing files at target paths will be overwritten. +
- Example + Help ```shell $ ghost -h -Usage: ghost [options...] +Usage: ghost [options...] [arguments...] OPTIONS -d, --description (default: Go microservice) Project short description - -g, --go-image (default: nafigat0r/go:1.24.4) Go docker image + -g, --go-image (default: nafigat0r/go:1.26.0) Go docker image -c, --govulncheck-image (default: nafigat0r/govulncheck:1.1.4) Govulncheck docker image -h, --help display this help message - -l, --linter-image (default: nafigat0r/golangci-lint:2.1.6) Linter docker image + -l, --linter-image (default: nafigat0r/golangci-lint:2.9.0) Linter docker image -m, --module-name (default: github.com/test/test) Go module name -n, --name (default: test) Project short name -t, --shutdown-timeout (default: 10s) Timeout for graceful shutdown @@ -37,9 +40,9 @@ OPTIONS ENVIRONMENT GHOST_DESCRIPTION (default: Go microservice) Project short description - GHOST_GO_IMAGE (default: nafigat0r/go:1.24.4) Go docker image + GHOST_GO_IMAGE (default: nafigat0r/go:1.26.0) Go docker image GHOST_GOVULNCHECK_IMAGE (default: nafigat0r/govulncheck:1.1.4) Govulncheck docker image - GHOST_LINTER_IMAGE (default: nafigat0r/golangci-lint:2.1.6) Linter docker image + GHOST_LINTER_IMAGE (default: nafigat0r/golangci-lint:2.9.0) Linter docker image GHOST_MODULE_NAME (default: github.com/test/test) Go module name GHOST_NAME (default: test) Project short name GHOST_SHUTDOWN_TIMEOUT (default: 10s) Timeout for graceful shutdown @@ -47,6 +50,28 @@ ENVIRONMENT ```
+
+ Create REST microservice in dir + +```shell +$ ghost -r dir +2026-09-19 15:26:46.005 INFO config/config.go:60 Initial config: + --args=[dir] + --build=develop + --desc=GHOST (Go High-level Open Service Templater) + --description=Go microservice + --go-image=nafigat0r/go:1.26.0 + --govulncheck-image=nafigat0r/govulncheck:1.1.4 + --linter-image=nafigat0r/golangci-lint:2.9.0 + --module-name=github.com/test/test + --name=test + --shutdown-timeout=10s + --with-rest=true +2026-09-19 15:26:46.005 INFO app/app.go:33 ghost start +2026-09-19 15:26:46.007 INFO app/app.go:40 ghost shutdown +``` +
+ #### Docker
diff --git a/cmd/main.go b/cmd/main.go index faa7a7a..bc4584a 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -4,7 +4,6 @@ import ( "errors" "io/fs" - c "github.com/ardanlabs/conf/v3" "github.com/nafigator/zapper" "github.com/nafigator/zapper/conf" "go.uber.org/zap" @@ -17,7 +16,7 @@ func main() { defer sync(log) if err := app.Run(log); err != nil { - if errors.Is(err, c.ErrHelpWanted) { + if errors.Is(err, app.ErrInfo) { return } diff --git a/internal/app/app.go b/internal/app/app.go index 7721e33..4638266 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -15,14 +15,17 @@ const ( shutdownMsg = "ghost shutdown" ) -var buildInfo = "develop" //nolint:gochecknoglobals //build-flag +var ( + buildInfo = "develop" //nolint:gochecknoglobals //build-flag + // ErrInfo signals that the application has output requested information + // (help, version) and should exit successfully. + ErrInfo = config.ErrInfo +) // Run runs application. func Run(log *zap.SugaredLogger) error { - var c *config.Conf - var err error - - if c, err = config.Init(buildInfo, log); err != nil { + c, err := config.Init(buildInfo, log) + if err != nil { return err } diff --git a/internal/app/config/config.go b/internal/app/config/config.go index 5dc89f6..28fab6e 100644 --- a/internal/app/config/config.go +++ b/internal/app/config/config.go @@ -4,10 +4,11 @@ package config import ( "errors" "fmt" + "io/fs" + "os" "time" "github.com/ardanlabs/conf/v3" - "go.uber.org/zap" ) const ( @@ -15,9 +16,15 @@ const ( cfgPrefix = "GHOST" ) -type Conf struct { +type logger interface { + Infof(format string, args ...any) +} + +type Conf struct { //nolint:govet // Acknowledged + conf.Args conf.Version + OutputDir string `conf:"-"` Name string `conf:"default:test,short:n,help:Project short name"` Description string `conf:"default:Go microservice,short:d,help:Project short description"` ModuleName string `conf:"default:github.com/test/test,short:m,help:Go module name"` @@ -28,7 +35,11 @@ type Conf struct { WithREST bool `conf:"default:false,short:r,help:Add HTTP server with REST API functionality"` } -func Init(build string, log *zap.SugaredLogger) (*Conf, error) { +// ErrInfo is returned when the user requested informational output +// (help, version) and the application should exit without error. +var ErrInfo = errors.New("info wanted") + +func Init(build string, log logger) (*Conf, error) { var c string var err error @@ -40,13 +51,19 @@ func Init(build string, log *zap.SugaredLogger) (*Conf, error) { } if c, err = conf.Parse(cfgPrefix, &cfg); err != nil { - if errors.Is(err, conf.ErrHelpWanted) { + if errors.Is(err, conf.ErrHelpWanted) || errors.Is(err, conf.ErrVersionWanted) { fmt.Println(c) //nolint:forbidigo // Need for raw output of help message + + return nil, ErrInfo } return nil, err } + if err = resolveOutputDir(&cfg); err != nil { + return nil, err + } + if c, err = conf.String(&cfg); err != nil { return nil, err } @@ -55,3 +72,20 @@ func Init(build string, log *zap.SugaredLogger) (*Conf, error) { return &cfg, nil } + +func resolveOutputDir(cfg *Conf) error { + cfg.OutputDir = cfg.Num(0) + if cfg.OutputDir == "" { + return errors.New("output directory not specified") + } + + if d, err := os.Stat(cfg.OutputDir); err == nil { + if !d.IsDir() { + return fmt.Errorf("output dir %q exists and is not a directory", cfg.OutputDir) + } + } else if !errors.Is(err, fs.ErrNotExist) { + return fmt.Errorf("stat %s: %w", cfg.OutputDir, err) + } + + return nil +} diff --git a/internal/app/generate.go b/internal/app/generate.go index 9bd97ad..277602a 100644 --- a/internal/app/generate.go +++ b/internal/app/generate.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "os" + "path/filepath" "strings" "text/template" @@ -37,7 +38,7 @@ func generate(c *config.Conf) error { } for _, t := range tt { - if err = write(t, fn, vars); err != nil { + if err = write(c.OutputDir, t, fn, vars); err != nil { return err } } @@ -57,23 +58,23 @@ func createDir(d string) error { return nil } -func write(t tp, fn template.FuncMap, vars map[string]any) error { - if err := createDir(t.dir); err != nil { +func write(target string, t tp, fn template.FuncMap, vars map[string]any) error { + if err := createDir(filepath.Join(target, t.dir)); err != nil { return err } - tpl, err := template.New(t.file).Funcs(fn).Parse(t.src) + tpl, err := template.New(t.path).Funcs(fn).Parse(t.src) if err != nil { - return err + return fmt.Errorf("parse %s: %w", t.path, err) } var buf bytes.Buffer if err = tpl.Execute(&buf, vars); err != nil { - return fmt.Errorf("execute %s: %w", t.file, err) + return fmt.Errorf("execute %s: %w", t.path, err) } - if err = os.WriteFile(t.file, buf.Bytes(), fileStrictMode); err != nil { - return fmt.Errorf("write %s: %w", t.file, err) + if err = os.WriteFile(filepath.Join(target, t.path), buf.Bytes(), fileStrictMode); err != nil { + return fmt.Errorf("write %s: %w", t.path, err) } return nil diff --git a/internal/app/init.go b/internal/app/init.go index e8a5a0d..23d6cf1 100644 --- a/internal/app/init.go +++ b/internal/app/init.go @@ -12,7 +12,7 @@ import ( type tp struct { dir string - file string + path string src string } @@ -28,7 +28,7 @@ func transformName(name string) string { return name } -var rename = map[string]string{ //nolint: gochecknoglobals // Acknowledged +var rename = map[string]string{ //nolint:gochecknoglobals // Acknowledged "makefile.gotmpl": "Makefile", "gomod.gotmpl": "go.mod", "gosum.gotmpl": "go.sum", @@ -93,7 +93,7 @@ func load(tt tps, rfs fs.FS) (tps, error) { t := tp{ dir: filepath.Dir(out), - file: out, + path: out, src: string(b), }