rapidkit 0.25.5 → 0.25.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1656 @@
1
+ import {b as b$1}from'./chunk-Q7ULIFQA.js';import {promises}from'fs';import s from'path';import n from'chalk';import f from'ora';import {execa}from'execa';function l(e){return e.split(/[-_\s]+/).map(o=>o.charAt(0).toUpperCase()+o.slice(1)).join("")}async function w(e,o){await promises.mkdir(s.dirname(e),{recursive:true}),await promises.writeFile(e,o,"utf8");}function v(e){return `package main
2
+
3
+ import (
4
+ "context"
5
+ "errors"
6
+ "fmt"
7
+ "log/slog"
8
+ "net/http"
9
+ "os"
10
+ "os/signal"
11
+ "syscall"
12
+ "time"
13
+
14
+ _ "${e.module_path}/docs"
15
+ "${e.module_path}/internal/config"
16
+ "${e.module_path}/internal/server"
17
+ )
18
+
19
+ // Build-time variables \u2014 injected via -ldflags.
20
+ var (
21
+ version = "dev"
22
+ commit = "none"
23
+ date = "unknown"
24
+ )
25
+
26
+ func main() {
27
+ cfg := config.Load()
28
+
29
+ log := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
30
+ Level: config.ParseLogLevel(cfg.LogLevel),
31
+ }))
32
+ slog.SetDefault(log)
33
+
34
+ r := server.NewRouter(cfg)
35
+
36
+ srv := &http.Server{
37
+ Addr: ":" + cfg.Port,
38
+ Handler: r,
39
+ ReadTimeout: 5 * time.Second,
40
+ WriteTimeout: 10 * time.Second,
41
+ IdleTimeout: 30 * time.Second,
42
+ }
43
+
44
+ go func() {
45
+ slog.Info("starting", "port", cfg.Port, "version", version, "commit", commit, "date", date, "env", cfg.Env)
46
+ fmt.Printf("\\n\u{1F680} Server \u2192 http://127.0.0.1:%s\\n", cfg.Port)
47
+ fmt.Printf("\u{1F4D6} Docs \u2192 http://127.0.0.1:%s/docs\\n\\n", cfg.Port)
48
+ if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
49
+ slog.Error("server error", "err", err)
50
+ os.Exit(1)
51
+ }
52
+ }()
53
+
54
+ quit := make(chan os.Signal, 1)
55
+ signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
56
+ <-quit
57
+
58
+ slog.Info("shutting down\u2026")
59
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
60
+ defer cancel()
61
+
62
+ if err := srv.Shutdown(ctx); err != nil {
63
+ slog.Error("forced shutdown", "err", err)
64
+ os.Exit(1)
65
+ }
66
+ slog.Info("server stopped")
67
+ }
68
+ `}function R(e){return `module ${e.module_path}
69
+
70
+ go ${e.go_version}
71
+
72
+ require (
73
+ github.com/gin-gonic/gin v1.10.0
74
+ github.com/swaggo/gin-swagger v1.6.0
75
+ github.com/swaggo/swag v1.16.3
76
+ )
77
+
78
+ require (
79
+ github.com/KyleBanks/depth v1.2.1 // indirect
80
+ github.com/bytedance/sonic v1.11.6 // indirect
81
+ github.com/bytedance/sonic/loader v0.1.1 // indirect
82
+ github.com/cloudwego/base64x v0.1.4 // indirect
83
+ github.com/cloudwego/iasm v0.2.0 // indirect
84
+ github.com/gabriel-vasile/mimetype v1.4.3 // indirect
85
+ github.com/ghodss/yaml v1.0.0 // indirect
86
+ github.com/gin-contrib/sse v0.1.0 // indirect
87
+ github.com/go-openapi/jsonpointer v0.21.0 // indirect
88
+ github.com/go-openapi/jsonreference v0.21.0 // indirect
89
+ github.com/go-openapi/spec v0.21.0 // indirect
90
+ github.com/go-openapi/swag v0.23.0 // indirect
91
+ github.com/go-playground/locales v0.14.1 // indirect
92
+ github.com/go-playground/universal-translator v0.18.1 // indirect
93
+ github.com/go-playground/validator/v10 v10.20.0 // indirect
94
+ github.com/goccy/go-json v0.10.2 // indirect
95
+ github.com/josharian/intern v1.0.0 // indirect
96
+ github.com/json-iterator/go v1.1.12 // indirect
97
+ github.com/klauspost/cpuid/v2 v2.2.7 // indirect
98
+ github.com/leodido/go-urn v1.4.0 // indirect
99
+ github.com/mailru/easyjson v0.7.7 // indirect
100
+ github.com/mattn/go-isatty v0.0.20 // indirect
101
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
102
+ github.com/modern-go/reflect2 v1.0.2 // indirect
103
+ github.com/pelletier/go-toml/v2 v2.2.2 // indirect
104
+ github.com/swaggo/files v1.0.1 // indirect
105
+ github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
106
+ github.com/ugorji/go/codec v1.2.12 // indirect
107
+ golang.org/x/arch v0.8.0 // indirect
108
+ golang.org/x/crypto v0.23.0 // indirect
109
+ golang.org/x/net v0.25.0 // indirect
110
+ golang.org/x/sys v0.20.0 // indirect
111
+ golang.org/x/text v0.15.0 // indirect
112
+ golang.org/x/tools v0.21.0 // indirect
113
+ google.golang.org/protobuf v1.34.1 // indirect
114
+ gopkg.in/yaml.v2 v2.4.0 // indirect
115
+ gopkg.in/yaml.v3 v3.0.1 // indirect
116
+ )
117
+ `}function S(e){return `package config
118
+
119
+ import (
120
+ "log/slog"
121
+ "os"
122
+ "strings"
123
+ )
124
+
125
+ // Config holds application configuration loaded from environment variables.
126
+ type Config struct {
127
+ Port string
128
+ Env string
129
+ GinMode string
130
+ LogLevel string
131
+ }
132
+
133
+ // Load reads configuration from environment variables with sensible defaults.
134
+ func Load() *Config {
135
+ env := getEnv("APP_ENV", "development")
136
+ return &Config{
137
+ Port: getEnv("PORT", "${e.port}"),
138
+ Env: env,
139
+ GinMode: getEnv("GIN_MODE", "debug"),
140
+ LogLevel: getEnv("LOG_LEVEL", defaultLogLevel(env)),
141
+ }
142
+ }
143
+
144
+ // ParseLogLevel maps a level string to the corresponding slog.Level.
145
+ // Falls back to Info for unrecognised values.
146
+ func ParseLogLevel(s string) slog.Level {
147
+ switch strings.ToLower(s) {
148
+ case "debug":
149
+ return slog.LevelDebug
150
+ case "warn", "warning":
151
+ return slog.LevelWarn
152
+ case "error":
153
+ return slog.LevelError
154
+ default:
155
+ return slog.LevelInfo
156
+ }
157
+ }
158
+
159
+ func defaultLogLevel(env string) string {
160
+ if env == "development" {
161
+ return "debug"
162
+ }
163
+ return "info"
164
+ }
165
+
166
+ func getEnv(key, fallback string) string {
167
+ if v, ok := os.LookupEnv(key); ok && v != "" {
168
+ return v
169
+ }
170
+ return fallback
171
+ }
172
+ `}function b(e){return `package server
173
+
174
+ import (
175
+ "net/http"
176
+
177
+ "github.com/gin-gonic/gin"
178
+ ginSwagger "github.com/swaggo/gin-swagger"
179
+ swaggerFiles "github.com/swaggo/files"
180
+
181
+ "${e.module_path}/internal/apierr"
182
+ "${e.module_path}/internal/config"
183
+ "${e.module_path}/internal/handlers"
184
+ "${e.module_path}/internal/middleware"
185
+ )
186
+
187
+ // NewRouter assembles the Gin engine with all middleware and routes.
188
+ // Call this from main \u2014 or from tests via server.NewRouter(cfg).
189
+ func NewRouter(cfg *config.Config) *gin.Engine {
190
+ if cfg.GinMode == "release" {
191
+ gin.SetMode(gin.ReleaseMode)
192
+ }
193
+
194
+ r := gin.New()
195
+ r.Use(gin.Recovery())
196
+ r.Use(middleware.CORS())
197
+ r.Use(middleware.RequestID())
198
+ r.Use(middleware.RateLimit())
199
+ r.Use(middleware.Logger())
200
+
201
+ // Swagger UI \u2014 /docs redirects to /docs/index.html
202
+ r.GET("/docs", func(c *gin.Context) { c.Redirect(http.StatusFound, "/docs/index.html") })
203
+ r.GET("/docs/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
204
+
205
+ v1 := r.Group("/api/v1")
206
+ {
207
+ v1.GET("/health/live", handlers.Liveness)
208
+ v1.GET("/health/ready", handlers.Readiness)
209
+ v1.GET("/echo/:name", handlers.EchoParams)
210
+ }
211
+
212
+ // Return JSON for unknown routes/methods instead of Gin's default text responses.
213
+ // HandleMethodNotAllowed must be true so NoMethod handler fires for 405 cases.
214
+ r.HandleMethodNotAllowed = true
215
+ r.NoRoute(func(c *gin.Context) {
216
+ apierr.NotFound(c, "route not found")
217
+ })
218
+ r.NoMethod(func(c *gin.Context) {
219
+ apierr.MethodNotAllowed(c)
220
+ })
221
+
222
+ return r
223
+ }
224
+ `}function T(){return `package handlers
225
+
226
+ import (
227
+ "net/http"
228
+ "time"
229
+
230
+ "github.com/gin-gonic/gin"
231
+ )
232
+
233
+ // Liveness signals the process is alive (Kubernetes livenessProbe).
234
+ //
235
+ // @Summary Liveness probe
236
+ // @Description Returns 200 when the process is alive.
237
+ // @Tags health
238
+ // @Produce json
239
+ // @Success 200 {object} map[string]string
240
+ // @Router /api/v1/health/live [get]
241
+ func Liveness(c *gin.Context) {
242
+ c.JSON(http.StatusOK, gin.H{
243
+ "status": "ok",
244
+ "time": time.Now().UTC().Format(time.RFC3339),
245
+ })
246
+ }
247
+
248
+ // Readiness signals the service can accept traffic (Kubernetes readinessProbe).
249
+ // Extend this function to check database connectivity, caches, etc.
250
+ //
251
+ // @Summary Readiness probe
252
+ // @Description Returns 200 when the service is ready to accept traffic.
253
+ // @Tags health
254
+ // @Produce json
255
+ // @Success 200 {object} map[string]string
256
+ // @Router /api/v1/health/ready [get]
257
+ func Readiness(c *gin.Context) {
258
+ c.JSON(http.StatusOK, gin.H{
259
+ "status": "ready",
260
+ "time": time.Now().UTC().Format(time.RFC3339),
261
+ })
262
+ }
263
+ `}function _(e){return `package handlers_test
264
+
265
+ import (
266
+ "encoding/json"
267
+ "net/http"
268
+ "net/http/httptest"
269
+ "testing"
270
+
271
+ "github.com/gin-gonic/gin"
272
+
273
+ "${e.module_path}/internal/config"
274
+ "${e.module_path}/internal/server"
275
+ )
276
+
277
+ func init() { gin.SetMode(gin.TestMode) }
278
+
279
+ func newRouter() *gin.Engine { return server.NewRouter(config.Load()) }
280
+
281
+ func TestLiveness(t *testing.T) {
282
+ w := httptest.NewRecorder()
283
+ req, _ := http.NewRequest(http.MethodGet, "/api/v1/health/live", nil)
284
+ newRouter().ServeHTTP(w, req)
285
+
286
+ if w.Code != http.StatusOK {
287
+ t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
288
+ }
289
+
290
+ var body map[string]any
291
+ if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
292
+ t.Fatalf("invalid JSON: %v", err)
293
+ }
294
+ if body["status"] != "ok" {
295
+ t.Fatalf("expected ok, got %v", body["status"])
296
+ }
297
+ }
298
+
299
+ func TestReadiness(t *testing.T) {
300
+ w := httptest.NewRecorder()
301
+ req, _ := http.NewRequest(http.MethodGet, "/api/v1/health/ready", nil)
302
+ newRouter().ServeHTTP(w, req)
303
+
304
+ if w.Code != http.StatusOK {
305
+ t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
306
+ }
307
+ }
308
+ `}function O(){return `# \u2500\u2500 Build stage \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
309
+ FROM golang:1.24-alpine AS builder
310
+
311
+ # Build-time version injection
312
+ ARG VERSION=dev
313
+ ARG COMMIT=none
314
+ ARG DATE=unknown
315
+
316
+ WORKDIR /app
317
+ COPY go.mod go.sum ./
318
+ RUN go mod download
319
+
320
+ COPY . .
321
+ RUN CGO_ENABLED=0 GOOS=linux go build \\
322
+ -ldflags="-s -w -X main.version=$\${VERSION} -X main.commit=$\${COMMIT} -X main.date=$\${DATE}" \\
323
+ -o server ./cmd/server
324
+
325
+ # \u2500\u2500 Runtime stage \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
326
+ # alpine includes busybox wget required for the HEALTHCHECK below.
327
+ FROM alpine:3.21
328
+
329
+ RUN addgroup -S app && adduser -S -G app app
330
+ COPY --from=builder /app/server /server
331
+ USER app
332
+
333
+ EXPOSE 8080
334
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \\
335
+ CMD wget -qO- http://localhost:8080/api/v1/health/live || exit 1
336
+ ENTRYPOINT ["/server"]
337
+ `}function N(e){return `version: "3.9"
338
+
339
+ services:
340
+ api:
341
+ build: .
342
+ container_name: ${e.project_name}
343
+ ports:
344
+ - "${e.port}:${e.port}"
345
+ environment:
346
+ PORT: "${e.port}"
347
+ APP_ENV: development
348
+ GIN_MODE: debug
349
+ LOG_LEVEL: info
350
+ CORS_ALLOW_ORIGINS: "*"
351
+ RATE_LIMIT_RPS: "100"
352
+ restart: unless-stopped
353
+ `}function E(e){return `.PHONY: dev run build test cover lint fmt tidy docs docker-up docker-down
354
+
355
+ # Build-time metadata
356
+ VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
357
+ COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo "none")
358
+ DATE ?= $(shell date -u +"%Y-%m-%dT%H:%M:%SZ")
359
+ LDFLAGS = -ldflags "-s -w -X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.date=$(DATE)"
360
+ # Go tool binaries are installed to GOPATH/bin; include it so \`air\` and \`swag\` are found.
361
+ GOBIN ?= $(shell go env GOPATH)/bin
362
+
363
+ # Hot reload \u2014 installs air on first use
364
+ dev:
365
+ @test -x "$(GOBIN)/air" || go install github.com/air-verse/air@latest
366
+ GIN_MODE=debug $(GOBIN)/air
367
+
368
+ run:
369
+ GIN_MODE=debug go run $(LDFLAGS) ./cmd/server
370
+
371
+ build:
372
+ go build $(LDFLAGS) -o bin/${e.project_name} ./cmd/server
373
+
374
+ test:
375
+ GIN_MODE=test go test ./... -v -race
376
+
377
+ cover:
378
+ GIN_MODE=test go test ./... -race -coverprofile=coverage.out
379
+ go tool cover -html=coverage.out -o coverage.html
380
+ @echo "Coverage report: coverage.html"
381
+
382
+ # Generate Swagger docs \u2014 installs swag on first use
383
+ docs:
384
+ @test -x "$(GOBIN)/swag" || go install github.com/swaggo/swag/cmd/swag@latest
385
+ $(GOBIN)/swag init -g main.go -d cmd/server,internal/handlers,internal/apierr -o docs --parseDependency
386
+
387
+ lint:
388
+ @command -v golangci-lint >/dev/null 2>&1 || (echo "golangci-lint not found. Install: https://golangci-lint.run/usage/install/" && exit 1)
389
+ golangci-lint run ./...
390
+
391
+ fmt:
392
+ gofmt -w .
393
+
394
+ tidy:
395
+ go mod tidy
396
+
397
+ docker-up:
398
+ go mod tidy
399
+ docker compose up --build \\
400
+ --build-arg VERSION=$(VERSION) \\
401
+ --build-arg COMMIT=$(COMMIT) \\
402
+ --build-arg DATE=$(DATE) \\
403
+ -d
404
+
405
+ docker-down:
406
+ docker compose down
407
+ `}function G(e){return `# Application
408
+ PORT=${e.port}
409
+ APP_ENV=development
410
+ GIN_MODE=debug
411
+ LOG_LEVEL=debug
412
+
413
+ # CORS \u2014 comma-separated list of allowed origins, or * to allow all
414
+ CORS_ALLOW_ORIGINS=*
415
+
416
+ # Rate limiting \u2014 max requests per IP per second
417
+ RATE_LIMIT_RPS=100
418
+ `}function q(){return `# Binaries
419
+ bin/
420
+ *.exe
421
+ *.exe~
422
+ *.dll
423
+ *.so
424
+ *.dylib
425
+
426
+ # Test binary
427
+ *.test
428
+
429
+ # Output of go coverage tool
430
+ *.out
431
+ coverage.html
432
+
433
+ # Go workspace
434
+ go.work
435
+ go.work.sum
436
+
437
+ # Environment
438
+ .env
439
+ .env.local
440
+
441
+ # Hot reload (air)
442
+ tmp/
443
+
444
+ # Swagger \u2014 generated files (committed stub docs/doc.go; run \`make docs\` to regenerate)
445
+ docs/swagger.json
446
+ docs/swagger.yaml
447
+ docs/docs.go
448
+
449
+ # Editor
450
+ .idea/
451
+ .vscode/
452
+ *.swp
453
+ *.swo
454
+
455
+ # OS
456
+ .DS_Store
457
+ Thumbs.db
458
+ `}function C(e){return `name: CI
459
+
460
+ on:
461
+ push:
462
+ branches: [main, develop]
463
+ pull_request:
464
+ branches: [main]
465
+
466
+ jobs:
467
+ test:
468
+ name: Test
469
+ runs-on: ubuntu-latest
470
+
471
+ steps:
472
+ - uses: actions/checkout@v4
473
+
474
+ - name: Set up Go
475
+ uses: actions/setup-go@v5
476
+ with:
477
+ go-version: "${e.go_version}"
478
+ cache: true
479
+
480
+ - name: Tidy
481
+ run: go mod tidy
482
+
483
+ - name: Build
484
+ run: go build ./...
485
+
486
+ - name: Test
487
+ run: GIN_MODE=test go test ./... -race -coverprofile=coverage.out
488
+
489
+ - name: Upload coverage
490
+ uses: actions/upload-artifact@v4
491
+ with:
492
+ name: coverage
493
+ path: coverage.out
494
+
495
+ lint:
496
+ name: Lint
497
+ runs-on: ubuntu-latest
498
+
499
+ steps:
500
+ - uses: actions/checkout@v4
501
+
502
+ - name: Set up Go
503
+ uses: actions/setup-go@v5
504
+ with:
505
+ go-version: "${e.go_version}"
506
+ cache: true
507
+
508
+ - name: golangci-lint
509
+ uses: golangci/golangci-lint-action@v6
510
+ with:
511
+ version: latest
512
+ `}function y(e){return `# ${l(e.project_name)}
513
+
514
+ > ${e.description}
515
+
516
+ Built with [Go](https://go.dev/) + [Gin](https://gin-gonic.com/) \xB7 Scaffolded by [RapidKit](https://getrapidkit.com)
517
+
518
+ ## Quick start
519
+
520
+ \`\`\`bash
521
+ # Run locally (hot reload)
522
+ make dev
523
+
524
+ # Run tests
525
+ make test
526
+
527
+ # Build binary
528
+ make build
529
+
530
+ # Generate / refresh Swagger docs
531
+ make docs
532
+
533
+ # Docker
534
+ make docker-up
535
+ \`\`\`
536
+
537
+ ## Swagger / OpenAPI
538
+
539
+ After running \`make docs\`, the interactive UI is available at:
540
+
541
+ \`\`\`
542
+ http://localhost:${e.port}/docs
543
+ \`\`\`
544
+
545
+ The raw OpenAPI spec is served at \`/docs/doc.json\`.
546
+
547
+ ## Endpoints
548
+
549
+ | Method | Path | Description |
550
+ |--------|------|--------------|
551
+ | GET | /api/v1/health/live | Kubernetes livenessProbe |
552
+ | GET | /api/v1/health/ready | Kubernetes readinessProbe |
553
+ | GET | /api/v1/echo/:name | Example handler \u2014 remove in production |
554
+ | GET | /docs/* | Swagger UI (OpenAPI docs) |
555
+
556
+ ## Configuration
557
+
558
+ All configuration is done through environment variables (see \`.env.example\`):
559
+
560
+ | Variable | Default | Description |
561
+ |----------|---------|-------------|
562
+ | \`PORT\` | \`${e.port}\` | HTTP listen port |
563
+ | \`APP_ENV\` | \`development\` | Application environment |
564
+ | \`GIN_MODE\` | \`debug\` | \`debug\` \\| \`release\` \\| \`test\` |
565
+ | \`LOG_LEVEL\` | \`debug\` / \`info\` | \`debug\` \\| \`info\` \\| \`warn\` \\| \`error\` |
566
+ | \`CORS_ALLOW_ORIGINS\` | \`*\` | Comma-separated list of allowed origins, or \`*\` |
567
+ | \`RATE_LIMIT_RPS\` | \`100\` | Max requests per IP per second |
568
+
569
+ ## Project structure
570
+
571
+ \`\`\`
572
+ ${e.project_name}/
573
+ \u251C\u2500\u2500 cmd/
574
+ \u2502 \u2514\u2500\u2500 server/
575
+ \u2502 \u2514\u2500\u2500 main.go # Graceful shutdown + version ldflags
576
+ \u251C\u2500\u2500 docs/ # Swagger generated files (\`make docs\`)
577
+ \u2502 \u2514\u2500\u2500 doc.go # Package-level OpenAPI annotations
578
+ \u251C\u2500\u2500 internal/
579
+ \u2502 \u251C\u2500\u2500 apierr/ # Consistent JSON error envelope
580
+ \u2502 \u2502 \u251C\u2500\u2500 apierr.go
581
+ \u2502 \u2502 \u2514\u2500\u2500 apierr_test.go
582
+ \u2502 \u251C\u2500\u2500 config/ # 12-factor configuration
583
+ \u2502 \u2502 \u251C\u2500\u2500 config.go
584
+ \u2502 \u2502 \u2514\u2500\u2500 config_test.go
585
+ \u2502 \u251C\u2500\u2500 handlers/ # HTTP handlers + tests
586
+ \u2502 \u2502 \u251C\u2500\u2500 health.go
587
+ \u2502 \u2502 \u251C\u2500\u2500 health_test.go
588
+ \u2502 \u2502 \u251C\u2500\u2500 example.go # EchoParams \u2014 replace with your own handlers
589
+ \u2502 \u2502 \u2514\u2500\u2500 example_test.go
590
+ \u2502 \u251C\u2500\u2500 middleware/
591
+ \u2502 \u2502 \u251C\u2500\u2500 requestid.go # X-Request-ID + structured logger
592
+ \u2502 \u2502 \u251C\u2500\u2500 requestid_test.go
593
+ \u2502 \u2502 \u251C\u2500\u2500 cors.go # CORS (CORS_ALLOW_ORIGINS)
594
+ \u2502 \u2502 \u251C\u2500\u2500 cors_test.go
595
+ \u2502 \u2502 \u251C\u2500\u2500 ratelimit.go # Per-IP limiter (RATE_LIMIT_RPS)
596
+ \u2502 \u2502 \u2514\u2500\u2500 ratelimit_test.go
597
+ \u2502 \u2514\u2500\u2500 server/
598
+ \u2502 \u251C\u2500\u2500 server.go
599
+ \u2502 \u2514\u2500\u2500 server_test.go
600
+ \u251C\u2500\u2500 .air.toml # Hot reload
601
+ \u251C\u2500\u2500 .github/workflows/ci.yml # CI: test + lint
602
+ \u251C\u2500\u2500 .golangci.yml
603
+ \u251C\u2500\u2500 Dockerfile # Multi-stage, alpine HEALTHCHECK
604
+ \u251C\u2500\u2500 docker-compose.yml
605
+ \u251C\u2500\u2500 Makefile
606
+ \u2514\u2500\u2500 README.md
607
+ \`\`\`
608
+
609
+ ## Available commands
610
+
611
+ | Command | Description |
612
+ |---------|-------------|
613
+ | \`make dev\` | Hot reload via [air](https://github.com/air-verse/air) |
614
+ | \`make run\` | Run without hot reload |
615
+ | \`make build\` | Binary with version ldflags |
616
+ | \`make test\` | Run tests with race detector |
617
+ | \`make cover\` | HTML coverage report |
618
+ | \`make docs\` | Re-generate Swagger JSON (needs \`swag\`) |
619
+ | \`make lint\` | golangci-lint |
620
+ | \`make fmt\` | gofmt |
621
+ | \`make tidy\` | go mod tidy |
622
+ | \`make docker-up\` | Build & run via Docker Compose |
623
+ | \`make docker-down\` | Stop |
624
+
625
+ ## License
626
+
627
+ ${e.app_version} \xB7 ${e.author}
628
+ `}function I(){return `package middleware
629
+
630
+ import (
631
+ "crypto/rand"
632
+ "encoding/hex"
633
+ "log/slog"
634
+ "time"
635
+
636
+ "github.com/gin-gonic/gin"
637
+ )
638
+
639
+ const headerRequestID = "X-Request-ID"
640
+
641
+ // RequestID injects a unique identifier into every request.
642
+ // If the caller sends an X-Request-ID header it is reused; otherwise a new one
643
+ // is generated and written back in the response.
644
+ func RequestID() gin.HandlerFunc {
645
+ return func(c *gin.Context) {
646
+ id := c.GetHeader(headerRequestID)
647
+ if id == "" {
648
+ id = newID()
649
+ }
650
+ c.Set(headerRequestID, id)
651
+ c.Header(headerRequestID, id)
652
+ c.Next()
653
+ }
654
+ }
655
+
656
+ // Logger emits a structured JSON log line after each request.
657
+ func Logger() gin.HandlerFunc {
658
+ return func(c *gin.Context) {
659
+ start := time.Now()
660
+ c.Next()
661
+ slog.Info("http",
662
+ "method", c.Request.Method,
663
+ "path", c.Request.URL.Path,
664
+ "status", c.Writer.Status(),
665
+ "bytes", c.Writer.Size(),
666
+ "latency_ms", time.Since(start).Milliseconds(),
667
+ "ip", c.ClientIP(),
668
+ "request_id", c.GetString(headerRequestID),
669
+ )
670
+ }
671
+ }
672
+
673
+ func newID() string {
674
+ b := make([]byte, 8)
675
+ if _, err := rand.Read(b); err != nil {
676
+ return "unknown"
677
+ }
678
+ return hex.EncodeToString(b)
679
+ }
680
+ `}function L(e){return `package middleware_test
681
+
682
+ import (
683
+ "net/http"
684
+ "net/http/httptest"
685
+ "testing"
686
+
687
+ "github.com/gin-gonic/gin"
688
+
689
+ "${e.module_path}/internal/middleware"
690
+ )
691
+
692
+ func init() { gin.SetMode(gin.TestMode) }
693
+
694
+ func newTestRouter() *gin.Engine {
695
+ r := gin.New()
696
+ r.Use(middleware.RequestID())
697
+ r.Use(middleware.Logger())
698
+ r.GET("/ping", func(c *gin.Context) {
699
+ c.String(http.StatusOK, "pong")
700
+ })
701
+ return r
702
+ }
703
+
704
+ func TestRequestID_IsGenerated(t *testing.T) {
705
+ w := httptest.NewRecorder()
706
+ req, _ := http.NewRequest(http.MethodGet, "/ping", nil)
707
+ newTestRouter().ServeHTTP(w, req)
708
+
709
+ id := w.Header().Get("X-Request-ID")
710
+ if id == "" {
711
+ t.Fatal("expected X-Request-ID header to be set")
712
+ }
713
+ if len(id) != 16 { // 8 random bytes \u2192 16 hex chars
714
+ t.Fatalf("unexpected request ID length %d, want 16", len(id))
715
+ }
716
+ }
717
+
718
+ func TestRequestID_IsReused(t *testing.T) {
719
+ w := httptest.NewRecorder()
720
+ req, _ := http.NewRequest(http.MethodGet, "/ping", nil)
721
+ req.Header.Set("X-Request-ID", "my-trace-id")
722
+ newTestRouter().ServeHTTP(w, req)
723
+
724
+ id := w.Header().Get("X-Request-ID")
725
+ if id != "my-trace-id" {
726
+ t.Fatalf("expected X-Request-ID to be reused, got %q", id)
727
+ }
728
+ }
729
+ `}function x(){return `// Package apierr provides a consistent JSON error envelope for all API responses.
730
+ //
731
+ // Every error response looks like:
732
+ //
733
+ // {"error": "user not found", "code": "NOT_FOUND", "request_id": "a1b2c3d4..."}
734
+ package apierr
735
+
736
+ import (
737
+ "net/http"
738
+
739
+ "github.com/gin-gonic/gin"
740
+ )
741
+
742
+ // Response is the standard error envelope returned by all API endpoints.
743
+ type Response struct {
744
+ Error string \`json:"error"\`
745
+ Code string \`json:"code"\`
746
+ RequestID string \`json:"request_id,omitempty"\`
747
+ }
748
+
749
+ func reply(c *gin.Context, status int, msg, code string) {
750
+ c.AbortWithStatusJSON(status, Response{
751
+ Error: msg,
752
+ Code: code,
753
+ RequestID: c.GetString("X-Request-ID"),
754
+ })
755
+ }
756
+
757
+ // BadRequest responds with 400 and code "BAD_REQUEST".
758
+ func BadRequest(c *gin.Context, msg string) {
759
+ reply(c, http.StatusBadRequest, msg, "BAD_REQUEST")
760
+ }
761
+
762
+ // NotFound responds with 404 and code "NOT_FOUND".
763
+ func NotFound(c *gin.Context, msg string) {
764
+ reply(c, http.StatusNotFound, msg, "NOT_FOUND")
765
+ }
766
+
767
+ // Unauthorized responds with 401 and code "UNAUTHORIZED".
768
+ func Unauthorized(c *gin.Context) {
769
+ reply(c, http.StatusUnauthorized, "authentication required", "UNAUTHORIZED")
770
+ }
771
+
772
+ // Forbidden responds with 403 and code "FORBIDDEN".
773
+ func Forbidden(c *gin.Context) {
774
+ reply(c, http.StatusForbidden, "access denied", "FORBIDDEN")
775
+ }
776
+
777
+ // MethodNotAllowed responds with 405 and code "METHOD_NOT_ALLOWED".
778
+ func MethodNotAllowed(c *gin.Context) {
779
+ reply(c, http.StatusMethodNotAllowed, "method not allowed", "METHOD_NOT_ALLOWED")
780
+ }
781
+
782
+ // InternalError responds with 500 and code "INTERNAL_ERROR".
783
+ // The original error is intentionally not exposed to the client.
784
+ func InternalError(c *gin.Context, _ error) {
785
+ reply(c, http.StatusInternalServerError, "an internal error occurred", "INTERNAL_ERROR")
786
+ }
787
+
788
+ // TooManyRequests responds with 429 and code "TOO_MANY_REQUESTS".
789
+ func TooManyRequests(c *gin.Context) {
790
+ reply(c, http.StatusTooManyRequests, "rate limit exceeded", "TOO_MANY_REQUESTS")
791
+ }
792
+ `}function k(e){return `package apierr_test
793
+
794
+ import (
795
+ "encoding/json"
796
+ "net/http"
797
+ "net/http/httptest"
798
+ "testing"
799
+
800
+ "github.com/gin-gonic/gin"
801
+
802
+ "${e.module_path}/internal/apierr"
803
+ )
804
+
805
+ func init() { gin.SetMode(gin.TestMode) }
806
+
807
+ func makeRouter(fn gin.HandlerFunc) *gin.Engine {
808
+ r := gin.New()
809
+ r.GET("/test", fn)
810
+ return r
811
+ }
812
+
813
+ func readJSON(t *testing.T, w *httptest.ResponseRecorder) apierr.Response {
814
+ t.Helper()
815
+ var out apierr.Response
816
+ if err := json.NewDecoder(w.Body).Decode(&out); err != nil {
817
+ t.Fatalf("invalid JSON: %v", err)
818
+ }
819
+ return out
820
+ }
821
+
822
+ func TestBadRequest(t *testing.T) {
823
+ w := httptest.NewRecorder()
824
+ req, _ := http.NewRequest(http.MethodGet, "/test", nil)
825
+ makeRouter(func(c *gin.Context) { apierr.BadRequest(c, "invalid email") }).ServeHTTP(w, req)
826
+
827
+ if w.Code != http.StatusBadRequest {
828
+ t.Fatalf("expected 400, got %d", w.Code)
829
+ }
830
+ body := readJSON(t, w)
831
+ if body.Code != "BAD_REQUEST" {
832
+ t.Fatalf("expected BAD_REQUEST, got %q", body.Code)
833
+ }
834
+ if body.Error != "invalid email" {
835
+ t.Fatalf("unexpected error message: %q", body.Error)
836
+ }
837
+ }
838
+
839
+ func TestNotFound(t *testing.T) {
840
+ w := httptest.NewRecorder()
841
+ req, _ := http.NewRequest(http.MethodGet, "/test", nil)
842
+ makeRouter(func(c *gin.Context) { apierr.NotFound(c, "user not found") }).ServeHTTP(w, req)
843
+
844
+ if w.Code != http.StatusNotFound {
845
+ t.Fatalf("expected 404, got %d", w.Code)
846
+ }
847
+ body := readJSON(t, w)
848
+ if body.Code != "NOT_FOUND" {
849
+ t.Fatalf("expected NOT_FOUND, got %q", body.Code)
850
+ }
851
+ }
852
+
853
+ func TestUnauthorized(t *testing.T) {
854
+ w := httptest.NewRecorder()
855
+ req, _ := http.NewRequest(http.MethodGet, "/test", nil)
856
+ makeRouter(func(c *gin.Context) { apierr.Unauthorized(c) }).ServeHTTP(w, req)
857
+
858
+ if w.Code != http.StatusUnauthorized {
859
+ t.Fatalf("expected 401, got %d", w.Code)
860
+ }
861
+ }
862
+
863
+ func TestForbidden(t *testing.T) {
864
+ w := httptest.NewRecorder()
865
+ req, _ := http.NewRequest(http.MethodGet, "/test", nil)
866
+ makeRouter(func(c *gin.Context) { apierr.Forbidden(c) }).ServeHTTP(w, req)
867
+
868
+ if w.Code != http.StatusForbidden {
869
+ t.Fatalf("expected 403, got %d", w.Code)
870
+ }
871
+ body := readJSON(t, w)
872
+ if body.Code != "FORBIDDEN" {
873
+ t.Fatalf("expected FORBIDDEN, got %q", body.Code)
874
+ }
875
+ }
876
+
877
+ func TestMethodNotAllowed(t *testing.T) {
878
+ w := httptest.NewRecorder()
879
+ req, _ := http.NewRequest(http.MethodGet, "/test", nil)
880
+ makeRouter(func(c *gin.Context) { apierr.MethodNotAllowed(c) }).ServeHTTP(w, req)
881
+
882
+ if w.Code != http.StatusMethodNotAllowed {
883
+ t.Fatalf("expected 405, got %d", w.Code)
884
+ }
885
+ body := readJSON(t, w)
886
+ if body.Code != "METHOD_NOT_ALLOWED" {
887
+ t.Fatalf("expected METHOD_NOT_ALLOWED, got %q", body.Code)
888
+ }
889
+ }
890
+
891
+ func TestInternalError(t *testing.T) {
892
+ w := httptest.NewRecorder()
893
+ req, _ := http.NewRequest(http.MethodGet, "/test", nil)
894
+ makeRouter(func(c *gin.Context) { apierr.InternalError(c, nil) }).ServeHTTP(w, req)
895
+
896
+ if w.Code != http.StatusInternalServerError {
897
+ t.Fatalf("expected 500, got %d", w.Code)
898
+ }
899
+ body := readJSON(t, w)
900
+ if body.Code != "INTERNAL_ERROR" {
901
+ t.Fatalf("expected INTERNAL_ERROR, got %q", body.Code)
902
+ }
903
+ }
904
+
905
+ func TestTooManyRequests(t *testing.T) {
906
+ w := httptest.NewRecorder()
907
+ req, _ := http.NewRequest(http.MethodGet, "/test", nil)
908
+ makeRouter(func(c *gin.Context) { apierr.TooManyRequests(c) }).ServeHTTP(w, req)
909
+
910
+ if w.Code != http.StatusTooManyRequests {
911
+ t.Fatalf("expected 429, got %d", w.Code)
912
+ }
913
+ body := readJSON(t, w)
914
+ if body.Code != "TOO_MANY_REQUESTS" {
915
+ t.Fatalf("expected TOO_MANY_REQUESTS, got %q", body.Code)
916
+ }
917
+ }
918
+ `}function M(e){return `// Package docs provides the swaggo-generated OpenAPI specification.
919
+ //
920
+ // Run \`make docs\` to regenerate after changing handler annotations.
921
+ //
922
+ // @title ${l(e.project_name)} API
923
+ // @version ${e.app_version}
924
+ // @description ${e.description}
925
+ // @host localhost:${e.port}
926
+ // @BasePath /
927
+ // @schemes http https
928
+ //
929
+ // @contact.name ${e.author}
930
+ // @license.name MIT
931
+ package docs
932
+ `}function D(e){return `package handlers
933
+
934
+ import (
935
+ "net/http"
936
+
937
+ "github.com/gin-gonic/gin"
938
+
939
+ "${e.module_path}/internal/apierr"
940
+ )
941
+
942
+ // EchoResponse is the JSON body returned by EchoParams.
943
+ type EchoResponse struct {
944
+ Name string \`json:"name"\`
945
+ RequestID string \`json:"request_id"\`
946
+ }
947
+
948
+ // EchoParams is an example handler demonstrating how to:
949
+ // - read URL path parameters
950
+ // - use apierr for consistent JSON error responses
951
+ // - access the request ID injected by RequestID middleware
952
+ //
953
+ // Replace or remove this file once you add your own business logic.
954
+ //
955
+ // @Summary Echo path parameter
956
+ // @Description Returns the :name path parameter together with the request ID.
957
+ // @Tags example
958
+ // @Produce json
959
+ // @Param name path string true "Name to echo"
960
+ // @Success 200 {object} handlers.EchoResponse
961
+ // @Failure 400 {object} apierr.Response
962
+ // @Router /api/v1/echo/{name} [get]
963
+ func EchoParams(c *gin.Context) {
964
+ name := c.Param("name")
965
+ if name == "" {
966
+ apierr.BadRequest(c, "name parameter is required")
967
+ return
968
+ }
969
+ c.JSON(http.StatusOK, EchoResponse{
970
+ Name: name,
971
+ RequestID: c.GetString("X-Request-ID"),
972
+ })
973
+ }
974
+ `}function P(e){return `package handlers_test
975
+
976
+ import (
977
+ "encoding/json"
978
+ "net/http"
979
+ "net/http/httptest"
980
+ "testing"
981
+
982
+ "github.com/gin-gonic/gin"
983
+
984
+ "${e.module_path}/internal/handlers"
985
+ "${e.module_path}/internal/middleware"
986
+ )
987
+
988
+ func newEchoRouter() *gin.Engine {
989
+ r := gin.New()
990
+ r.Use(middleware.RequestID())
991
+ r.GET("/echo/:name", handlers.EchoParams)
992
+ return r
993
+ }
994
+
995
+ func TestEchoParams_Success(t *testing.T) {
996
+ w := httptest.NewRecorder()
997
+ req, _ := http.NewRequest(http.MethodGet, "/echo/alice", nil)
998
+ newEchoRouter().ServeHTTP(w, req)
999
+
1000
+ if w.Code != http.StatusOK {
1001
+ t.Fatalf("expected 200, got %d", w.Code)
1002
+ }
1003
+
1004
+ var body map[string]any
1005
+ if err := json.NewDecoder(w.Body).Decode(&body); err != nil {
1006
+ t.Fatalf("invalid JSON: %v", err)
1007
+ }
1008
+ if body["name"] != "alice" {
1009
+ t.Fatalf("expected name=alice, got %v", body["name"])
1010
+ }
1011
+ if body["request_id"] == nil || body["request_id"] == "" {
1012
+ t.Fatal("expected request_id to be set by RequestID middleware")
1013
+ }
1014
+ }
1015
+
1016
+ // TestEchoParams_EmptyName registers EchoParams on a param-free route so that
1017
+ // c.Param("name") returns "" and the 400 guard executes.
1018
+ func TestEchoParams_EmptyName(t *testing.T) {
1019
+ gin.SetMode(gin.TestMode)
1020
+ r := gin.New()
1021
+ r.Use(middleware.RequestID())
1022
+ r.GET("/echo-bare", handlers.EchoParams)
1023
+
1024
+ w := httptest.NewRecorder()
1025
+ req, _ := http.NewRequest(http.MethodGet, "/echo-bare", nil)
1026
+ r.ServeHTTP(w, req)
1027
+
1028
+ if w.Code != http.StatusBadRequest {
1029
+ t.Fatalf("expected 400, got %d", w.Code)
1030
+ }
1031
+ var body map[string]any
1032
+ if err := json.NewDecoder(w.Body).Decode(&body); err != nil {
1033
+ t.Fatalf("invalid JSON: %v", err)
1034
+ }
1035
+ if body["code"] != "BAD_REQUEST" {
1036
+ t.Fatalf("expected code=BAD_REQUEST, got %v", body["code"])
1037
+ }
1038
+ }
1039
+ `}function A(e){return `package config_test
1040
+
1041
+ import (
1042
+ "log/slog"
1043
+ "testing"
1044
+
1045
+ "${e.module_path}/internal/config"
1046
+ )
1047
+
1048
+ func TestParseLogLevel(t *testing.T) {
1049
+ tests := []struct {
1050
+ input string
1051
+ want slog.Level
1052
+ }{
1053
+ {"debug", slog.LevelDebug},
1054
+ {"DEBUG", slog.LevelDebug},
1055
+ {"warn", slog.LevelWarn},
1056
+ {"warning", slog.LevelWarn},
1057
+ {"error", slog.LevelError},
1058
+ {"info", slog.LevelInfo},
1059
+ {"", slog.LevelInfo},
1060
+ {"unknown", slog.LevelInfo},
1061
+ }
1062
+ for _, tc := range tests {
1063
+ got := config.ParseLogLevel(tc.input)
1064
+ if got != tc.want {
1065
+ t.Errorf("ParseLogLevel(%q) = %v, want %v", tc.input, got, tc.want)
1066
+ }
1067
+ }
1068
+ }
1069
+
1070
+ func TestLoad_EnvOverride(t *testing.T) {
1071
+ t.Setenv("PORT", "9090")
1072
+ t.Setenv("APP_ENV", "production")
1073
+ t.Setenv("LOG_LEVEL", "warn")
1074
+ t.Setenv("GIN_MODE", "release")
1075
+
1076
+ cfg := config.Load()
1077
+
1078
+ if cfg.Port != "9090" {
1079
+ t.Errorf("expected Port=9090, got %q", cfg.Port)
1080
+ }
1081
+ if cfg.Env != "production" {
1082
+ t.Errorf("expected Env=production, got %q", cfg.Env)
1083
+ }
1084
+ if cfg.LogLevel != "warn" {
1085
+ t.Errorf("expected LogLevel=warn, got %q", cfg.LogLevel)
1086
+ }
1087
+ if cfg.GinMode != "release" {
1088
+ t.Errorf("expected GinMode=release, got %q", cfg.GinMode)
1089
+ }
1090
+ }
1091
+
1092
+ func TestLoad_Defaults(t *testing.T) {
1093
+ // Empty string forces getEnv() to return the built-in fallback value.
1094
+ t.Setenv("PORT", "")
1095
+ t.Setenv("APP_ENV", "")
1096
+ t.Setenv("LOG_LEVEL", "")
1097
+ t.Setenv("GIN_MODE", "")
1098
+
1099
+ cfg := config.Load()
1100
+
1101
+ if cfg.Port != "${e.port}" {
1102
+ t.Errorf("expected default Port=${e.port}, got %q", cfg.Port)
1103
+ }
1104
+ if cfg.Env != "development" {
1105
+ t.Errorf("expected default Env=development, got %q", cfg.Env)
1106
+ }
1107
+ // APP_ENV="" \u2192 fallback "development" \u2192 defaultLogLevel \u2192 "debug"
1108
+ if cfg.LogLevel != "debug" {
1109
+ t.Errorf("expected default LogLevel=debug (development env), got %q", cfg.LogLevel)
1110
+ }
1111
+ if cfg.GinMode != "debug" {
1112
+ t.Errorf("expected default GinMode=debug, got %q", cfg.GinMode)
1113
+ }
1114
+ }
1115
+ `}function H(){return `package middleware
1116
+
1117
+ import (
1118
+ "net/http"
1119
+ "os"
1120
+ "strings"
1121
+
1122
+ "github.com/gin-gonic/gin"
1123
+ )
1124
+
1125
+ // CORS returns a Gin middleware configured via CORS_ALLOW_ORIGINS env var.
1126
+ //
1127
+ // Set CORS_ALLOW_ORIGINS="*" for development (the default when unset).
1128
+ // In production supply a comma-separated list of allowed origins:
1129
+ //
1130
+ // CORS_ALLOW_ORIGINS=https://app.example.com,https://admin.example.com
1131
+ func CORS() gin.HandlerFunc {
1132
+ allowed := os.Getenv("CORS_ALLOW_ORIGINS")
1133
+ if allowed == "" {
1134
+ allowed = "*"
1135
+ }
1136
+ allowAll := allowed == "*"
1137
+
1138
+ return func(c *gin.Context) {
1139
+ origin := c.Request.Header.Get("Origin")
1140
+ if allowAll {
1141
+ c.Header("Access-Control-Allow-Origin", "*")
1142
+ } else if origin != "" {
1143
+ for _, o := range strings.Split(allowed, ",") {
1144
+ if strings.TrimSpace(o) == origin {
1145
+ c.Header("Access-Control-Allow-Origin", origin)
1146
+ c.Header("Vary", "Origin")
1147
+ break
1148
+ }
1149
+ }
1150
+ }
1151
+ c.Header("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS")
1152
+ c.Header("Access-Control-Allow-Headers", "Origin,Content-Type,Authorization,X-Request-ID")
1153
+ c.Header("Access-Control-Expose-Headers", "X-Request-ID")
1154
+ c.Header("Access-Control-Max-Age", "600")
1155
+
1156
+ if c.Request.Method == http.MethodOptions {
1157
+ c.AbortWithStatus(http.StatusNoContent)
1158
+ return
1159
+ }
1160
+ c.Next()
1161
+ }
1162
+ }
1163
+ `}function $(e){return `package middleware_test
1164
+
1165
+ import (
1166
+ "net/http"
1167
+ "net/http/httptest"
1168
+ "testing"
1169
+
1170
+ "github.com/gin-gonic/gin"
1171
+
1172
+ "${e.module_path}/internal/middleware"
1173
+ )
1174
+
1175
+ func newCORSRouter(t *testing.T) *gin.Engine {
1176
+ t.Helper()
1177
+ gin.SetMode(gin.TestMode)
1178
+ r := gin.New()
1179
+ r.Use(middleware.CORS())
1180
+ r.GET("/ping", func(c *gin.Context) { c.Status(http.StatusOK) })
1181
+ return r
1182
+ }
1183
+
1184
+ func TestCORS_Wildcard(t *testing.T) {
1185
+ t.Setenv("CORS_ALLOW_ORIGINS", "*")
1186
+ w := httptest.NewRecorder()
1187
+ req, _ := http.NewRequest(http.MethodGet, "/ping", nil)
1188
+ req.Header.Set("Origin", "https://example.com")
1189
+ newCORSRouter(t).ServeHTTP(w, req)
1190
+
1191
+ if got := w.Header().Get("Access-Control-Allow-Origin"); got != "*" {
1192
+ t.Fatalf("expected ACAO=*, got %q", got)
1193
+ }
1194
+ }
1195
+
1196
+ func TestCORS_Preflight(t *testing.T) {
1197
+ t.Setenv("CORS_ALLOW_ORIGINS", "*")
1198
+ gin.SetMode(gin.TestMode)
1199
+ r := gin.New()
1200
+ r.Use(middleware.CORS())
1201
+
1202
+ w := httptest.NewRecorder()
1203
+ req, _ := http.NewRequest(http.MethodOptions, "/ping", nil)
1204
+ req.Header.Set("Origin", "https://example.com")
1205
+ req.Header.Set("Access-Control-Request-Method", "POST")
1206
+ r.ServeHTTP(w, req)
1207
+
1208
+ if w.Code != http.StatusNoContent {
1209
+ t.Fatalf("expected 204 preflight, got %d", w.Code)
1210
+ }
1211
+ }
1212
+
1213
+ func TestCORS_SpecificOrigin_Allowed(t *testing.T) {
1214
+ t.Setenv("CORS_ALLOW_ORIGINS", "https://app.example.com")
1215
+ w := httptest.NewRecorder()
1216
+ req, _ := http.NewRequest(http.MethodGet, "/ping", nil)
1217
+ req.Header.Set("Origin", "https://app.example.com")
1218
+ newCORSRouter(t).ServeHTTP(w, req)
1219
+
1220
+ if got := w.Header().Get("Access-Control-Allow-Origin"); got != "https://app.example.com" {
1221
+ t.Fatalf("expected ACAO=https://app.example.com, got %q", got)
1222
+ }
1223
+ }
1224
+
1225
+ func TestCORS_SpecificOrigin_Denied(t *testing.T) {
1226
+ t.Setenv("CORS_ALLOW_ORIGINS", "https://app.example.com")
1227
+ w := httptest.NewRecorder()
1228
+ req, _ := http.NewRequest(http.MethodGet, "/ping", nil)
1229
+ req.Header.Set("Origin", "https://evil.com")
1230
+ newCORSRouter(t).ServeHTTP(w, req)
1231
+
1232
+ if got := w.Header().Get("Access-Control-Allow-Origin"); got != "" {
1233
+ t.Fatalf("expected no ACAO header for denied origin, got %q", got)
1234
+ }
1235
+ }
1236
+
1237
+ func TestCORS_Default_Origin(t *testing.T) {
1238
+ // When CORS_ALLOW_ORIGINS is unset the middleware must default to "*".
1239
+ t.Setenv("CORS_ALLOW_ORIGINS", "")
1240
+ w := httptest.NewRecorder()
1241
+ req, _ := http.NewRequest(http.MethodGet, "/ping", nil)
1242
+ req.Header.Set("Origin", "https://anywhere.com")
1243
+ newCORSRouter(t).ServeHTTP(w, req)
1244
+
1245
+ if got := w.Header().Get("Access-Control-Allow-Origin"); got == "" {
1246
+ t.Fatal("expected CORS header when CORS_ALLOW_ORIGINS defaults to *")
1247
+ }
1248
+ }
1249
+ `}function F(e){return `package server_test
1250
+
1251
+ import (
1252
+ "encoding/json"
1253
+ "net/http"
1254
+ "net/http/httptest"
1255
+ "testing"
1256
+
1257
+ "github.com/gin-gonic/gin"
1258
+
1259
+ "${e.module_path}/internal/config"
1260
+ "${e.module_path}/internal/server"
1261
+ )
1262
+
1263
+ func init() { gin.SetMode(gin.TestMode) }
1264
+
1265
+ type serverAPIError struct {
1266
+ Code string \`json:"code"\`
1267
+ Message string \`json:"message"\`
1268
+ }
1269
+
1270
+ func TestServer_NotFound_JSON(t *testing.T) {
1271
+ w := httptest.NewRecorder()
1272
+ req, _ := http.NewRequest(http.MethodGet, "/no-such-route", nil)
1273
+ server.NewRouter(config.Load()).ServeHTTP(w, req)
1274
+
1275
+ if w.Code != http.StatusNotFound {
1276
+ t.Fatalf("expected 404, got %d", w.Code)
1277
+ }
1278
+ var body serverAPIError
1279
+ if err := json.NewDecoder(w.Body).Decode(&body); err != nil {
1280
+ t.Fatalf("expected JSON error body: %v", err)
1281
+ }
1282
+ if body.Code != "NOT_FOUND" {
1283
+ t.Fatalf("expected code=NOT_FOUND, got %q", body.Code)
1284
+ }
1285
+ }
1286
+
1287
+ func TestServer_MethodNotAllowed_JSON(t *testing.T) {
1288
+ w := httptest.NewRecorder()
1289
+ req, _ := http.NewRequest(http.MethodPost, "/api/v1/health/live", nil)
1290
+ server.NewRouter(config.Load()).ServeHTTP(w, req)
1291
+
1292
+ if w.Code != http.StatusMethodNotAllowed {
1293
+ t.Fatalf("expected 405, got %d", w.Code)
1294
+ }
1295
+ var body serverAPIError
1296
+ if err := json.NewDecoder(w.Body).Decode(&body); err != nil {
1297
+ t.Fatalf("expected JSON error body: %v", err)
1298
+ }
1299
+ if body.Code != "METHOD_NOT_ALLOWED" {
1300
+ t.Fatalf("expected code=METHOD_NOT_ALLOWED, got %q", body.Code)
1301
+ }
1302
+ }
1303
+
1304
+ func TestServer_CORS_Header(t *testing.T) {
1305
+ t.Setenv("CORS_ALLOW_ORIGINS", "*")
1306
+ w := httptest.NewRecorder()
1307
+ req, _ := http.NewRequest(http.MethodGet, "/api/v1/health/live", nil)
1308
+ req.Header.Set("Origin", "https://example.com")
1309
+ server.NewRouter(config.Load()).ServeHTTP(w, req)
1310
+
1311
+ if got := w.Header().Get("Access-Control-Allow-Origin"); got == "" {
1312
+ t.Fatal("expected Access-Control-Allow-Origin header to be set")
1313
+ }
1314
+ }
1315
+
1316
+ func TestServer_HealthLive(t *testing.T) {
1317
+ w := httptest.NewRecorder()
1318
+ req, _ := http.NewRequest(http.MethodGet, "/api/v1/health/live", nil)
1319
+ server.NewRouter(config.Load()).ServeHTTP(w, req)
1320
+
1321
+ if w.Code != http.StatusOK {
1322
+ t.Fatalf("expected 200, got %d", w.Code)
1323
+ }
1324
+ var body map[string]any
1325
+ if err := json.NewDecoder(w.Body).Decode(&body); err != nil {
1326
+ t.Fatalf("invalid JSON: %v", err)
1327
+ }
1328
+ if body["status"] != "ok" {
1329
+ t.Fatalf("expected status=ok, got %v", body["status"])
1330
+ }
1331
+ }
1332
+
1333
+ func TestServer_Docs_Redirect(t *testing.T) {
1334
+ w := httptest.NewRecorder()
1335
+ req, _ := http.NewRequest(http.MethodGet, "/docs", nil)
1336
+ server.NewRouter(config.Load()).ServeHTTP(w, req)
1337
+
1338
+ if w.Code != http.StatusFound {
1339
+ t.Fatalf("expected 302 redirect from /docs, got %d", w.Code)
1340
+ }
1341
+ if loc := w.Header().Get("Location"); loc != "/docs/index.html" {
1342
+ t.Fatalf("expected Location=/docs/index.html, got %q", loc)
1343
+ }
1344
+ }
1345
+
1346
+ func TestServer_ReleaseMode(t *testing.T) {
1347
+ // Covers the gin.SetMode(gin.ReleaseMode) branch in NewRouter.
1348
+ // Restore test mode after so other tests are not affected.
1349
+ t.Cleanup(func() { gin.SetMode(gin.TestMode) })
1350
+ t.Setenv("GIN_MODE", "release")
1351
+ w := httptest.NewRecorder()
1352
+ req, _ := http.NewRequest(http.MethodGet, "/api/v1/health/live", nil)
1353
+ server.NewRouter(config.Load()).ServeHTTP(w, req)
1354
+
1355
+ if w.Code != http.StatusOK {
1356
+ t.Fatalf("expected 200 in release mode, got %d", w.Code)
1357
+ }
1358
+ }
1359
+ `}function j(e){return `package middleware
1360
+
1361
+ import (
1362
+ "os"
1363
+ "strconv"
1364
+ "sync"
1365
+ "time"
1366
+
1367
+ "github.com/gin-gonic/gin"
1368
+
1369
+ "${e.module_path}/internal/apierr"
1370
+ )
1371
+
1372
+ // ipBucket tracks the per-IP fixed-window request counter.
1373
+ type ipBucket struct {
1374
+ mu sync.Mutex
1375
+ count int
1376
+ windowStart time.Time
1377
+ }
1378
+
1379
+ // RateLimit returns a per-IP fixed-window rate limiter.
1380
+ // Configure the limit via RATE_LIMIT_RPS env var (requests per second, default 100).
1381
+ func RateLimit() gin.HandlerFunc {
1382
+ rps := 100
1383
+ if raw := os.Getenv("RATE_LIMIT_RPS"); raw != "" {
1384
+ if n, err := strconv.Atoi(raw); err == nil && n > 0 {
1385
+ rps = n
1386
+ }
1387
+ }
1388
+ var buckets sync.Map
1389
+ return func(c *gin.Context) {
1390
+ ip := c.ClientIP()
1391
+ now := time.Now()
1392
+ v, _ := buckets.LoadOrStore(ip, &ipBucket{windowStart: now})
1393
+ b := v.(*ipBucket)
1394
+ b.mu.Lock()
1395
+ if now.Sub(b.windowStart) >= time.Second {
1396
+ b.count = 0
1397
+ b.windowStart = now
1398
+ }
1399
+ b.count++
1400
+ count := b.count
1401
+ b.mu.Unlock()
1402
+ if count > rps {
1403
+ apierr.TooManyRequests(c)
1404
+ c.Header("Retry-After", "1")
1405
+ return
1406
+ }
1407
+ c.Next()
1408
+ }
1409
+ }
1410
+ `}function U(e){return `package middleware_test
1411
+
1412
+ import (
1413
+ "net/http"
1414
+ "net/http/httptest"
1415
+ "testing"
1416
+
1417
+ "github.com/gin-gonic/gin"
1418
+
1419
+ "${e.module_path}/internal/middleware"
1420
+ )
1421
+
1422
+ func newRateLimitRouter(t *testing.T) *gin.Engine {
1423
+ t.Helper()
1424
+ gin.SetMode(gin.TestMode)
1425
+ r := gin.New()
1426
+ r.Use(middleware.RateLimit())
1427
+ r.GET("/", func(c *gin.Context) { c.Status(http.StatusOK) })
1428
+ return r
1429
+ }
1430
+
1431
+ func TestRateLimit_AllowsUnderLimit(t *testing.T) {
1432
+ t.Setenv("RATE_LIMIT_RPS", "3")
1433
+ r := newRateLimitRouter(t)
1434
+
1435
+ for i := 0; i < 3; i++ {
1436
+ w := httptest.NewRecorder()
1437
+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
1438
+ r.ServeHTTP(w, req)
1439
+ if w.Code != http.StatusOK {
1440
+ t.Fatalf("request %d: expected 200, got %d", i+1, w.Code)
1441
+ }
1442
+ }
1443
+ }
1444
+
1445
+ func TestRateLimit_Blocks_After_Limit(t *testing.T) {
1446
+ t.Setenv("RATE_LIMIT_RPS", "2")
1447
+ r := newRateLimitRouter(t)
1448
+
1449
+ // Exhaust the limit.
1450
+ for i := 0; i < 2; i++ {
1451
+ w := httptest.NewRecorder()
1452
+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
1453
+ r.ServeHTTP(w, req)
1454
+ }
1455
+
1456
+ // Next request must be rejected.
1457
+ w := httptest.NewRecorder()
1458
+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
1459
+ r.ServeHTTP(w, req)
1460
+
1461
+ if w.Code != http.StatusTooManyRequests {
1462
+ t.Fatalf("expected 429 after limit, got %d", w.Code)
1463
+ }
1464
+ }
1465
+
1466
+ func TestRateLimit_InvalidRPS(t *testing.T) {
1467
+ // When RATE_LIMIT_RPS is not a valid positive integer, the middleware
1468
+ // must fall back to the default limit (100 rps) and allow normal requests.
1469
+ t.Setenv("RATE_LIMIT_RPS", "not-a-number")
1470
+ r := newRateLimitRouter(t)
1471
+ w := httptest.NewRecorder()
1472
+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
1473
+ r.ServeHTTP(w, req)
1474
+
1475
+ if w.Code != http.StatusOK {
1476
+ t.Fatalf("expected 200 with invalid RPS env, got %d", w.Code)
1477
+ }
1478
+ }
1479
+ `}function B(e){return `# Air \u2014 live reload for Go projects
1480
+ # https://github.com/air-verse/air
1481
+ root = "."
1482
+ tmp_dir = "tmp"
1483
+
1484
+ [build]
1485
+ pre_cmd = ["$(go env GOPATH)/bin/swag init -g main.go -d cmd/server,internal/handlers,internal/apierr -o docs --parseDependency 2>/dev/null || true"]
1486
+ cmd = "go build -o ./tmp/server ./cmd/server"
1487
+ bin = "./tmp/server"
1488
+ include_ext = ["go", "yaml", "yml", "env"]
1489
+ exclude_dir = ["tmp", "vendor", ".git", "testdata", "docs"]
1490
+ delay = 500
1491
+ rerun_delay = 500
1492
+ send_interrupt = true
1493
+ kill_delay = "200ms"
1494
+
1495
+ [env]
1496
+ GIN_MODE = "debug"
1497
+ PORT = "${e.port}"
1498
+
1499
+ [misc]
1500
+ clean_on_exit = true
1501
+
1502
+ [log]
1503
+ time = false
1504
+ `}function V(e){return `run:
1505
+ timeout: 5m
1506
+
1507
+ linters:
1508
+ enable:
1509
+ - bodyclose
1510
+ - durationcheck
1511
+ - errcheck
1512
+ - errname
1513
+ - errorlint
1514
+ - gci
1515
+ - goimports
1516
+ - gosimple
1517
+ - govet
1518
+ - ineffassign
1519
+ - misspell
1520
+ - noctx
1521
+ - nolintlint
1522
+ - prealloc
1523
+ - staticcheck
1524
+ - unconvert
1525
+ - unused
1526
+ - wrapcheck
1527
+
1528
+ linters-settings:
1529
+ gci:
1530
+ sections:
1531
+ - standard
1532
+ - default
1533
+ - prefix(${e})
1534
+ goimports:
1535
+ local-prefixes: "${e}"
1536
+ govet:
1537
+ enable:
1538
+ - shadow
1539
+ wrapcheck:
1540
+ ignorePackageGlobs:
1541
+ - "${e}/*"
1542
+
1543
+ issues:
1544
+ max-same-issues: 5
1545
+ exclude-rules:
1546
+ - path: _test.go
1547
+ linters:
1548
+ - errcheck
1549
+ - wrapcheck
1550
+ `}function W(){return JSON.stringify({engine:"npm",runtime:"go"},null,2)}function J(e,o){return JSON.stringify({kit_name:"gogin.standard",runtime:"go",module_support:false,project_name:e.project_name,module_path:e.module_path,app_version:e.app_version,created_by:"rapidkit-npm",rapidkit_version:o,created_at:new Date().toISOString()},null,2)}function K(e){return `#!/usr/bin/env sh
1551
+ # RapidKit Go/Gin project launcher \u2014 generated by RapidKit CLI
1552
+ # https://getrapidkit.com
1553
+
1554
+ SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
1555
+ CMD="\${1:-}"
1556
+ shift 2>/dev/null || true
1557
+
1558
+ case "$CMD" in
1559
+ init)
1560
+ cd "$SCRIPT_DIR"
1561
+ echo "\u{1F439} Initializing Go/Gin project\u2026"
1562
+ GOBIN="$(go env GOPATH)/bin"
1563
+ echo " \u2192 installing air (hot reload)\u2026"
1564
+ go install github.com/air-verse/air@latest 2>/dev/null && echo " \u2713 air" || echo " \u26A0 air install failed (run: go install github.com/air-verse/air@latest)"
1565
+ echo " \u2192 installing swag (swagger)\u2026"
1566
+ go install github.com/swaggo/swag/cmd/swag@latest 2>/dev/null && echo " \u2713 swag" || echo " \u26A0 swag install failed (run: go install github.com/swaggo/swag/cmd/swag@latest)"
1567
+ if [ ! -f ".env" ] && [ -f ".env.example" ]; then
1568
+ cp .env.example .env && echo " \u2713 .env created from .env.example"
1569
+ fi
1570
+ go mod tidy && echo " \u2713 go mod tidy"
1571
+ echo " \u2192 generating swagger docs (first build)\u2026"
1572
+ "$(go env GOPATH)/bin/swag" init -g main.go -d cmd/server,internal/handlers,internal/apierr -o docs --parseDependency 2>/dev/null && echo " \u2713 swagger docs generated" || echo " \u26A0 swagger docs skipped (run: rapidkit docs)"
1573
+ echo "\u2705 Ready \u2014 run: rapidkit dev"
1574
+ ;;
1575
+ dev)
1576
+ cd "$SCRIPT_DIR"
1577
+ echo "\u{1F4D6} Syncing swagger docs\u2026"
1578
+ "$(go env GOPATH)/bin/swag" init -g main.go -d cmd/server,internal/handlers,internal/apierr -o docs --parseDependency 2>/dev/null || true
1579
+ if [ -f "$SCRIPT_DIR/Makefile" ]; then
1580
+ exec make -C "$SCRIPT_DIR" dev "$@"
1581
+ else
1582
+ cd "$SCRIPT_DIR" && GIN_MODE=debug exec go run ./cmd/server "$@"
1583
+ fi
1584
+ ;;
1585
+ start)
1586
+ BIN="$SCRIPT_DIR/bin/${e.project_name}"
1587
+ if [ ! -f "$BIN" ]; then
1588
+ make -C "$SCRIPT_DIR" build
1589
+ fi
1590
+ exec "$BIN" "$@"
1591
+ ;;
1592
+ build)
1593
+ exec make -C "$SCRIPT_DIR" build "$@"
1594
+ ;;
1595
+ test)
1596
+ exec make -C "$SCRIPT_DIR" test "$@"
1597
+ ;;
1598
+ lint)
1599
+ exec make -C "$SCRIPT_DIR" lint "$@"
1600
+ ;;
1601
+ format|fmt)
1602
+ exec make -C "$SCRIPT_DIR" fmt "$@"
1603
+ ;;
1604
+ docs)
1605
+ exec make -C "$SCRIPT_DIR" docs "$@"
1606
+ ;;
1607
+ help|--help|-h)
1608
+ echo "RapidKit \u2014 Go/Gin project: ${e.project_name}"
1609
+ echo ""
1610
+ echo "Usage: rapidkit <command>"
1611
+ echo ""
1612
+ echo " init Install tools + create .env (air, swag, go mod tidy)"
1613
+ echo " dev Hot reload dev server (make dev \u2014 requires air)"
1614
+ echo " start Run compiled binary (make build + bin)"
1615
+ echo " build Build binary (make build)"
1616
+ echo " docs Generate Swagger docs (make docs \u2014 requires swag)"
1617
+ echo " test Run tests (make test)"
1618
+ echo " lint Run linter (make lint)"
1619
+ echo " format Format code (make fmt)"
1620
+ ;;
1621
+ *)
1622
+ if [ -n "$CMD" ]; then
1623
+ echo "rapidkit: unknown command: $CMD" >&2
1624
+ fi
1625
+ echo "Available: init, dev, start, build, docs, test, lint, format" >&2
1626
+ exit 1
1627
+ ;;
1628
+ esac
1629
+ `}function X(e){return `@echo off
1630
+ rem RapidKit Go/Gin project launcher \u2014 Windows
1631
+ set CMD=%1
1632
+ if "%CMD%"=="" goto usage
1633
+ shift
1634
+
1635
+ if "%CMD%"=="init" (
1636
+ echo Initializing Go/Gin project...
1637
+ go install github.com/air-verse/air@latest
1638
+ go install github.com/swaggo/swag/cmd/swag@latest
1639
+ if not exist .env if exist .env.example copy .env.example .env
1640
+ go mod tidy
1641
+ exit /b %ERRORLEVEL%
1642
+ )
1643
+ if "%CMD%"=="dev" ( make dev %* & exit /b %ERRORLEVEL% )
1644
+ if "%CMD%"=="build" ( make build %* & exit /b %ERRORLEVEL% )
1645
+ if "%CMD%"=="test" ( make test %* & exit /b %ERRORLEVEL% )
1646
+ if "%CMD%"=="lint" ( make lint %* & exit /b %ERRORLEVEL% )
1647
+ if "%CMD%"=="format" ( make fmt %* & exit /b %ERRORLEVEL% )
1648
+ if "%CMD%"=="docs" ( make docs %* & exit /b %ERRORLEVEL% )
1649
+ if "%CMD%"=="start" ( bin\\${e.project_name}.exe %* & exit /b %ERRORLEVEL% )
1650
+
1651
+ :usage
1652
+ echo Available: init, dev, start, build, docs, test, lint, format
1653
+ exit /b 1
1654
+ `}async function ne(e,o){let r={project_name:o.project_name,module_path:o.module_path||o.project_name,author:o.author||"RapidKit User",description:o.description||`Go/Gin REST API \u2014 ${o.project_name}`,go_version:o.go_version||"1.24",app_version:o.app_version||"0.1.0",port:o.port||"8080",skipGit:o.skipGit??false},g=b$1();try{await execa("go",["version"],{timeout:3e3});}catch{console.log(n.yellow("\n\u26A0 Go not found in PATH \u2014 project will be scaffolded, but `go mod tidy` requires Go 1.21+")),console.log(n.gray(` Install: https://go.dev/dl/
1655
+ `));}let i=f(`Generating Go/Gin project: ${r.project_name}\u2026`).start();try{let t=(m,h)=>w(s.join(e,m),h),u=s.join(e,"rapidkit"),p=s.join(e,"rapidkit.cmd");await Promise.all([t("cmd/server/main.go",v(r)),t("go.mod",R(r)),t("internal/config/config.go",S(r)),t("internal/server/server.go",b(r)),t("internal/middleware/requestid.go",I()),t("internal/middleware/requestid_test.go",L(r)),t("internal/apierr/apierr.go",x()),t("internal/apierr/apierr_test.go",k(r)),t("internal/handlers/health.go",T()),t("internal/handlers/health_test.go",_(r)),t("internal/handlers/example.go",D(r)),t("internal/handlers/example_test.go",P(r)),t("internal/config/config_test.go",A(r)),t("internal/middleware/cors.go",H()),t("internal/middleware/cors_test.go",$(r)),t("internal/middleware/ratelimit.go",j(r)),t("internal/middleware/ratelimit_test.go",U(r)),t("internal/server/server_test.go",F(r)),t("docs/doc.go",M(r)),t(".air.toml",B(r)),t("Dockerfile",O()),t("docker-compose.yml",N(r)),t("Makefile",E(r)),t(".golangci.yml",V(r.module_path)),t(".env.example",G(r)),t(".gitignore",q()),t(".github/workflows/ci.yml",C(r)),t("README.md",y(r)),t(".rapidkit/project.json",J(r,g)),t(".rapidkit/context.json",W()),t("rapidkit",K(r)),t("rapidkit.cmd",X(r))]),await promises.chmod(u,493),await promises.chmod(p,493),i.succeed(n.green(`Project created at ${e}`));try{i.start("Fetching Go dependencies\u2026"),await execa("go",["mod","tidy"],{cwd:e,timeout:12e4}),i.succeed(n.gray("\u2713 go mod tidy completed"));}catch{i.warn(n.yellow("\u26A0 go mod tidy failed \u2014 run manually: go mod tidy"));}if(!r.skipGit)try{await execa("git",["init"],{cwd:e}),await execa("git",["add","-A"],{cwd:e}),await execa("git",["commit","-m","chore: initial scaffold (rapidkit gogin.standard)"],{cwd:e}),console.log(n.gray("\u2713 git repository initialized"));}catch{console.log(n.gray("\u26A0 git init skipped (git not found or error)"));}console.log(""),console.log(n.bold("\u2705 Go/Gin project ready!")),console.log(""),console.log(n.cyan("Next steps:")),console.log(n.white(` cd ${r.project_name}`)),console.log(n.white(" make run # start dev server")),console.log(n.white(" make test # run tests")),console.log(""),console.log(n.gray("Server will listen on port "+r.port)),console.log(n.gray(" http://localhost:"+r.port+"/api/v1/health/live")),console.log(n.gray(" http://localhost:"+r.port+"/api/v1/health/ready")),console.log(""),console.log(n.yellow("\u2139 RapidKit modules are not available for Go projects (module system uses Python/pip).")),console.log("");}catch(t){throw i.fail(n.red("Failed to generate Go/Gin project")),t}}
1656
+ export{ne as generateGoGinKit};