Skip to content

Commit 849557e

Browse files
authored
FEAT: Add backend template (#2)
* feat: embed backend * fix: versioned router * fix: rearrange packages * fix: make writing concurrent
1 parent 476d447 commit 849557e

27 files changed

Lines changed: 569 additions & 35 deletions

File tree

cli/command/cmds/build.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ func (c *buildCommand) invoke() *cobra.Command {
8585
if err != nil {
8686
return &errorhandling.CommandError{
8787
Err: fmt.Errorf("failed to load template resources for category %q: %w", category, err),
88-
ExitCode: errorhandling.ExitGenericError,
88+
ExitCode: errorhandling.ExitTemplateError,
8989
HelpText: "Failed to load template resources. Please check the category name and try again.",
9090
}
9191
}
@@ -107,10 +107,11 @@ func (c *buildCommand) invoke() *cobra.Command {
107107
if err := templater.Render(ctx, c.outputPath); err != nil {
108108
return &errorhandling.CommandError{
109109
Err: fmt.Errorf("failed to render template %q: %w", category, err),
110-
ExitCode: errorhandling.ExitGenericError,
110+
ExitCode: errorhandling.ExitTemplateError,
111111
HelpText: "Please check the template and try again.",
112112
}
113113
}
114+
114115
logger.WithFields(logrus.Fields{
115116
"category": category,
116117
"output": c.outputPath,

cli/command/cmds/build_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ func TestBuildCommand(t *testing.T) {
8989
return newTestBuildCommand(errorLoader, nil)
9090
},
9191
wantErr: true,
92-
wantExit: errorhandling.ExitGenericError,
92+
wantExit: errorhandling.ExitTemplateError,
9393
},
9494
{
9595
name: "renderer failure returns generic error",
@@ -100,7 +100,7 @@ func TestBuildCommand(t *testing.T) {
100100
return newTestBuildCommand(successLoader, errors.New("render failure"))
101101
},
102102
wantErr: true,
103-
wantExit: errorhandling.ExitGenericError,
103+
wantExit: errorhandling.ExitTemplateError,
104104
},
105105
{
106106
name: "successful render on empty dir prints success message",

cli/entrypoint/entrypoint.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ func Run(metadata []byte) {
5151
})
5252
if err != nil {
5353
printError("Error creating command: %v", err)
54-
os.Exit(errorhandling.ExitGenericError.Int())
54+
os.Exit(errorhandling.ExitRuntimeError.Int())
5555
}
5656

5757
var exitCode int
@@ -71,7 +71,7 @@ func handlerExecError(err error) int {
7171
return cmdErr.ExitCode.Int()
7272
}
7373
fmt.Printf("An unexpected error occurred: %v\n", err)
74-
return errorhandling.ExitGenericError.Int()
74+
return errorhandling.ExitRuntimeError.Int()
7575
}
7676

7777
func printError(format string, args ...any) {

cli/internal/errorhandling/errors.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ func (e ExitCode) Int() int {
1515

1616
const (
1717
ExitSuccess ExitCode = 0
18-
ExitGenericError ExitCode = 1
18+
ExitRuntimeError ExitCode = 1
1919
ExitPanicError ExitCode = 2
2020
ExitTemplateError ExitCode = 3
2121
ExitInputError ExitCode = 4

cli/internal/errorhandling/errors_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import (
1111
func TestCommandError(t *testing.T) {
1212
mockErr := CommandError{
1313
Err: errors.New("mock error"),
14-
ExitCode: ExitGenericError,
14+
ExitCode: ExitRuntimeError,
1515
HelpText: "This is some help text.",
1616
}
1717

cli/internal/templating/embed.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import (
1111
"github.com/jgfranco17/hackstack/cli/internal/logging"
1212
)
1313

14-
//go:embed resources
14+
//go:embed all:resources
1515
var embeddedResources embed.FS
1616

1717
// CLIProject holds the variables required to render the CLI project templates.
@@ -41,10 +41,22 @@ func (d *CLIProject) Validate() error {
4141
return nil
4242
}
4343

44+
// Load retrieves the embedded template files for the specified category and returns
45+
// them as an fs.FS instance for use in rendering. The category must be one of the
46+
// currently-supported categories.
4447
func Load(ctx context.Context, category string) (fs.FS, error) {
4548
logger := logging.FromContext(ctx).WithField("module", "templating")
4649

50+
allowedCategories := map[string]bool{
51+
"backend": true,
52+
"cli": true,
53+
}
54+
4755
category = strings.ToLower(category)
56+
if _, ok := allowedCategories[category]; !ok {
57+
return nil, fmt.Errorf("invalid templating category %q", category)
58+
}
59+
4860
subDirPath := filepath.Join("resources", category)
4961
sub, err := fs.Sub(embeddedResources, subDirPath)
5062
if err != nil {

cli/internal/templating/embed_test.go

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,16 +67,43 @@ func TestCLIProject_Validate(t *testing.T) {
6767
}
6868

6969
func TestLoad_ValidCategory(t *testing.T) {
70+
testCases := []struct {
71+
name string
72+
category string
73+
invalid bool
74+
}{
75+
{name: "backend category", category: "backend"},
76+
{name: "cli category", category: "cli"},
77+
{name: "category case insensitivity", category: "CLI"},
78+
{name: "invalid category", category: "unknown", invalid: true},
79+
}
80+
for _, tc := range testCases {
81+
t.Run(tc.name, func(t *testing.T) {
82+
ctx := testContext(t)
83+
sub, err := Load(ctx, tc.category)
84+
85+
if tc.invalid {
86+
require.Error(t, err)
87+
assert.ErrorContains(t, err, "invalid templating category")
88+
} else {
89+
require.NoError(t, err)
90+
require.NotNil(t, sub)
91+
92+
entries, err := fs.ReadDir(sub, ".")
93+
require.NoError(t, err)
94+
assert.NotEmpty(t, entries)
95+
}
96+
})
97+
}
98+
}
99+
100+
func TestLoad_InvalidCategory(t *testing.T) {
70101
ctx := testContext(t)
71-
sub, err := Load(ctx, "cli")
72102

73-
require.NoError(t, err)
74-
require.NotNil(t, sub)
103+
_, err := Load(ctx, "unknown")
75104

76-
// Confirm the returned FS is non-empty.
77-
entries, err := fs.ReadDir(sub, ".")
78-
require.NoError(t, err)
79-
assert.NotEmpty(t, entries, "loaded FS should contain template files")
105+
require.Error(t, err)
106+
assert.Contains(t, err.Error(), `invalid templating category "unknown"`)
80107
}
81108

82109
func TestLoad_CategoryIsCaseInsensitive(t *testing.T) {

cli/internal/templating/engine.go

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ import (
77
"os"
88
"path/filepath"
99
"strings"
10+
"sync/atomic"
1011
"text/template"
1112

12-
"github.com/jgfranco17/hackstack/cli/internal/errorhandling"
13+
"golang.org/x/sync/errgroup"
14+
1315
"github.com/jgfranco17/hackstack/cli/internal/fileutils"
1416
"github.com/jgfranco17/hackstack/cli/internal/logging"
1517
)
@@ -29,7 +31,9 @@ func NewEngine(files fs.FS, data CLIProject) *Engine {
2931
func (e *Engine) Render(ctx context.Context, outputPath string) error {
3032
logger := logging.FromContext(ctx).WithField("module", "templating")
3133

32-
count := 0
34+
var count atomic.Int64
35+
g, ctx := errgroup.WithContext(ctx)
36+
3337
walker := func(path string, d fs.DirEntry, err error) error {
3438
if err != nil {
3539
return fmt.Errorf("walk error at %q: %w", path, err)
@@ -40,30 +44,38 @@ func (e *Engine) Render(ctx context.Context, outputPath string) error {
4044

4145
destPath := filepath.Join(outputPath, filepath.FromSlash(path))
4246

47+
var work func() error
4348
switch {
4449
case strings.HasSuffix(path, ".j2"):
4550
destPath = strings.TrimSuffix(destPath, ".j2")
46-
logger.WithField("file", path).Trace("Rendering from template")
47-
count++
48-
return renderTemplate(e.Files, path, destPath, e.Data)
51+
work = func() error {
52+
logger.WithField("file", path).Trace("Rendering from template")
53+
return renderTemplate(e.Files, path, destPath, e.Data)
54+
}
4955
case strings.HasSuffix(path, ".copy"):
5056
destPath = strings.TrimSuffix(destPath, ".copy")
51-
logger.WithField("file", path).Trace("Copying file")
52-
count++
53-
return fileutils.CopyFile(e.Files, path, destPath)
57+
work = func() error {
58+
logger.WithField("file", path).Trace("Copying file")
59+
return fileutils.CopyFile(e.Files, path, destPath)
60+
}
5461
default:
55-
return fmt.Errorf("unrecognized resource extension for %q: expected .j2 or .copy", path)
62+
return fmt.Errorf("unrecognized resource extension for %q", path)
5663
}
64+
65+
count.Add(1)
66+
g.Go(work)
67+
return nil
5768
}
5869

5970
if err := fs.WalkDir(e.Files, ".", walker); err != nil {
60-
return &errorhandling.CommandError{
61-
Err: fmt.Errorf("failed to render templates: %w", err),
62-
ExitCode: errorhandling.ExitTemplateError,
63-
HelpText: "Check template resources and verify the contents.",
64-
}
71+
return fmt.Errorf("failed to render templates: %w", err)
6572
}
66-
logger.WithField("fileCount", count).Debug("Completed render")
73+
74+
if err := g.Wait(); err != nil {
75+
return fmt.Errorf("failed to render templates: %w", err)
76+
}
77+
78+
logger.WithField("fileCount", count.Load()).Debug("Completed render")
6779
return nil
6880
}
6981

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
name: "Setup Go Workspace"
3+
description: "Configure Go and prepare the workspace"
4+
5+
runs:
6+
using: composite
7+
steps:
8+
- name: Set up Golang
9+
uses: actions/setup-go@v5
10+
with:
11+
cache: false
12+
13+
- name: Install Just
14+
uses: extractions/setup-just@v2
15+
16+
- name: Install Go modules
17+
shell: bash
18+
run: |
19+
go mod tidy
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
name: Compliance Checks
3+
4+
on:
5+
push:
6+
branches:
7+
- main
8+
pull_request:
9+
branches:
10+
- main
11+
12+
jobs:
13+
lint:
14+
name: Run linters
15+
runs-on: ubuntu-latest
16+
17+
steps:
18+
- name: Checkout repository
19+
uses: actions/checkout@v5
20+
21+
- name: Setup workspace
22+
uses: ./.github/actions/setup-workspace
23+
24+
- name: Install linters
25+
run: |
26+
pip install --upgrade pip
27+
pip install pre-commit==3.5.0
28+
29+
- name: Install dependencies
30+
run: |
31+
just tidy
32+
go install golang.org/x/tools/cmd/goimports@latest
33+
go install github.com/fzipp/gocyclo/cmd/gocyclo@latest
34+
35+
- name: Lint
36+
run: pre-commit run --all-files

0 commit comments

Comments
 (0)