Skip to content

Commit e810fd0

Browse files
committed
rest server done
1 parent db39ec1 commit e810fd0

24 files changed

Lines changed: 663 additions & 79 deletions

README.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,18 @@ Inspired by the fact that I hate rewriting and copy pasting code that I have alr
88

99
P.S. This was encountered while working on [fáfnir](https://github.com/andrearcaina/fafnir).
1010

11+
> The idea is that these are backend services (like microservices), not an entire backend monolith implementation. For starters though this generates a good strtucture, but for it to be an entire monolith it would need to require extra setup from yourself.
12+
1113

1214
### TODO
1315

1416
In no particular order:
1517

1618
- [ ] Add REST, gRPC, and GraphQL templates to my liking
17-
- [ ] Add SQLc support to my liking
18-
- [ ] Implement quality of life features like:
19-
- [ ] Flags for every possible option
20-
- [ ] Interactive UI for when flags are not provided
19+
- [X] REST
20+
- [ ] gRPC
21+
- [ ] GraphQL
22+
- [X] Add SQLc support to my liking
23+
- [X] Implement quality of life features like:
24+
- [X] Flags for every possible option
25+
- [X] Interactive UI for when flags are not provided

cmd/generate.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,9 @@ func init() {
3232
rootCmd.AddCommand(generateCmd)
3333

3434
generateCmd.Flags().StringVarP(&cfg.OutputPath, "path", "p", ".", "The directory to write the generated file to")
35+
generateCmd.Flags().BoolVarP(&cfg.Default, "default", "d", false, "Use default configuration values")
3536

36-
// following flags are just for test data
37-
generateCmd.Flags().StringVar(&cfg.Form.SomeFlag, "some-flag", "", "A flag to specify who to say hello to")
38-
generateCmd.Flags().StringVar(&cfg.Form.AnotherFlag, "another-flag", "", "Another flag")
37+
generateCmd.Flags().StringVarP(&cfg.Form.Name, "name", "n", "", "The name for the go.mod module")
38+
generateCmd.Flags().StringVarP((*string)(&cfg.Form.ServerTypeFlag), "server", "s", "", "Type of server to generate (rest/grpc/graphql)")
39+
generateCmd.Flags().BoolVar(&cfg.Form.DatabaseFlag, "database", true, "Generate database files")
3940
}

go.mod

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,21 @@ module github.com/andrearcaina/goforge
22

33
go 1.24.5
44

5-
require github.com/spf13/cobra v1.10.2
5+
require (
6+
github.com/charmbracelet/huh v0.8.0
7+
github.com/charmbracelet/huh/spinner v0.0.0-20251215014908-6f7d32faaff3
8+
github.com/charmbracelet/lipgloss v1.1.0
9+
github.com/spf13/cobra v1.10.2
10+
)
611

712
require (
813
github.com/atotto/clipboard v0.1.4 // indirect
914
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
1015
github.com/catppuccin/go v0.3.0 // indirect
1116
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 // indirect
12-
github.com/charmbracelet/bubbletea v1.3.6 // indirect
17+
github.com/charmbracelet/bubbletea v1.3.10 // indirect
1318
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
14-
github.com/charmbracelet/huh v0.8.0 // indirect
15-
github.com/charmbracelet/lipgloss v1.1.0 // indirect
16-
github.com/charmbracelet/x/ansi v0.9.3 // indirect
19+
github.com/charmbracelet/x/ansi v0.10.1 // indirect
1720
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
1821
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
1922
github.com/charmbracelet/x/term v0.2.1 // indirect
@@ -31,7 +34,6 @@ require (
3134
github.com/rivo/uniseg v0.4.7 // indirect
3235
github.com/spf13/pflag v1.0.9 // indirect
3336
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
34-
golang.org/x/sync v0.15.0 // indirect
35-
golang.org/x/sys v0.33.0 // indirect
37+
golang.org/x/sys v0.36.0 // indirect
3638
golang.org/x/text v0.23.0 // indirect
3739
)

go.sum

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,46 @@
1+
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
2+
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
13
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
24
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
35
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
46
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
7+
github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
8+
github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
59
github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
610
github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
711
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 h1:JFgG/xnwFfbezlUnFMJy0nusZvytYysV4SCS2cYbvws=
812
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7/go.mod h1:ISC1gtLcVilLOf23wvTfoQuYbW2q0JevFxPfUzZ9Ybw=
9-
github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU=
10-
github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc=
13+
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
14+
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
1115
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
1216
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
1317
github.com/charmbracelet/huh v0.8.0 h1:Xz/Pm2h64cXQZn/Jvele4J3r7DDiqFCNIVteYukxDvY=
1418
github.com/charmbracelet/huh v0.8.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4=
19+
github.com/charmbracelet/huh/spinner v0.0.0-20251215014908-6f7d32faaff3 h1:KUeWGoKnmyrLaDIa0smE6pK5eFMZWNIxPGweQR12iLg=
20+
github.com/charmbracelet/huh/spinner v0.0.0-20251215014908-6f7d32faaff3/go.mod h1:OMqKat/mm9a/qOnpuNOPyYO9bPzRNnmzLnRZT5KYltg=
1521
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
1622
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
17-
github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0=
18-
github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
23+
github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
24+
github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
1925
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
2026
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
27+
github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U=
28+
github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ=
29+
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA=
30+
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=
31+
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
32+
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
2133
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4=
2234
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
2335
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
2436
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
37+
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
38+
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
39+
github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI=
40+
github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4=
2541
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
42+
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
43+
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
2644
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
2745
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
2846
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
@@ -56,12 +74,12 @@ github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
5674
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
5775
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
5876
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
59-
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
60-
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
77+
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
78+
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
6179
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
6280
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
63-
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
64-
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
81+
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
82+
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
6583
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
6684
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
6785
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

internal/goforge/forge.go

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,33 @@ import (
66

77
"github.com/andrearcaina/goforge/internal/spec"
88
"github.com/andrearcaina/goforge/internal/ui"
9+
"github.com/charmbracelet/huh/spinner"
910
)
1011

11-
func Forge(initial *spec.Config) error {
12-
// try to run the ui
13-
cfg, err := ui.Run(initial)
14-
if err != nil {
15-
return errors.New("ui failed")
12+
func Forge(cfg *spec.Config) error {
13+
if cfg.Default {
14+
cfg.Form = spec.Form{
15+
Name: "example-server",
16+
ServerTypeFlag: "rest",
17+
}
18+
} else {
19+
if err := ui.Run(cfg); err != nil {
20+
return errors.New("failed to run ui form")
21+
}
1622
}
1723

18-
// try to generate the file
19-
if err := Generate(cfg); err != nil {
20-
return errors.New("generating file failed")
24+
var err error
25+
_ = spinner.New().
26+
Title("Forging your project...").
27+
Action(func() {
28+
err = Generate(cfg)
29+
}).Run()
30+
31+
if err != nil {
32+
return fmt.Errorf("failed to generate the file: %w", err)
2133
}
2234

23-
fmt.Println("File generated successfully")
35+
ui.OutputSuccess(cfg)
2436

25-
// success if none fails
2637
return nil
2738
}

internal/goforge/generator.go

Lines changed: 93 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,118 @@
11
package goforge
22

33
import (
4+
"errors"
5+
"fmt"
46
"os"
57
"path/filepath"
68
"text/template"
9+
"time"
710

811
"github.com/andrearcaina/goforge/internal/spec"
912
"github.com/andrearcaina/goforge/internal/templates"
1013
)
1114

1215
func Generate(cfg *spec.Config) error {
13-
// parse the template from the embedded FS
14-
tmpl, err := template.ParseFS(templates.FS, "hello.go.tmpl")
15-
if err != nil {
16+
if cfg.Form.ServerTypeFlag == "rest" {
17+
return generateRESTServer(cfg)
18+
}
19+
20+
return errors.New("unknown server type")
21+
}
22+
23+
func generateRESTServer(cfg *spec.Config) error {
24+
dirs := []string{
25+
"cmd/server",
26+
"internal/api",
27+
"internal/config",
28+
}
29+
30+
if cfg.Form.DatabaseFlag {
31+
dirs = append(dirs, "internal/db")
32+
}
33+
34+
for _, dir := range dirs {
35+
fullPath := filepath.Join(cfg.OutputPath, dir)
36+
if err := os.MkdirAll(fullPath, 0755); err != nil {
37+
return err
38+
}
39+
}
40+
41+
if err := generateFile("go.mod.tmpl", filepath.Join(cfg.OutputPath, "go.mod"), cfg); err != nil {
1642
return err
1743
}
1844

19-
var out *os.File
20-
if cfg.OutputPath != "" {
21-
// ensure the directory exists
22-
fullPath := filepath.Join(cfg.OutputPath, "hello.go")
45+
if err := generateFile("config.go.tmpl", filepath.Join(cfg.OutputPath, "internal/config/config.go"), cfg); err != nil {
46+
return err
47+
}
48+
if err := generateFile("main.go.tmpl", filepath.Join(cfg.OutputPath, "cmd/server/main.go"), cfg); err != nil {
49+
return err
50+
}
2351

24-
// if it doesn't exist, create it
25-
if err := os.MkdirAll(filepath.Dir(fullPath), 0755); err != nil {
52+
if err := generateFile("rest/server.go.tmpl", filepath.Join(cfg.OutputPath, "internal/api/server.go"), cfg); err != nil {
53+
return err
54+
}
55+
56+
if err := generateFile("rest/handler.go.tmpl", filepath.Join(cfg.OutputPath, "internal/api/handler.go"), cfg); err != nil {
57+
return err
58+
}
59+
60+
if err := generateFile("rest/service.go.tmpl", filepath.Join(cfg.OutputPath, "internal/api/service.go"), cfg); err != nil {
61+
return err
62+
}
63+
64+
if err := generateFile("rest/types.go.tmpl", filepath.Join(cfg.OutputPath, "internal/api/types.go"), cfg); err != nil {
65+
return err
66+
}
67+
68+
if cfg.Form.DatabaseFlag {
69+
if err := os.MkdirAll(filepath.Join(cfg.OutputPath, "internal/db/generated"), 0755); err != nil {
2670
return err
2771
}
2872

29-
// create the file
30-
f, err := os.Create(fullPath)
31-
if err != nil {
73+
if err := generateFile("db/db.go.tmpl", filepath.Join(cfg.OutputPath, "internal/db/db.go"), cfg); err != nil {
3274
return err
3375
}
3476

35-
defer f.Close()
36-
out = f
37-
} else {
38-
out = os.Stdout
77+
if err := generateFile("sqlc.yaml.tmpl", filepath.Join(cfg.OutputPath, "sqlc.yaml"), cfg); err != nil {
78+
return err
79+
}
80+
81+
timestamp := time.Now().Format("20060102150405")
82+
migrationFile := fmt.Sprintf("db/migrations/%s_init.sql", timestamp)
83+
if err := generateFile("db/migrations/init.sql.tmpl", filepath.Join(cfg.OutputPath, "internal", migrationFile), cfg); err != nil {
84+
return err
85+
}
86+
87+
if err := generateFile("db/queries/user.sql.tmpl", filepath.Join(cfg.OutputPath, "internal/db/queries/user.sql"), cfg); err != nil {
88+
return err
89+
}
90+
}
91+
92+
return nil
93+
}
94+
95+
func generateFile(tmplPath, outputPath string, data interface{}) error {
96+
tmpl, err := template.ParseFS(templates.FS, tmplPath)
97+
if err != nil {
98+
return err
99+
}
100+
101+
// ensure the directory exists
102+
if err := os.MkdirAll(filepath.Dir(outputPath), 0755); err != nil {
103+
return err
104+
}
105+
106+
// create the file
107+
out, err := os.Create(outputPath)
108+
if err != nil {
109+
return err
110+
}
111+
defer out.Close()
112+
113+
if err := tmpl.Execute(out, data); err != nil {
114+
return err
39115
}
40116

41-
// execute the template (write to file or stdout)
42-
return tmpl.Execute(out, cfg)
117+
return nil
43118
}

internal/spec/config.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,22 @@
11
package spec
22

3+
// ServerTypeFlag is an enum and can be one of the following values
4+
type ServerTypeFlag string
5+
6+
const (
7+
REST ServerTypeFlag = "rest"
8+
GRPC ServerTypeFlag = "grpc"
9+
GraphQL ServerTypeFlag = "graphql"
10+
)
11+
312
type Config struct {
413
OutputPath string
14+
Default bool
515
Form Form
616
}
717

818
type Form struct {
9-
SomeFlag string
10-
AnotherFlag string
19+
Name string
20+
ServerTypeFlag ServerTypeFlag
21+
DatabaseFlag bool
1122
}

internal/templates/api/.gitkeep

Whitespace-only changes.

0 commit comments

Comments
 (0)