Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 32 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.

<details>
<summary>Example</summary>
<summary>Help</summary>

```shell
$ ghost -h
Usage: ghost [options...]
Usage: ghost [options...] [arguments...]

OPTIONS
-d, --description <string> (default: Go microservice) Project short description
-g, --go-image <string> (default: nafigat0r/go:1.24.4) Go docker image
-g, --go-image <string> (default: nafigat0r/go:1.26.0) Go docker image
-c, --govulncheck-image <string> (default: nafigat0r/govulncheck:1.1.4) Govulncheck docker image
-h, --help display this help message
-l, --linter-image <string> (default: nafigat0r/golangci-lint:2.1.6) Linter docker image
-l, --linter-image <string> (default: nafigat0r/golangci-lint:2.9.0) Linter docker image
-m, --module-name <string> (default: github.com/test/test) Go module name
-n, --name <string> (default: test) Project short name
-t, --shutdown-timeout <duration> (default: 10s) Timeout for graceful shutdown
Expand All @@ -37,16 +40,38 @@ OPTIONS

ENVIRONMENT
GHOST_DESCRIPTION <string> (default: Go microservice) Project short description
GHOST_GO_IMAGE <string> (default: nafigat0r/go:1.24.4) Go docker image
GHOST_GO_IMAGE <string> (default: nafigat0r/go:1.26.0) Go docker image
GHOST_GOVULNCHECK_IMAGE <string> (default: nafigat0r/govulncheck:1.1.4) Govulncheck docker image
GHOST_LINTER_IMAGE <string> (default: nafigat0r/golangci-lint:2.1.6) Linter docker image
GHOST_LINTER_IMAGE <string> (default: nafigat0r/golangci-lint:2.9.0) Linter docker image
GHOST_MODULE_NAME <string> (default: github.com/test/test) Go module name
GHOST_NAME <string> (default: test) Project short name
GHOST_SHUTDOWN_TIMEOUT <duration> (default: 10s) Timeout for graceful shutdown
GHOST_WITH_REST <bool> (default: false) Add HTTP server with REST API functionality
```
</details>

<details>
<summary>Create REST microservice in <b>dir</b></summary>

```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
```
</details>

#### Docker

<details>
Expand Down
3 changes: 1 addition & 2 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
}

Expand Down
13 changes: 8 additions & 5 deletions internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
42 changes: 38 additions & 4 deletions internal/app/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,27 @@ package config
import (
"errors"
"fmt"
"io/fs"
"os"
"time"

"github.com/ardanlabs/conf/v3"
"go.uber.org/zap"
)

const (
appName = "GHOST (Go High-level Open Service Templater)"
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"`
Expand All @@ -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

Expand All @@ -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
}
Expand All @@ -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
}
17 changes: 9 additions & 8 deletions internal/app/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"fmt"
"os"
"path/filepath"
"strings"
"text/template"

Expand Down Expand Up @@ -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
}
}
Expand All @@ -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
Expand Down
6 changes: 3 additions & 3 deletions internal/app/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (

type tp struct {
dir string
file string
path string
src string
}

Expand All @@ -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",
Expand Down Expand Up @@ -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),
}

Expand Down
Loading