From dbd19304f7201581c8f62e322c0adfe3e5fffd97 Mon Sep 17 00:00:00 2001 From: just do it <52734504+wdcodecn@users.noreply.github.com> Date: Mon, 1 Sep 2025 17:55:22 +0800 Subject: [PATCH 1/3] refactor: optimize Fiber app initialization to use singleton pattern --- example/internal/api/server.go | 26 +++++++++-------- .../templates/init/internal/api/server.tmpl | 28 ++++++++++--------- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/example/internal/api/server.go b/example/internal/api/server.go index 73ad5f2..3c135de 100644 --- a/example/internal/api/server.go +++ b/example/internal/api/server.go @@ -4,18 +4,20 @@ import ( "github.com/gofiber/fiber/v2" ) +var app = fiber.New(fiber.Config{ + AppName: "E-commerce API", + ErrorHandler: func(c *fiber.Ctx, err error) error { + code := fiber.StatusInternalServerError + if e, ok := err.(*fiber.Error); ok { + code = e.Code + } + return c.Status(code).JSON(fiber.Map{ + "error": err.Error(), + }) + }, +}) + // ProvideFiberApp creates a new Fiber application func ProvideFiberApp() *fiber.App { - return fiber.New(fiber.Config{ - AppName: "E-commerce API", - ErrorHandler: func(c *fiber.Ctx, err error) error { - code := fiber.StatusInternalServerError - if e, ok := err.(*fiber.Error); ok { - code = e.Code - } - return c.Status(code).JSON(fiber.Map{ - "error": err.Error(), - }) - }, - }) + return app } diff --git a/internal/generator/templates/init/internal/api/server.tmpl b/internal/generator/templates/init/internal/api/server.tmpl index 8e045b3..6338188 100644 --- a/internal/generator/templates/init/internal/api/server.tmpl +++ b/internal/generator/templates/init/internal/api/server.tmpl @@ -4,18 +4,20 @@ import ( "github.com/gofiber/fiber/v2" ) +var app = fiber.New(fiber.Config{ + AppName: "{{.ProjectName}} API", + ErrorHandler: func(c *fiber.Ctx, err error) error { + code := fiber.StatusInternalServerError + if e, ok := err.(*fiber.Error); ok { + code = e.Code + } + return c.Status(code).JSON(fiber.Map{ + "error": err.Error(), + }) + }, +}) + // ProvideFiberApp creates a new Fiber application func ProvideFiberApp() *fiber.App { - return fiber.New(fiber.Config{ - AppName: "{{.ProjectName}} API", - ErrorHandler: func(c *fiber.Ctx, err error) error { - code := fiber.StatusInternalServerError - if e, ok := err.(*fiber.Error); ok { - code = e.Code - } - return c.Status(code).JSON(fiber.Map{ - "error": err.Error(), - }) - }, - }) -} \ No newline at end of file + return app +} From 34de9260d011a46bb2d01187a6598cdef826e1cd Mon Sep 17 00:00:00 2001 From: just do it <52734504+wdcodecn@users.noreply.github.com> Date: Tue, 9 Sep 2025 18:40:28 +0800 Subject: [PATCH 2/3] feat: enhance ASTScanner and RouteGenerator to include full package paths CLAUDE.md and enhance scanner to include full package paths - Add CLAUDE.md for project guidance - Update scanner to extract and use full package paths - Modify RouteGenerator to derive import paths from full package paths --- CLAUDE.md | 259 ++++++++++++++++++++++++++++++++ internal/generator/routes.go | 38 +++-- internal/scanner/ast_scanner.go | 80 ++++++++-- internal/scanner/scanner.go | 2 +- internal/scanner/types.go | 12 +- 5 files changed, 357 insertions(+), 34 deletions(-) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..a2479ea --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,259 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Taskw is a Go CLI tool that generates code for Go APIs using the Fiber web framework, Wire dependency injection, and Swaggo annotations. It eliminates boilerplate by scanning your code for annotations and provider functions to automatically generate route registration and dependency injection code. + +**Core Technologies:** +- Go 1.24.1 +- Fiber v2 (web framework) +- Google Wire (dependency injection) +- Swaggo (OpenAPI/Swagger annotations) +- Cobra (CLI framework) +- Viper (configuration management) + +## Common Development Commands + +### Build and Install +```bash +# Build the binary +task build + +# Install development version to PATH +task install-dev + +# Install production version to PATH +task install + +# Build manually +go build -o bin/taskw main.go +``` + +### Testing +```bash +# Run all tests +task test + +# Run end-to-end tests +task test-e2e + +# Run tests manually +go test -v ./... +go test -v ./test/e2e/... +``` + +### Code Generation +The project uses Wire for dependency injection. After making changes to providers: +```bash +# Generate Wire code +go generate ./... + +# Or specifically for CLI services +go generate ./internal/cli/ +``` + +### Using Taskw CLI +```bash +# Initialize new project +taskw init [module-name] + +# Generate all code +taskw generate +taskw generate all + +# Generate specific components +taskw generate routes +taskw generate deps + +# Scan and preview what will be generated +taskw scan + +# Clean generated files +taskw clean +``` + +## Architecture Overview + +### Project Structure +``` +taskw/ +├── cmd/taskw/ # CLI entry point (Cobra commands) +├── internal/ +│ ├── cli/ # CLI service layer with Wire DI +│ ├── config/ # Configuration management (Viper) +│ ├── scanner/ # Code scanning (AST parsing + file filtering) +│ └── generator/ # Code generation (templates + formatting) +├── main.go # Application entry point +└── taskw.yaml # Configuration file +``` + +### Dependency Injection Architecture + +The CLI uses Wire for dependency injection with the following pattern: + +1. **Services**: Located in `internal/cli/*/service.go` files +2. **Wire Configuration**: `internal/cli/wire.go` defines providers +3. **Generated Code**: `internal/cli/wire_gen.go` and `dependencies_gen.go` +4. **Container Pattern**: `cli.Container` holds all injected services + +**Key Services:** +- `ui.Service`: User interface and spinner management +- `project.Service`: Project scaffolding and validation +- `scan.Service`: Code scanning and analysis +- `generation.Service`: Code generation orchestration +- `clean.Service`: Generated file cleanup +- `file.Service`: File system operations + +### Route Generation Architecture + +The route generator handles complex routing patterns with specificity-based ordering: + +**Route Processing Pipeline:** +1. **Path Conversion**: Converts OpenAPI `{param}` format to Fiber `:param` format +2. **Specificity Scoring**: Routes are scored and sorted (more specific routes first) +3. **Package Organization**: Routes are grouped by package but sorted globally +4. **Import Path Resolution**: Automatically derives handler import paths from package structure + +**Key Components:** +- `RouteGenerator`: Main route generation orchestrator +- `HandlerInfo`: Represents handler dependency injection information with full package paths +- **Specificity Algorithm**: Uses segment count and parameter penalties for routing order +- **Template System**: Embedded templates with Go formatting and import management + +**Route Specificity Rules:** +- Longer paths get higher scores (1000 points per segment) +- Static segments add 100 points each +- Parameters (`:param`) subtract 100 points each +- Routes with higher scores are registered first to prevent conflicts + +### Scanning Architecture + +The scanner uses a hybrid approach with parallel processing: +- **File Filtering**: Identifies candidate files quickly +- **AST Parsing**: Parallel processing with semaphore-controlled goroutines (max 10) +- **Error Collection**: Non-fatal errors are collected in ScanResult.Errors + +**Key Scanner Types:** +- `scanner.HandlerFunction`: Represents provider functions +- `scanner.RouteMapping`: Represents route annotations with package paths +- `scanner.ProviderFunction`: Represents Wire provider functions +- `scanner.ScanResult`: Combined scan results with error tracking +- `scanner.ScanError`: Error information with file path and type +- `scanner.ScanStatistics`: Performance metrics for debugging + +**Scanner Components:** +- `Scanner`: Main hybrid scanner orchestrating file filtering and AST parsing +- `ASTScanner`: Handles Go AST parsing for annotations and providers +- `FileFilter`: Optimizes file discovery before AST parsing + +## Configuration Management + +Taskw uses Viper for configuration with the following hierarchy: +1. Command line flags (`--config`) +2. `taskw.yaml` file +3. Default values + +**Key Configuration Sections:** +- `paths.scan_dirs`: Directories to scan for code +- `paths.output_dir`: Where to generate files +- `generation.routes.enabled`: Enable route generation +- `generation.dependencies.enabled`: Enable dependency generation + +## Code Style and Patterns + +### Service Pattern +All CLI functionality is organized into services with this pattern: +```go +type Service interface { + // Interface defines contract +} + +type serviceImpl struct { + // Dependencies injected via constructor +} + +func ProvideService(deps...) Service { + return &serviceImpl{...} +} +``` + +### Error Handling +Use wrapped errors with context: +```go +return fmt.Errorf("failed to scan directory %s: %w", dir, err) +``` + +### Template Usage +Code generation uses embedded templates in `internal/generator/templates/`: +- Templates are embedded using `//go:embed` +- Use `text/template` for Go code generation +- Apply `go/format` to generated code + +### Wire Integration +When adding new services: +1. Create service interface and implementation +2. Add `Provide*` function +3. Add to `internal/cli/dependencies_gen.go` (or let Taskw generate it) +4. Run `go generate ./internal/cli/` + +## Testing Approach + +- **Unit Tests**: Standard Go tests in `*_test.go` files +- **E2E Tests**: Located in `test/e2e/` directory +- **Integration Tests**: Test complete CLI workflows + +## Important Implementation Details + +### File Generation Safety +- Generated files include header comments marking them as generated +- Clean command only removes files with generation markers +- Templates handle Go imports and formatting automatically + +### CLI UX Patterns +- Use spinners for long-running operations +- Provide clear error messages with actionable suggestions +- Support both interactive and non-interactive modes + +### Performance Considerations +- **Parallel Scanning**: Uses semaphore-controlled goroutines (max 10) for file processing +- **File Filtering**: Pre-filters candidates before expensive AST parsing +- **Route Specificity**: Complex scoring algorithm ensures correct route registration order +- **Error Resilience**: Non-fatal scan errors don't stop processing of other files +- **Generated Code Caching**: Files are only regenerated when source changes +- **Wire Compile-Time Safety**: Dependency injection validated at compile time + +### Route Generation Specifics +- **Path Parameter Conversion**: Automatically converts `{id}` to `:id` for Fiber compatibility +- **Import Path Derivation**: Uses project module config to build correct import paths +- **Handler Reference Resolution**: Converts handler refs like `userHandler.GetUsers` to `ar.userHandler.GetUsers` +- **Deterministic Ordering**: Routes and handlers are sorted for consistent generated code + +## Common Tasks for Contributors + +### Adding a New CLI Command +1. Add command definition to `cmd/taskw/main.go` +2. Create service interface if complex logic needed +3. Add service implementation in `internal/cli/[service]/` +4. Wire dependencies in `internal/cli/wire.go` +5. Generate Wire code with `go generate ./internal/cli/` + +### Modifying Code Generation +1. Update scanner types in `internal/scanner/types.go` +2. Modify templates in `internal/generator/templates/` +3. Update generator logic in `internal/generator/` +4. Consider route specificity impact if changing route handling +5. Test with `taskw scan` and `taskw generate` + +### Working with Scanner Changes +1. Understand parallel processing model - changes affect goroutine safety +2. Update `ScanResult` struct if adding new scan data types +3. Handle errors gracefully using `ScanError` collection pattern +4. Test performance impact with `GetStatistics()` for large codebases +5. Consider AST vs file filtering trade-offs for new features + +### Adding Configuration Options +1. Update config structs in `internal/config/config.go` +2. Add Viper defaults in `setDefaults()` +3. Update example `taskw.yaml` in documentation \ No newline at end of file diff --git a/internal/generator/routes.go b/internal/generator/routes.go index 70a839e..58d2911 100644 --- a/internal/generator/routes.go +++ b/internal/generator/routes.go @@ -31,10 +31,11 @@ func NewRouteGenerator(cfg *config.Config) *RouteGenerator { // HandlerInfo represents information about a handler for dependency injection type HandlerInfo struct { - FieldName string // e.g., "userHandler" - ParamName string // e.g., "userHandler" - TypeName string // e.g., "user.Handler" - Package string // e.g., "user" + FieldName string // e.g., "userHandler" + ParamName string // e.g., "userHandler" + TypeName string // e.g., "user.Handler" + Package string // e.g., "user" + FullPackagePath string // e.g., "domain/user" } // GenerateRoutes generates the routes_gen.go file @@ -89,8 +90,8 @@ func (g *RouteGenerator) generateImports(handlers []scanner.HandlerFunction, rou // Add imports for handler packages packageSet := make(map[string]bool) for _, handler := range handlerInfo { - // Derive the import path from the handler package - importPath := g.deriveHandlerImportPath(handler.Package) + // Derive the import path from the handler full package path + importPath := g.deriveHandlerImportPath(handler.FullPackagePath) if importPath != "" { packageSet[fmt.Sprintf(`"%s"`, importPath)] = true } @@ -103,7 +104,6 @@ func (g *RouteGenerator) generateImports(handlers []scanner.HandlerFunction, rou } sort.Strings(packageImports) imports = append(imports, packageImports...) - return imports } @@ -335,10 +335,11 @@ func (g *RouteGenerator) extractHandlerInfo(handlers []scanner.HandlerFunction, // Create handler info if not already present if _, exists := handlerMap[handlerName]; !exists { handlerMap[handlerName] = HandlerInfo{ - FieldName: handlerName, // e.g., "userHandler" - ParamName: handlerName, // e.g., "userHandler" - TypeName: g.getHandlerTypeName(pkg), - Package: pkg, + FieldName: handlerName, // e.g., "userHandler" + ParamName: handlerName, // e.g., "userHandler" + TypeName: g.getHandlerTypeName(pkg), + Package: pkg, + FullPackagePath: route.FullPackagePath, } } } @@ -367,14 +368,21 @@ func (g *RouteGenerator) getHandlerTypeName(pkg string) string { } // deriveHandlerImportPath derives the import path for a handler package -func (g *RouteGenerator) deriveHandlerImportPath(pkg string) string { +func (g *RouteGenerator) deriveHandlerImportPath(fullPackagePath string) string { // Use the project module from config and construct the path - // Assuming handlers are in internal/ relative to project root + // fullPackagePath already contains the complete path (e.g., "domain/user") if g.config != nil && g.config.Project.Module != "" { - return fmt.Sprintf("%s/internal/%s", g.config.Project.Module, pkg) + if fullPackagePath != "" { + return fmt.Sprintf("%s/internal/%s", g.config.Project.Module, fullPackagePath) + } + // Fallback to simple package name if full path is empty + return fmt.Sprintf("%s/internal", g.config.Project.Module) } // Fallback - this should be improved based on actual project structure - return fmt.Sprintf("internal/%s", pkg) + if fullPackagePath != "" { + return fmt.Sprintf("internal/%s", fullPackagePath) + } + return "internal" } // writeGeneratedFile writes content to a file with proper Go formatting diff --git a/internal/scanner/ast_scanner.go b/internal/scanner/ast_scanner.go index 870e8c6..7408d8b 100644 --- a/internal/scanner/ast_scanner.go +++ b/internal/scanner/ast_scanner.go @@ -5,19 +5,22 @@ import ( "go/ast" "go/parser" "go/token" + "path/filepath" "regexp" "strings" ) // ASTScanner uses Go's AST parser for accurate code analysis type ASTScanner struct { - fset *token.FileSet + fset *token.FileSet + scanDirs []string } // NewASTScanner creates a new AST-based scanner -func NewASTScanner() *ASTScanner { +func NewASTScanner(scanDirs []string) *ASTScanner { return &ASTScanner{ - fset: token.NewFileSet(), + fset: token.NewFileSet(), + scanDirs: scanDirs, } } @@ -104,12 +107,16 @@ func (s *ASTScanner) extractHandler(fn *ast.FuncDecl, pkg, filePath string) *Han return nil } + // Extract full package path from file path + fullPackagePath := s.extractFullPackagePath(filePath, s.scanDirs) + return &HandlerFunction{ - FunctionName: fn.Name.Name, - Package: pkg, - HandlerName: handlerName, - ReturnType: "error", - FilePath: filePath, + FunctionName: fn.Name.Name, + Package: pkg, + FullPackagePath: fullPackagePath, + HandlerName: handlerName, + ReturnType: "error", + FilePath: filePath, } } @@ -150,11 +157,12 @@ func (s *ASTScanner) extractRoute(fn *ast.FuncDecl, handler HandlerFunction) *Ro } return &RouteMapping{ - MethodName: fn.Name.Name, - Path: path, - HTTPMethod: method, - HandlerRef: s.generateHandlerRef(handler), - Package: handler.Package, + MethodName: fn.Name.Name, + Path: path, + HTTPMethod: method, + HandlerRef: s.generateHandlerRef(handler), + Package: handler.Package, + FullPackagePath: handler.FullPackagePath, } } } @@ -466,3 +474,49 @@ func (s *ASTScanner) getTypeString(expr ast.Expr) string { return "" } } + +// extractFullPackagePath extracts the complete package path from file path +// This method takes a file path and returns the relative path from the project root +// that represents the package path (e.g., "internal/domain/user" -> "domain/user") +func (s *ASTScanner) extractFullPackagePath(filePath string, scanDirs []string) string { + // Normalize the file path + absFilePath, err := filepath.Abs(filePath) + if err != nil { + return "" + } + + // Find the directory containing the file + fileDir := filepath.Dir(absFilePath) + + // Find which scan directory this file belongs to + for _, scanDir := range scanDirs { + absScanDir, err := filepath.Abs(scanDir) + if err != nil { + continue + } + + // Check if the file is within this scan directory + if strings.HasPrefix(fileDir, absScanDir) { + // Get the relative path from scan directory to file directory + relPath, err := filepath.Rel(absScanDir, fileDir) + if err != nil { + continue + } + + // Convert to forward slashes for consistency + relPath = filepath.ToSlash(relPath) + + // Remove "internal/" prefix if present, as it's typically not part of the import path + relPath = strings.TrimPrefix(relPath, "internal/") + + // Return the package path (empty if it's the root) + if relPath == "." || relPath == "" { + return "" + } + + return relPath + } + } + + return "" +} diff --git a/internal/scanner/scanner.go b/internal/scanner/scanner.go index 6c5135c..479a634 100644 --- a/internal/scanner/scanner.go +++ b/internal/scanner/scanner.go @@ -18,7 +18,7 @@ type Scanner struct { func NewScanner(cfg *config.Config) *Scanner { return &Scanner{ config: cfg, - astScanner: NewASTScanner(), + astScanner: NewASTScanner(cfg.Paths.ScanDirs), fileFilter: NewFileFilter(), } } diff --git a/internal/scanner/types.go b/internal/scanner/types.go index 314bd0f..d57a7d4 100644 --- a/internal/scanner/types.go +++ b/internal/scanner/types.go @@ -4,6 +4,7 @@ package scanner type HandlerFunction struct { FunctionName string // e.g., "GetUser" Package string // e.g., "user" + FullPackagePath string // e.g., "domain/user" (complete package path from file system) HandlerName string // e.g., "UserHandler" (interface name if using interface pattern) ImplementerName string // e.g., "HandlerImpl" (only for interface pattern) ReturnType string // Always "error" for Fiber handlers @@ -13,11 +14,12 @@ type HandlerFunction struct { // RouteMapping represents a @Router annotation mapping type RouteMapping struct { - MethodName string // e.g., "GetUser" - Path string // e.g., "/users/:id" - HTTPMethod string // e.g., "GET", "POST", "PUT", "DELETE" - HandlerRef string // e.g., "userHandler.GetUser" - Package string // Package name for import resolution + MethodName string // e.g., "GetUser" + Path string // e.g., "/users/:id" + HTTPMethod string // e.g., "GET", "POST", "PUT", "DELETE" + HandlerRef string // e.g., "userHandler.GetUser" + Package string // Package name for import resolution + FullPackagePath string // e.g., "domain/user" (complete package path from file system) } // ProviderFunction represents a Wire provider function From c3336526eed905b072341025ed661d0a1817c3fd Mon Sep 17 00:00:00 2001 From: just do it <52734504+wdcodecn@users.noreply.github.com> Date: Sun, 2 Nov 2025 13:19:46 +0800 Subject: [PATCH 3/3] refactor: clean up example project structure and remove unused e2e tests --- .gitignore | 1 + example/.air.toml | 44 - example/.gitignore | 0 example/.golangci.yml | 83 ++ example/.taskwignore | 66 - example/CLAUDE.md | 191 +++ example/Dockerfile | 0 example/README.md | 552 -------- example/Taskfile.yml | 111 +- example/cmd/generate/ent_generator.go | 0 example/cmd/server/main.go | 173 ++- example/configs/config.dev.toml | 0 example/configs/config.docker.toml | 0 example/configs/config.prod.toml | 0 example/docker-compose.yml | 188 +++ example/docs/docs.go | 1430 -------------------- example/ent/generate.go | 0 example/ent/schema/user.go | 0 example/go.mod | 98 +- example/go.sum | 182 --- example/internal/api/adapters.go | 49 - example/internal/api/server.go | 75 +- example/internal/api/wire.go | 16 +- example/internal/domain/user/handler.go | 0 example/internal/domain/user/repository.go | 0 example/internal/domain/user/service.go | 32 + example/internal/health/handler.go | 129 +- example/internal/health/repository.go | 22 - example/internal/health/service.go | 43 - example/internal/logger/service.go | 12 - example/internal/middleware/cors.go | 0 example/internal/middleware/logger.go | 0 example/internal/middleware/recovery.go | 0 example/internal/middleware/response.go | 0 example/internal/middleware/scalar.go | 0 example/internal/middleware/trace.go | 0 example/internal/middleware/validator.go | 0 example/internal/models/order.go | 74 - example/internal/models/product.go | 55 - example/internal/models/user.go | 40 - example/internal/order/handler.go | 346 ----- example/internal/order/repository.go | 211 --- example/internal/order/service.go | 285 ---- example/internal/pkg/config/config.go | 0 example/internal/pkg/db/db.go | 0 example/internal/pkg/db/sql_logger.go | 0 example/internal/pkg/errors/errors.go | 0 example/internal/pkg/logger/logger.go | 0 example/internal/pkg/types/types.go | 0 example/internal/product/handler.go | 313 ----- example/internal/product/repository.go | 221 --- example/internal/product/service.go | 212 --- example/internal/user/handler.go | 245 ---- example/internal/user/repository.go | 134 -- example/internal/user/service.go | 153 --- example/taskw.yaml | 2 +- example/templates/entity.template | 0 test/e2e/01_init_test.go | 269 ---- test/e2e/02_route_test.go | 574 -------- test/e2e/README.md | 192 --- test/e2e/utils.go | 17 - 61 files changed, 955 insertions(+), 5885 deletions(-) create mode 100644 example/.gitignore create mode 100644 example/.golangci.yml create mode 100644 example/CLAUDE.md create mode 100644 example/Dockerfile delete mode 100644 example/README.md create mode 100644 example/cmd/generate/ent_generator.go create mode 100644 example/configs/config.dev.toml create mode 100644 example/configs/config.docker.toml create mode 100644 example/configs/config.prod.toml create mode 100644 example/docker-compose.yml create mode 100644 example/ent/generate.go create mode 100644 example/ent/schema/user.go delete mode 100644 example/internal/api/adapters.go create mode 100644 example/internal/domain/user/handler.go create mode 100644 example/internal/domain/user/repository.go create mode 100644 example/internal/domain/user/service.go delete mode 100644 example/internal/health/repository.go delete mode 100644 example/internal/health/service.go delete mode 100644 example/internal/logger/service.go create mode 100644 example/internal/middleware/cors.go create mode 100644 example/internal/middleware/logger.go create mode 100644 example/internal/middleware/recovery.go create mode 100644 example/internal/middleware/response.go create mode 100644 example/internal/middleware/scalar.go create mode 100644 example/internal/middleware/trace.go create mode 100644 example/internal/middleware/validator.go delete mode 100644 example/internal/models/order.go delete mode 100644 example/internal/models/product.go delete mode 100644 example/internal/models/user.go delete mode 100644 example/internal/order/handler.go delete mode 100644 example/internal/order/repository.go delete mode 100644 example/internal/order/service.go create mode 100644 example/internal/pkg/config/config.go create mode 100644 example/internal/pkg/db/db.go create mode 100644 example/internal/pkg/db/sql_logger.go create mode 100644 example/internal/pkg/errors/errors.go create mode 100644 example/internal/pkg/logger/logger.go create mode 100644 example/internal/pkg/types/types.go delete mode 100644 example/internal/product/handler.go delete mode 100644 example/internal/product/repository.go delete mode 100644 example/internal/product/service.go delete mode 100644 example/internal/user/handler.go delete mode 100644 example/internal/user/repository.go delete mode 100644 example/internal/user/service.go create mode 100644 example/templates/entity.template delete mode 100644 test/e2e/01_init_test.go delete mode 100644 test/e2e/02_route_test.go delete mode 100644 test/e2e/README.md delete mode 100644 test/e2e/utils.go diff --git a/.gitignore b/.gitignore index 319f2dd..d3d3717 100644 --- a/.gitignore +++ b/.gitignore @@ -66,3 +66,4 @@ bin/ # Air tmp files tmp +.idea diff --git a/example/.air.toml b/example/.air.toml index 81acaca..e69de29 100644 --- a/example/.air.toml +++ b/example/.air.toml @@ -1,44 +0,0 @@ -root = "." -testdata_dir = "testdata" -tmp_dir = "tmp" - -[build] -args_bin = [] -bin = "./tmp/main" -cmd = "go build -o ./tmp/main ./cmd/server" -delay = 1000 -exclude_dir = ["assets", "tmp", "vendor", "testdata", "node_modules", "bin"] -exclude_file = [] -exclude_regex = ["_test.go", "_gen.go"] -exclude_unchanged = false -follow_symlink = false -full_bin = "" -include_dir = [] -include_ext = ["go", "tpl", "tmpl", "html"] -include_file = [] -kill_delay = "0s" -log = "build-errors.log" -poll = false -poll_interval = 0 -rerun = true -rerun_delay = 500 -send_interrupt = false -stop_on_error = false - -[color] -app = "" -build = "yellow" -main = "magenta" -runner = "green" -watcher = "cyan" - -[log] -main_only = false -time = false - -[misc] -clean_on_exit = false - -[screen] -clear_on_rebuild = false -keep_scroll = true diff --git a/example/.gitignore b/example/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/example/.golangci.yml b/example/.golangci.yml new file mode 100644 index 0000000..6358b7a --- /dev/null +++ b/example/.golangci.yml @@ -0,0 +1,83 @@ +run: + timeout: 5m + tests: false + +linters: + fast: true + enable: + # # --- 核心 bug 检查 --- + # - govet # Go 官方 vet 工具 + # - staticcheck # 静态分析,检查 bug 和废弃 API + # - errcheck # 检查未处理的错误 + # - gosimple # 代码简化建议 + # - ineffassign # 检测无效赋值 + # - typecheck # 类型检查 + # - unused # 检查未使用的代码 + + # # --- 代码质量 & 风格 --- + # - gocritic # 综合代码质量检查 + # - revive # 现代风格检查器 + # - goconst # 检查重复字符串(只对常量化需求强的项目有价值) + # - misspell # 拼写检查 + # - nolintlint # 检查 nolint 指令 + # - rowserrcheck # 检查数据库 rows 错误 + # - unconvert # 检查不必要的类型转换 + + # --- 可选 (根据项目需要开启) --- + # - gocyclo # 圈复杂度检查(容易吵,建议在 code review 抓) + # - nakedret # 裸返回检查(小函数可忍,长函数才需要) + # - noctx # http 请求缺少 context(看项目规范,常报噪音) + # - unparam # 未使用参数(接口常见,很多误报) + # - whitespace # 空白字符(基本可交给 gofumpt 处理) + + +# linters-settings: +# revive: +# rules: +# - name: exported +# disabled: true +# - name: package-comments +# disabled: true +# - name: line-length-limit +# disabled: true +# gocritic: +# enabled-tags: +# - diagnostic +# - experimental +# - opinionated +# - performance +# - style +# disabled-checks: +# - dupImport +# - ifElseChain +# - octalLiteral +# - whyNoLint +# - wrapperFunc +# - hugeParam +# - paramTypeCombine +# - commentedOutCode +# - nestingReduce +# - singleCaseSwitch +# - emptyStringTest +# gocyclo: +# min-complexity: 15 +# goconst: +# min-len: 3 +# min-occurrences: 3 +# nakedret: +# max-func-lines: 30 + +issues: + exclude-dirs: + - vendor + - ent + - mock + exclude-files: + - ".*_gen.go" + - ".*_mock.go" + +output: + formats: + - format: colored-line-number + print-issued-lines: true + print-linter-name: true \ No newline at end of file diff --git a/example/.taskwignore b/example/.taskwignore index e375d96..e69de29 100644 --- a/example/.taskwignore +++ b/example/.taskwignore @@ -1,66 +0,0 @@ -# Taskw Ignore Patterns -# This file tells taskw which files and directories to exclude from scanning - -# Test files -**/*_test.go -**/testdata/** -**/test/** -**/*_mock.go - -# Build artifacts -**/bin/** -**/build/** -**/dist/** -target/ - -# Dependencies -**/vendor/** -**/node_modules/** - -# Generated files (except the ones taskw generates) -**/*_gen.go -!routes_gen.go -!dependencies_gen.go -**/wire_gen.go - -# IDE and editor files -.vscode/ -.idea/ -**/*.swp -**/*.swo -**/*~ - -# OS files -.DS_Store -Thumbs.db - -# Temporary files -**/*.tmp -**/*.temp -**/*.log - -# Git -.git/ -.gitignore - -# Documentation (optional) -*.md -!README.md - -# Configuration files (optional) -*.yaml -*.yml -*.json -*.toml -!taskw.yaml - -# Main/cmd files that don't contain handlers -cmd/ -main.go - -# Models and shared types (no handlers here) -**/models/** -**/types/** -**/errors/** -**/utils/** -**/config/** diff --git a/example/CLAUDE.md b/example/CLAUDE.md new file mode 100644 index 0000000..37d33e0 --- /dev/null +++ b/example/CLAUDE.md @@ -0,0 +1,191 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This is a Go web API project built with modern tooling using Fiber, Ent ORM, Wire dependency injection, and includes code generation via taskw. The project follows a clean architecture pattern with clear separation between handlers, services, and repositories. + +## Essential Commands + +### Development +- `task dev` - Start development server with hot reload (uses Air) +- `task generate` - Generate all code (Ent entities, taskw routes/dependencies, Wire DI, Swagger docs) +- `task build` - Build the production binary +- `task test` - Run all tests +- `task setup` - Initial project setup (dependencies + code generation) +- `task clean` - Clean generated files and binaries +- `task fix` - Run golangci-lint with auto-fix + +### Environment-Specific Commands +- `task test` - Run in test environment +- `task staging` - Run in staging environment +- `task prod` - Run in production environment + +### Code Generation (Critical) +- `taskw generate all` - Generate routes and dependencies (must run after schema changes) +- `go run -mod=mod entgo.io/ent/cmd/ent generate ./ent/schema` - Generate Ent entities +- `wire ./internal/api` - Generate dependency injection code +- `swag init -g ./cmd/server/main.go -o ./docs` - Generate Swagger documentation + +### Database & Infrastructure +- `task check-local-db` - Check PostgreSQL database connection +- `task new-entity NAME=` - Create new Ent entity (e.g., `task new-entity NAME=Product`) + +### Testing & Performance +- `task test-concurrency` - Test high concurrency packages +- `task benchmark` - Run performance benchmarks +- `task stress-test` - Run stress test with high concurrency +- `task monitor` - Monitor system metrics +- `task example` - Run high concurrency example +- `task metrics` - Start metrics server only + +## Architecture + +### Core Technology Stack +- **Web Framework**: Fiber v2 (high-performance HTTP framework) +- **ORM**: Ent (Facebook's entity framework) +- **Dependency Injection**: Google Wire +- **Configuration**: Viper with TOML files +- **Logging**: Zap (structured JSON logging) +- **API Documentation**: Swagger +- **Hot Reload**: Air (development only) + +### Project Structure +``` +├── cmd/server/ # Application entry point +├── configs/ # Environment-specific TOML config files +├── ent/ # Ent generated code and schema definitions +├── internal/ +│ ├── api/ # Router, server setup, and Wire configuration +│ ├── middleware/ # HTTP middleware (CORS, logging, validation, etc.) +│ ├── pkg/ # Shared utilities (config, logger, types, errors) +│ ├── user/ # User domain module (handler, service, repository) +│ └── health/ # Health check endpoint +├── docs/ # Auto-generated Swagger documentation +└── Taskfile.yml # Task automation +``` + +### Generated Files (DO NOT EDIT MANUALLY) +- `internal/api/routes_gen.go` - Generated by taskw +- `internal/api/dependencies_gen.go` - Generated by taskw +- `internal/api/wire_gen.go` - Generated by Wire +- `ent/*.go` - Generated by Ent (except schema files) +- `docs/` directory - Generated by Swagger + +### Architecture Pattern +The project uses a 3-layer architecture within domain modules: +1. **Handler** - HTTP request/response handling, validation (`handler.go`) +2. **Service** - Business logic, data transformation, transaction management (`service.go`) +3. **Repository** - Data access layer, database operations (`repository.go`) + +### Domain Entities +The project includes the following Ent entities: +- **User** - Basic user management with name, email, age +- **Blockchain** - Blockchain network definitions +- **Collection** - NFT collections +- **NFT** - Individual NFT tokens +- **NFTHolder** - NFT ownership tracking + +## Configuration + +### Environment Configuration +Configuration uses TOML files in the `configs/` directory: +- `config.dev.toml` - Development (default) +- `config.prod.toml` - Production +- `config.test.toml` - Testing +- `config.staging.toml` - Staging +- `config.docker.toml` - Docker environment + +Set environment via `ENV` environment variable (defaults to "dev"). + +### Database Configuration +Currently configured for PostgreSQL in development: +```toml +[db] +driver = "postgres" +dsn = "postgres://postgres:postgres@localhost:5432/example?sslmode=disable" +``` + +## API Conventions + +### Route Format +- **Method**: All endpoints use POST +- **Path Pattern**: `/api/v{version}/{domain.action}` +- **Examples**: `/api/user.create`, `/api/user.list` + +### Request/Response Format +- **Content-Type**: `application/json` +- **Parameters**: All in JSON body +- **Pagination**: Uses `offset` and `limit` +- **Sorting**: Uses `field` and `asc` boolean + +### Response Structure +```json +{ + "status": "success|fail", + "code": 200, + "error": "", + "data": {} +} +``` + +## Development Workflow + +### Before Making Changes +1. Always run `task generate` after modifying: + - Ent schema files (`ent/schema/*.go`) + - API routes or handlers + - Wire dependency configurations + +### Adding New Endpoints +1. Create handler in appropriate module (e.g., `internal/user/handler.go`) +2. Add service logic in `service.go` +3. Add repository methods in `repository.go` if needed +4. Run `task generate` to update routes and dependencies +5. Add Swagger documentation comments to handlers + +### Adding New Domain Modules +1. Create new directory under `internal/` (e.g., `internal/product/`) +2. Create `handler.go`, `service.go`, and `repository.go` files +3. Follow existing patterns from `internal/user/` module +4. Run `task generate` to register new handlers automatically +5. Add Ent schema if database entities are needed + +### Database Changes +1. Modify schema in `ent/schema/*.go` +2. Run `task generate` to regenerate Ent code +3. Test migration behavior + +## Important Notes + +- **taskw Dependency**: This project relies on taskw for route and dependency generation +- **Wire Integration**: Dependency injection is handled by Google Wire +- **All POST Pattern**: The API uses POST for all endpoints (unconventional but project-specific) +- **Middleware Chain**: Request processing goes through CORS → Trace → Logger → Recovery → Validator → Response middlewares +- **Error Handling**: Uses custom error codes with 6-digit format (XXYYZZ) +- **Structured Logging**: All logs are JSON-formatted using Zap +- **Air Configuration**: Uses custom Air config (`.air.toml`) with port cleanup on restart +- **NFT Focus**: Project includes NFT-related entities and may be blockchain/Web3 oriented + +## Testing + +Run tests with: `task test` + +The project includes specific test commands for concurrency features: +- `task test-concurrency` - Test high concurrency packages +- `task benchmark` - Run performance benchmarks + +## Monitoring & Metrics + +- Health endpoint: `/health` +- Swagger docs: `/docs` +- The project includes built-in metrics and monitoring capabilities + +## Key Files to Understand + +- `cmd/server/main.go` - Application entry point with detailed startup logging +- `internal/api/wire.go` - Wire dependency injection configuration +- `internal/api/server.go` - Fiber app configuration +- `taskw.yaml` - taskw configuration for code generation +- `Taskfile.yml` - All available development tasks \ No newline at end of file diff --git a/example/Dockerfile b/example/Dockerfile new file mode 100644 index 0000000..e69de29 diff --git a/example/README.md b/example/README.md deleted file mode 100644 index 3be5899..0000000 --- a/example/README.md +++ /dev/null @@ -1,552 +0,0 @@ -# E-commerce API - Taskw Testing Example - -This is a comprehensive example e-commerce API that **requires taskw** to function. The project is structured to demonstrate how a real-world application should be built to rely entirely on taskw for route registration and dependency injection. - -## Important: This Project Won't Work Without Taskw - -🚨 **This example project will NOT compile or run until you generate the required code using taskw.** - -- The routes are not manually registered - taskw must scan `@Router` annotations and generate them -- The dependency injection is not manually wired - taskw must scan `Provide*` functions and generate Wire provider sets - -## What This Example Demonstrates - -This project showcases the complete taskw workflow: - -### 🎯 Handler Functions with @Router Annotations - -Every handler method includes proper Swaggo annotations: - -```go -// GetUsers retrieves all users -// @Summary Get all users -// @Description Get a list of all users in the system -// @Tags users -// @Accept json -// @Produce json -// @Success 200 {array} models.UserResponse -// @Failure 500 {object} map[string]string -// @Router /api/v1/users [get] -func (h *Handler) GetUsers(c *fiber.Ctx) error { - // implementation -} -``` - -### 📦 Provider Functions for Wire DI - -Every module includes provider functions: - -```go -// ProvideHandler creates a new user handler -func ProvideHandler(service *Service) *Handler { - return &Handler{service: service} -} - -// ProvideService creates a new user service -func ProvideService(repo *Repository) *Service { - return &Service{repo: repo} -} - -// ProvideRepository creates a new user repository -func ProvideRepository() *Repository { - return &Repository{users: make(map[uuid.UUID]*models.User)} -} -``` - -## Project Structure - -``` -example/ -├── cmd/ -│ └── server/ -│ └── main.go # Server entry point -├── internal/ -│ ├── api/ -│ │ ├── server.go # Server (routes generated by taskw) -│ │ ├── wire.go # Wire config (providers generated by taskw) -│ │ ├── adapters.go # Service adapters -│ │ ├── routes_gen.go # 🚨 GENERATED by taskw (placeholder) -│ │ └── dependencies_gen.go # 🚨 GENERATED by taskw (placeholder) -│ ├── models/ # Shared data models -│ │ ├── user.go -│ │ ├── product.go -│ │ └── order.go -│ ├── user/ # User module -│ │ ├── handler.go # 6 endpoints with @Router annotations -│ │ ├── service.go # Business logic + ProvideService -│ │ └── repository.go # Data layer + ProvideRepository -│ ├── product/ # Product module -│ │ ├── handler.go # 7 endpoints with @Router annotations -│ │ ├── service.go # Business logic + ProvideService -│ │ └── repository.go # Data layer + ProvideRepository -│ └── order/ # Order module -│ ├── handler.go # 6 endpoints with @Router annotations -│ ├── service.go # Business logic + ProvideService -│ └── repository.go # Data layer + ProvideRepository -├── .taskwignore # Taskw ignore patterns -├── taskw.yaml # Taskw configuration -├── go.mod -└── README.md -``` - -### Key Files - -- **`routes_gen.go`** - Generated by taskw from `@Router` annotations (currently placeholder) -- **`dependencies_gen.go`** - Generated by taskw from `Provide*` functions (currently placeholder) -- **`.taskwignore`** - Tells taskw which files to ignore during scanning -- **`taskw.yaml`** - Taskw configuration for scan directories and output - -## API Endpoints - -### Users (`/api/v1/users`) - -- `GET /` - Get all users -- `GET /{id}` - Get user by ID -- `POST /` - Create user -- `PUT /{id}` - Update user -- `DELETE /{id}` - Delete user -- `GET /by-email?email=` - Get user by email -- `GET /{user_id}/orders` - Get user's orders - -### Products (`/api/v1/products`) - -- `GET /` - Get all products (with category filter) -- `GET /{id}` - Get product by ID -- `POST /` - Create product -- `PUT /{id}` - Update product -- `DELETE /{id}` - Delete product -- `GET /{id}/stock?quantity=` - Check stock availability - -### Categories (`/api/v1/categories`) - -- `GET /` - Get all categories - -### Orders (`/api/v1/orders`) - -- `GET /` - Get all orders (with user/status filters) -- `GET /{id}` - Get order by ID -- `POST /?user_id=` - Create order -- `PUT /{id}/status` - Update order status -- `POST /{id}/cancel` - Cancel order - -## Step-by-Step Taskw Testing Workflow - -### 1. Prerequisites - -```bash -# Navigate to the example -cd example - -# Install dependencies -go mod tidy - -# Install taskw (replace with your actual taskw binary path) -# go install github.com/nkaewam/taskw@latest -``` - -### 2. Verify Project State (Before Taskw) - -The project should currently **fail to compile**: - -```bash -# This should fail because routes_gen.go and dependencies_gen.go are placeholders -go build cmd/server/main.go -# Expected: compilation errors about missing providers -``` - -### 3. Taskw Configuration - -The project comes with a pre-configured `taskw.yaml`: - -```yaml -version: "1.0" -project: - module: "github.com/example/ecommerce-api" -paths: - scan_dirs: - - "./internal/user" - - "./internal/product" - - "./internal/order" - output_dir: "./internal/api" -generation: - routes: - enabled: true - output_file: "routes_gen.go" - dependencies: - enabled: true - output_file: "dependencies_gen.go" -``` - -### 4. Scan and Preview - -```bash -# See what taskw discovers -taskw scan -``` - -Expected output: - -``` -🔍 Scanning codebase... -📋 Using ignore patterns from .taskwignore - -📊 Scan Results: - 🎯 Handlers found: 3 - 🛣️ Routes found: 19 - 📦 Providers found: 9 - 📄 Packages scanned: 3 - -🎯 Handlers: - - user.ProvideHandler -> *user.Handler - - product.ProvideHandler -> *product.Handler - - order.ProvideHandler -> *order.Handler - -🛣️ Routes: - - GetUsers: GET /api/v1/users - - GetUser: GET /api/v1/users/{id} - - CreateUser: POST /api/v1/users - - UpdateUser: PUT /api/v1/users/{id} - - DeleteUser: DELETE /api/v1/users/{id} - - GetUserByEmail: GET /api/v1/users/by-email - - GetProducts: GET /api/v1/products - - GetProduct: GET /api/v1/products/{id} - - CreateProduct: POST /api/v1/products - - UpdateProduct: PUT /api/v1/products/{id} - - DeleteProduct: DELETE /api/v1/products/{id} - - GetCategories: GET /api/v1/categories - - CheckStock: GET /api/v1/products/{id}/stock - - GetOrders: GET /api/v1/orders - - GetOrder: GET /api/v1/orders/{id} - - CreateOrder: POST /api/v1/orders - - UpdateOrderStatus: PUT /api/v1/orders/{id}/status - - CancelOrder: POST /api/v1/orders/{id}/cancel - - GetUserOrders: GET /api/v1/users/{user_id}/orders - -📦 Providers: - - user.ProvideRepository() -> *user.Repository - - user.ProvideService() -> *user.Service - - user.ProvideHandler() -> *user.Handler - - product.ProvideRepository() -> *product.Repository - - product.ProvideService() -> *product.Service - - product.ProvideHandler() -> *product.Handler - - order.ProvideRepository() -> *order.Repository - - order.ProvideService() -> *order.Service - - order.ProvideHandler() -> *order.Handler -``` - -### 5. Generate Code - -```bash -# Generate routes and dependencies -taskw generate - -# Or generate specific parts -taskw generate routes -taskw generate deps -``` - -This should create: - -- `internal/api/routes_gen.go` - Auto-generated route registration -- `internal/api/dependencies_gen.go` - Auto-generated Wire provider sets - -### 6. Update Wire Configuration - -After generation, update `internal/api/wire.go` to include the generated providers. - -Uncomment this line in the `ProviderSet`: - -```go -// Change this: -// GeneratedProviderSet, - -// To this: -GeneratedProviderSet, -``` - -The final `ProviderSet` should look like: - -```go -var ProviderSet = wire.NewSet( - // Infrastructure providers (manual) - provideLogger, - provideFiberApp, - - // Adapters (manual) - ProvideProductServiceAdapter, - ProvideUserServiceAdapter, - - // Server (manual) - ProvideServer, - - // Generated providers (from taskw) - GeneratedProviderSet, // Now uncommented! -) -``` - -### 7. Generate Wire Code - -```bash -# Generate Wire dependency injection -go generate ./... - -# Or manually -cd internal/api -wire -``` - -### 8. Verify Generated Code - -After taskw generation, the placeholder files should be replaced: - -- **`routes_gen.go`** - Should contain actual route registration code -- **`dependencies_gen.go`** - Should contain actual Wire provider set - -The project should now compile successfully! - -### 9. Test the Server - -```bash -# Build and run -go build -o bin/server cmd/server/main.go -./bin/server - -# Or with go run -go run cmd/server/main.go -``` - -## Testing the API - -### 1. Health Check - -```bash -curl http://localhost:3000/health -``` - -### 2. Create Test Data - -```bash -# Create a user -curl -X POST http://localhost:3000/api/v1/users \ - -H "Content-Type: application/json" \ - -d '{"email":"john@example.com","first_name":"John","last_name":"Doe"}' - -# Create a product -curl -X POST http://localhost:3000/api/v1/products \ - -H "Content-Type: application/json" \ - -d '{ - "name":"Gaming Laptop", - "description":"High-performance gaming laptop", - "price":1299.99, - "stock":5, - "category_id":"550e8400-e29b-41d4-a716-446655440001" - }' -``` - -### 3. Test Relationships - -```bash -# Create an order (replace with actual IDs from above) -curl -X POST "http://localhost:3000/api/v1/orders?user_id=" \ - -H "Content-Type: application/json" \ - -d '{ - "items":[ - {"product_id":"","quantity":2} - ] - }' - -# Update order status -curl -X PUT http://localhost:3000/api/v1/orders//status \ - -H "Content-Type: application/json" \ - -d '{"status":"confirmed"}' -``` - -### 4. Test Filters and Queries - -```bash -# Get products by category -curl "http://localhost:3000/api/v1/products?category=550e8400-e29b-41d4-a716-446655440001" - -# Get orders by user -curl "http://localhost:3000/api/v1/orders?user_id=" - -# Get orders by status -curl "http://localhost:3000/api/v1/orders?status=pending" - -# Check stock -curl "http://localhost:3000/api/v1/products//stock?quantity=3" -``` - -## What Taskw Should Generate - -### Expected Generated Code - -After running `taskw generate`, the placeholder files should be replaced with actual generated code. - -**Expected `routes_gen.go`:** - -```go -package api - -import "github.com/gofiber/fiber/v2" - -func (s *Server) RegisterRoutes(app *fiber.App) { - api := app.Group("/api/v1") - - // Auto-generated from @Router annotations - api.Get("/users", s.userHandler.GetUsers) - api.Get("/users/:id", s.userHandler.GetUser) - api.Post("/users", s.userHandler.CreateUser) - api.Put("/users/:id", s.userHandler.UpdateUser) - api.Delete("/users/:id", s.userHandler.DeleteUser) - api.Get("/users/by-email", s.userHandler.GetUserByEmail) - api.Get("/users/:user_id/orders", s.orderHandler.GetUserOrders) - - api.Get("/products", s.productHandler.GetProducts) - api.Get("/products/:id", s.productHandler.GetProduct) - api.Post("/products", s.productHandler.CreateProduct) - api.Put("/products/:id", s.productHandler.UpdateProduct) - api.Delete("/products/:id", s.productHandler.DeleteProduct) - api.Get("/products/:id/stock", s.productHandler.CheckStock) - - api.Get("/categories", s.productHandler.GetCategories) - - api.Get("/orders", s.orderHandler.GetOrders) - api.Get("/orders/:id", s.orderHandler.GetOrder) - api.Post("/orders", s.orderHandler.CreateOrder) - api.Put("/orders/:id/status", s.orderHandler.UpdateOrderStatus) - api.Post("/orders/:id/cancel", s.orderHandler.CancelOrder) - - s.logger.Info("All routes registered successfully") -} -``` - -**Expected `dependencies_gen.go`:** - -```go -package api - -import ( - "github.com/google/wire" - "github.com/example/ecommerce-api/internal/user" - "github.com/example/ecommerce-api/internal/product" - "github.com/example/ecommerce-api/internal/order" -) - -var GeneratedProviderSet = wire.NewSet( - // Auto-generated from Provide* functions - user.ProvideRepository, - user.ProvideService, - user.ProvideHandler, - product.ProvideRepository, - product.ProvideService, - product.ProvideHandler, - order.ProvideRepository, - order.ProvideService, - order.ProvideHandler, -) -``` - -## Features Tested - -This example tests taskw's ability to: - -✅ **Scan Multiple Modules** - User, Product, Order modules -✅ **Extract @Router Annotations** - 19 different endpoints -✅ **Extract Provider Functions** - 9 provider functions -✅ **Handle Different HTTP Methods** - GET, POST, PUT, DELETE -✅ **Parse Route Parameters** - Path params like `/{id}`, `/{user_id}` -✅ **Parse Query Parameters** - Query params in annotations -✅ **Handle Complex Routes** - Nested resources, multiple params -✅ **Generate Route Registration** - Fiber-specific syntax -✅ **Generate Wire Providers** - Wire.NewSet with all providers -✅ **Cross-Module Dependencies** - Order depends on User/Product services - -## Complete Testing Workflow - -### Before Taskw Generation - -```bash -cd example - -# Project should fail to build -go build cmd/server/main.go -# Error: routes_gen.go has placeholder content -# Error: dependencies_gen.go has incomplete provider set -``` - -### After Taskw Generation - -```bash -# 1. Scan what taskw finds -taskw scan - -# 2. Generate the code -taskw generate - -# 3. Update wire.go (uncomment GeneratedProviderSet) - -# 4. Generate wire code -go generate ./... - -# 5. Build should now succeed -go build cmd/server/main.go - -# 6. Run the server -./server -# OR -go run cmd/server/main.go -``` - -## Troubleshooting - -### Before Generation Issues - -**Problem**: `go build` fails with placeholder errors -**Solution**: This is expected! Run `taskw generate` first. - -### Taskw Not Finding Handlers - -- Check that files end with `handler.go` -- Ensure `Provide*` functions exist and return correct types -- Verify `@Router` annotations are properly formatted -- Check `.taskwignore` isn't excluding your files - -### After Generation Issues - -**Problem**: Still can't build after `taskw generate` -**Solutions**: - -- Check `taskw.yaml` has correct module path -- Ensure `scan_dirs` point to the right directories -- Verify output directory is writable -- Uncomment `GeneratedProviderSet` in `wire.go` - -**Problem**: Wire compilation issues -**Solutions**: - -- Run `go mod tidy` after generation -- Run `go generate ./...` to update wire_gen.go -- Check that all provider function signatures match -- Ensure interfaces are properly implemented (adapters.go) - -**Problem**: Server won't start -**Solutions**: - -- Check that Wire generated `wire_gen.go` exists -- Verify all imports are correct in generated files -- Check for circular dependencies in provider chain - -### Taskw Generation Issues - -**Problem**: Taskw generates empty or incorrect code -**Solutions**: - -- Verify `@Router` annotations follow exact format: `// @Router /path [method]` -- Ensure `Provide*` functions return pointer types (e.g., `*Handler`) -- Check that handler methods have correct signature: `func (h *Handler) Method(c *fiber.Ctx) error` -- Run `taskw scan` to debug what taskw is finding - ---- - -This example provides a comprehensive test case for taskw, covering all the patterns and edge cases you're likely to encounter in a real-world Go API project. diff --git a/example/Taskfile.yml b/example/Taskfile.yml index 497dda9..da599e1 100644 --- a/example/Taskfile.yml +++ b/example/Taskfile.yml @@ -1,16 +1,18 @@ version: '3' vars: - BINARY_NAME: ecommerce-api + BINARY_NAME: my-api MAIN_PATH: ./cmd/server AIR_CONFIG: .air.toml + ENV: 'test' + CONFIG_FILE: './configs/config.{{.ENV}}.toml' tasks: build: desc: Build the server binary deps: [generate] cmds: - - go build -o bin/{{.BINARY_NAME}} {{.MAIN_PATH}} + - go build -o bin/{{ .BINARY_NAME }} {{ .MAIN_PATH }} test: desc: Run tests @@ -18,17 +20,53 @@ tasks: cmds: - go test -v ./... + fix: + desc: Run fix + deps: [] + cmds: + - golangci-lint run --fix ./... + dev: - desc: Run development server with live reloading using air + desc: Run development server with live reloading using air + vars: + ENV: 'dev' deps: [generate, install-air] cmds: - - air -c {{.AIR_CONFIG}} + - ENV={{.ENV}} air -c {{ .AIR_CONFIG }} + + test: + desc: Run test environment + vars: + ENV: 'test' + deps: [generate] + cmds: + - ENV={{.ENV}} go run {{.MAIN_PATH}} + + staging: + desc: Run staging environment + vars: + ENV: 'staging' + deps: [generate] + cmds: + - ENV={{.ENV}} go run {{.MAIN_PATH}} + + prod: + desc: Run production environment + vars: + ENV: 'prod' + deps: [generate] + cmds: + - ENV={{.ENV}} go run {{.MAIN_PATH}} generate: desc: Generate code using taskw (includes swagger) and wire cmds: - - ../bin/taskw generate all + - go generate ./ent + - go run cmd/generate/ent_generator.go fiber-wire-ent + - taskw generate all - wire ./internal/api + - go fmt ./... + - golangci-lint run ./... swagger: desc: Generate Swagger documentation @@ -57,7 +95,7 @@ tasks: clean: desc: Clean generated files and binaries cmds: - - rm -f bin/{{.BINARY_NAME}} + - rm -f bin/{{ .BINARY_NAME }} - rm -f internal/api/*_gen.go - rm -rf docs/ @@ -71,4 +109,63 @@ tasks: desc: Run the server (production mode) deps: [build] cmds: - - ./bin/{{.BINARY_NAME}} + - ./bin/{{ .BINARY_NAME }} + + example: + desc: Run high concurrency example + cmds: + - go run examples/high_concurrency_example.go + + test-concurrency: + desc: Test high concurrency features + cmds: + - go test -v ./internal/pkg/atomic/... + - go test -v ./internal/pkg/ratelimit/... + - go test -v ./internal/pkg/errors/... + - go test -v ./internal/pkg/metrics/... + + benchmark: + desc: Run benchmarks for high concurrency packages + cmds: + - go test -bench=. -benchmem ./internal/pkg/atomic/... + - go test -bench=. -benchmem ./internal/pkg/ratelimit/... + - go test -bench=. -benchmem ./internal/pkg/errors/... + + metrics: + desc: Start metrics server only + cmds: + - go run -mod=mod cmd/metrics/main.go + + stress-test: + desc: Run stress test with high concurrency + cmds: + - | + echo "Starting stress test..." + for i in {1..10}; do + curl -X POST http://localhost:3000/api/user.list & + done + wait + echo "Stress test completed" + + monitor: + desc: Monitor system metrics + cmds: + - | + echo "Monitoring system metrics..." + while true; do + echo "=== $(date) ===" + curl -s http://localhost:9090/metrics | grep -E "(http_requests_total|user_created_total|goroutines_total)" + sleep 5 + done + + new-entity: + desc: Create new entity using ent CLI + cmds: + - | + if [ -z "$NAME" ]; then + echo "Usage: task new-entity NAME=" + echo "Example: task new-entity NAME=User" + exit 1 + fi + echo "Creating new entity: $NAME" + go run -mod=mod entgo.io/ent/cmd/ent new $NAME \ No newline at end of file diff --git a/example/cmd/generate/ent_generator.go b/example/cmd/generate/ent_generator.go new file mode 100644 index 0000000..e69de29 diff --git a/example/cmd/server/main.go b/example/cmd/server/main.go index f3388bf..e971bf6 100644 --- a/example/cmd/server/main.go +++ b/example/cmd/server/main.go @@ -9,19 +9,21 @@ import ( "syscall" "time" - "github.com/example/ecommerce-api/internal/api" - "github.com/gofiber/contrib/swagger" - "github.com/gofiber/fiber/v2" - "github.com/gofiber/fiber/v2/middleware/cors" - "github.com/gofiber/fiber/v2/middleware/logger" - "github.com/gofiber/fiber/v2/middleware/recover" + "{{.Module}}/internal/api" + "{{.Module}}/internal/middleware" + "{{.Module}}/internal/pkg/config" + "{{.Module}}/internal/pkg/logger" + "{{.Module}}/internal/pkg/types" + + // "{{.Module}}/docs" + // _ "{{.Module}}/docs" // swagger docs - _ "github.com/example/ecommerce-api/docs" // swagger docs + "github.com/gofiber/fiber/v2" ) -// @title E-commerce API +// @title my-api API // @version 1.0 -// @description A sample e-commerce API built with Go, Fiber, and Wire +// @description A Go API built with Fiber and Wire // @description Generated using taskw - Go API Code Generator // @termsOfService http://swagger.io/terms/ @@ -40,97 +42,140 @@ import ( // @externalDocs.url https://swagger.io/resources/open-api/ func main() { - fmt.Println("🚀 Starting E-commerce API...") - fmt.Println("📋 This example requires taskw to generate routes and dependencies") - fmt.Println("") + fmt.Println("Starting hello-taskw API...") + fmt.Println("This project requires taskw to generate routes and dependencies") - // Initialize the server using Wire (which uses taskw-generated providers) - router, err := api.InitializeRouter() + // Get environment info + env := os.Getenv("ENV") + if env == "" { + env = "dev" + } + fmt.Printf("Environment: %s\n", env) + + // Get config info + cfg := config.GetConfig() + if cfg != nil { + fmt.Printf("App: %s v%s\n", cfg.App.Name, cfg.App.Version) + fmt.Printf("Database: %s\n", cfg.DB.Driver) + fmt.Printf("Debug mode: %t\n", cfg.App.Debug) + fmt.Printf("Log level: %s\n", cfg.App.LogLevel) + } + // Use Wire-generated dependency injection container + fmt.Println("Initializing dependency injection container...") + router, err := api.InitializeRouter() if err != nil { - log.Fatalf("❌ Failed to initialize server: %v\n\n💡 Did you run 'taskw generate' to create the required code?", err) + log.Fatalf("❌ Failed to initialize server: %v\n\nDid you run 'taskw generate' to create the required code?", err) } - // Initialize Fiber app - app := api.ProvideFiberApp() - fmt.Println("✅ Server initialized successfully (taskw-generated code is working!)") + // Get app instance from router + app := router.GetApp() + fmt.Println("Fiber app instance obtained") + // Setup middleware - setupMiddleware(app) + fmt.Println("Setting up middleware...") + setupMiddleware(app, router) + fmt.Println("✅ Middleware setup completed") // Setup routes (this will use taskw-generated route registration) + fmt.Println("Setting up routes...") setupRoutes(app, router) + fmt.Println("✅ Routes setup completed") // Start server with graceful shutdown - startServer(app) + startServer(app, router) } -func setupMiddleware(app *fiber.App) { - // CORS middleware - app.Use(cors.New(cors.Config{ - AllowOrigins: "*", - AllowHeaders: "Origin, Content-Type, Accept, Authorization", - AllowMethods: "GET, POST, PUT, DELETE, OPTIONS", - })) - - // Logger middleware - app.Use(logger.New(logger.Config{ - Format: "[${time}] ${status} - ${method} ${path} - ${latency}\n", - })) - - // Recover middleware - app.Use(recover.New()) +// setupMiddleware configures global middleware +func setupMiddleware(app *fiber.App, _ *api.Router) { + fmt.Println(" Adding Recovery middleware...") + app.Use(middleware.RecoveryMiddleware()) + + fmt.Println(" Adding CORS middleware...") + app.Use(middleware.CORS()) + + // core middleware + fmt.Println(" Adding Trace middleware...") + app.Use(middleware.TraceMiddleware()) // trace_id + + fmt.Println(" Adding Response middleware...") + app.Use(middleware.ResponseMiddleware()) // unified JSON response + + fmt.Println(" Adding Validator middleware...") + app.Use(middleware.ValidatorMiddleware()) // request validation + + fmt.Println(" Creating development logger...") + devLogger := logger.NewDevelopmentLogger() + fmt.Println(" Adding Logger middleware...") + app.Use(middleware.LoggerMiddleware(devLogger)) } +// setupRoutes registers routes and Swagger documentation func setupRoutes(app *fiber.App, router *api.Router) { - cfg := swagger.Config{ - BasePath: "", - FilePath: "./docs/swagger.json", - Path: "docs", - Title: "Swagger API Docs", - } - app.Use(swagger.New(cfg)) - - // Health check endpoint is now generated via taskw from health module + // Scalar documentation + scalarConfig := middleware.DefaultScalarConfig() + scalarConfig.Title = "my-api API Documentation" + scalarConfig.Description = "A Go API built with Fiber and Wire - Generated using taskw" + scalarConfig.Version = "1.0" + middleware.SetupScalarRoutes(app, scalarConfig) // API routes - this uses taskw-generated route registration - fmt.Println("📡 Registering API routes (generated by taskw)...") + fmt.Println("Registering API routes (generated by taskw)...") router.RegisterHandlers() + // // Print all registered routes + // fmt.Println("Registered routes:") + // routes := app.GetRoutes(false) + // apiRoutes := 0 + // for _, route := range routes { + // if route.Path != "/" && route.Path != "/health" { + // fmt.Printf(" %s %s\n", route.Method, route.Path) + // apiRoutes++ + // } + // } + // fmt.Printf("Total API routes registered: %d\n", apiRoutes) + // 404 handler app.Use(func(c *fiber.Ctx) error { - return c.Status(404).JSON(fiber.Map{ - "error": "Not Found", - "message": fmt.Sprintf("Route '%s' not found", c.Path()), - "note": "Available routes were generated by taskw", - }) + response := types.Response{ + Code: 1, + Msg: fmt.Sprintf("Route '%s' not found", c.Path()), + Data: nil, + } + return c.JSON(response) }) - } -func startServer(app *fiber.App) { +// startServer starts HTTP service +func startServer(app *fiber.App, _ *api.Router) { // Channel to listen for interrupt signal c := make(chan os.Signal, 1) signal.Notify(c, os.Interrupt, syscall.SIGTERM) // Start server in a goroutine go func() { - port := os.Getenv("PORT") - if port == "" { - port = "3000" + // get port from config, prioritize environment variable + port := "3000" + if envPort := os.Getenv("PORT"); envPort != "" { + port = envPort + } else { + // get port from config singleton + cfg := config.GetConfig() + if cfg != nil && cfg.Server.Port > 0 { + port = fmt.Sprintf("%d", cfg.Server.Port) + } } - fmt.Printf("🌐 Server starting on port %s\n", port) - fmt.Println("📖 API Documentation:") - fmt.Printf(" Swagger: http://localhost:%s/docs\n", port) + fmt.Printf("Server starting on port %s\n", port) + fmt.Printf("API Documentation:\n") + fmt.Printf(" Scalar: http://localhost:%s/docs\n", port) fmt.Printf(" Health: http://localhost:%s/health\n", port) - fmt.Printf(" Users: http://localhost:%s/api/v1/users\n", port) - fmt.Printf(" Products: http://localhost:%s/api/v1/products\n", port) - fmt.Printf(" Orders: http://localhost:%s/api/v1/orders\n", port) + fmt.Printf(" API Base: http://localhost:%s/api/\n", port) fmt.Println("") - fmt.Println("🧪 Test the API with the examples in README.md") + fmt.Println("✅ Ready to accept requests!") fmt.Println("") if err := app.Listen(":" + port); err != nil { @@ -140,8 +185,8 @@ func startServer(app *fiber.App) { // Wait for interrupt signal <-c - fmt.Println("🛑 Received shutdown signal...") - fmt.Println("🔄 Gracefully shutting down...") + fmt.Println("Received shutdown signal...") + fmt.Println("Gracefully shutting down...") // Create a deadline for shutdown _, cancel := context.WithTimeout(context.Background(), 30*time.Second) diff --git a/example/configs/config.dev.toml b/example/configs/config.dev.toml new file mode 100644 index 0000000..e69de29 diff --git a/example/configs/config.docker.toml b/example/configs/config.docker.toml new file mode 100644 index 0000000..e69de29 diff --git a/example/configs/config.prod.toml b/example/configs/config.prod.toml new file mode 100644 index 0000000..e69de29 diff --git a/example/docker-compose.yml b/example/docker-compose.yml new file mode 100644 index 0000000..ff969ae --- /dev/null +++ b/example/docker-compose.yml @@ -0,0 +1,188 @@ +version: '3.8' + +services: + # 开发环境 + app-dev: + build: . + container_name: hello-taskw-dev + ports: + - "3000:3000" + environment: + - ENV=dev + volumes: + - ./configs:/root/configs + - ./logs:/root/logs + depends_on: + - postgres-dev + networks: + - hello-taskw-network + profiles: + - dev + + # 测试环境 + app-test: + build: . + container_name: hello-taskw-test + ports: + - "3000:3000" + environment: + - ENV=test + volumes: + - ./configs:/root/configs + - ./logs:/root/logs + depends_on: + - postgres-test + networks: + - hello-taskw-network + profiles: + - test + + # 预发布环境 + app-staging: + build: . + container_name: hello-taskw-staging + ports: + - "3002:3000" + environment: + - ENV=staging + volumes: + - ./configs:/root/configs + - ./logs:/root/logs + depends_on: + - postgres-staging + networks: + - hello-taskw-network + profiles: + - staging + + # 生产环境 + app-prod: + build: . + container_name: hello-taskw-prod + ports: + - "3003:3000" + environment: + - ENV=prod + volumes: + - ./configs:/root/configs + - ./logs:/root/logs + depends_on: + - postgres-prod + networks: + - hello-taskw-network + profiles: + - prod + restart: unless-stopped + + + # 开发环境 PostgreSQL + postgres-dev: + image: postgres:15-alpine + container_name: postgres-dev + environment: + POSTGRES_DB: hello_taskw_dev + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + ports: + - "5432:5432" + volumes: + - postgres-dev-data:/var/lib/postgresql/data + networks: + - hello-taskw-network + profiles: + - dev + + # 测试环境 PostgreSQL + postgres-test: + image: postgres:15-alpine + container_name: postgres-test + environment: + POSTGRES_DB: hello_taskw_test + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + ports: + - "5433:5432" + volumes: + - postgres-test-data:/var/lib/postgresql/data + networks: + - hello-taskw-network + profiles: + - test + + # 预发布环境 PostgreSQL + postgres-staging: + image: postgres:15-alpine + container_name: postgres-staging + environment: + POSTGRES_DB: hello_taskw_staging + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + ports: + - "5434:5432" + volumes: + - postgres-staging-data:/var/lib/postgresql/data + networks: + - hello-taskw-network + profiles: + - staging + + # 生产环境 PostgreSQL + postgres-prod: + image: postgres:15-alpine + container_name: postgres-prod + environment: + POSTGRES_DB: hello_taskw_prod + POSTGRES_USER: postgres + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + ports: + - "5435:5432" + volumes: + - postgres-prod-data:/var/lib/postgresql/data + networks: + - hello-taskw-network + profiles: + - prod + restart: unless-stopped + + # Redis (可选) + redis: + image: redis:7-alpine + container_name: redis + ports: + - "6379:6379" + volumes: + - redis-data:/data + networks: + - hello-taskw-network + profiles: + - staging + - prod + + # Nginx 反向代理 (生产环境) + nginx: + image: nginx:alpine + container_name: nginx + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf + - ./ssl:/etc/nginx/ssl + depends_on: + - app-prod + networks: + - hello-taskw-network + profiles: + - prod + restart: unless-stopped + +volumes: + postgres-dev-data: + postgres-test-data: + postgres-staging-data: + postgres-prod-data: + redis-data: + +networks: + hello-taskw-network: + driver: bridge diff --git a/example/docs/docs.go b/example/docs/docs.go index d02245a..e69de29 100644 --- a/example/docs/docs.go +++ b/example/docs/docs.go @@ -1,1430 +0,0 @@ -// Package docs Code generated by swaggo/swag. DO NOT EDIT -package docs - -import "github.com/swaggo/swag" - -const docTemplate = `{ - "schemes": {{ marshal .Schemes }}, - "swagger": "2.0", - "info": { - "description": "{{escape .Description}}", - "title": "{{.Title}}", - "termsOfService": "http://swagger.io/terms/", - "contact": { - "name": "API Support", - "url": "http://www.example.com/support", - "email": "support@example.com" - }, - "license": { - "name": "MIT", - "url": "https://opensource.org/licenses/MIT" - }, - "version": "{{.Version}}" - }, - "host": "{{.Host}}", - "basePath": "{{.BasePath}}", - "paths": { - "/api/v1/categories": { - "get": { - "description": "Get a list of all product categories", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "products" - ], - "summary": "Get all categories", - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/models.Category" - } - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - } - } - }, - "/api/v1/orders": { - "get": { - "description": "Get orders with optional filtering by user or status", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "orders" - ], - "summary": "Get orders", - "parameters": [ - { - "type": "string", - "description": "Filter by user ID", - "name": "user_id", - "in": "query" - }, - { - "type": "string", - "description": "Filter by status (pending, confirmed, shipped, delivered, cancelled)", - "name": "status", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/models.OrderResponse" - } - } - }, - "400": { - "description": "Bad Request", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - } - }, - "post": { - "description": "Create a new order for a user", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "orders" - ], - "summary": "Create a new order", - "parameters": [ - { - "type": "string", - "description": "User ID", - "name": "user_id", - "in": "query", - "required": true - }, - { - "description": "Order creation data", - "name": "order", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/models.CreateOrderRequest" - } - } - ], - "responses": { - "201": { - "description": "Created", - "schema": { - "$ref": "#/definitions/models.OrderResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - } - } - }, - "/api/v1/orders/{id}": { - "get": { - "description": "Get a specific order by its ID", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "orders" - ], - "summary": "Get order by ID", - "parameters": [ - { - "type": "string", - "description": "Order ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/models.OrderResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "404": { - "description": "Not Found", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - } - } - }, - "/api/v1/orders/{id}/cancel": { - "post": { - "description": "Cancel an existing order", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "orders" - ], - "summary": "Cancel order", - "parameters": [ - { - "type": "string", - "description": "Order ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/models.OrderResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "404": { - "description": "Not Found", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - } - } - }, - "/api/v1/orders/{id}/status": { - "put": { - "description": "Update the status of an existing order", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "orders" - ], - "summary": "Update order status", - "parameters": [ - { - "type": "string", - "description": "Order ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "Status update data", - "name": "status", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/models.UpdateOrderStatusRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/models.OrderResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "404": { - "description": "Not Found", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - } - } - }, - "/api/v1/products": { - "get": { - "description": "Get a list of all products in the system", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "products" - ], - "summary": "Get all products", - "parameters": [ - { - "type": "string", - "description": "Filter by category ID", - "name": "category", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/models.ProductResponse" - } - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - } - }, - "post": { - "description": "Create a new product in the system", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "products" - ], - "summary": "Create a new product", - "parameters": [ - { - "description": "Product creation data", - "name": "product", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/models.CreateProductRequest" - } - } - ], - "responses": { - "201": { - "description": "Created", - "schema": { - "$ref": "#/definitions/models.ProductResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - } - } - }, - "/api/v1/products/{id}": { - "get": { - "description": "Get a specific product by its ID", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "products" - ], - "summary": "Get product by ID", - "parameters": [ - { - "type": "string", - "description": "Product ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/models.ProductResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "404": { - "description": "Not Found", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - } - }, - "put": { - "description": "Update an existing product's information", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "products" - ], - "summary": "Update product", - "parameters": [ - { - "type": "string", - "description": "Product ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "Product update data", - "name": "product", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/models.UpdateProductRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/models.ProductResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "404": { - "description": "Not Found", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - } - }, - "delete": { - "description": "Delete a product from the system", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "products" - ], - "summary": "Delete product", - "parameters": [ - { - "type": "string", - "description": "Product ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "204": { - "description": "No Content" - }, - "400": { - "description": "Bad Request", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "404": { - "description": "Not Found", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - } - } - }, - "/api/v1/products/{id}/stock": { - "get": { - "description": "Check if enough stock is available for a product", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "products" - ], - "summary": "Check product stock", - "parameters": [ - { - "type": "string", - "description": "Product ID", - "name": "id", - "in": "path", - "required": true - }, - { - "type": "integer", - "description": "Quantity to check", - "name": "quantity", - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "object", - "additionalProperties": { - "type": "boolean" - } - } - }, - "400": { - "description": "Bad Request", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "404": { - "description": "Not Found", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - } - } - }, - "/api/v1/users": { - "get": { - "description": "Get a list of all users in the system", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "users" - ], - "summary": "Get all users", - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/models.UserResponse" - } - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - } - }, - "post": { - "description": "Create a new user in the system", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "users" - ], - "summary": "Create a new user", - "parameters": [ - { - "description": "User creation data", - "name": "user", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/models.CreateUserRequest" - } - } - ], - "responses": { - "201": { - "description": "Created", - "schema": { - "$ref": "#/definitions/models.UserResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "409": { - "description": "Conflict", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - } - } - }, - "/api/v1/users/by-email": { - "get": { - "description": "Get a specific user by their email address", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "users" - ], - "summary": "Get user by email", - "parameters": [ - { - "type": "string", - "description": "User email", - "name": "email", - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/models.UserResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "404": { - "description": "Not Found", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - } - } - }, - "/api/v1/users/{id}": { - "get": { - "description": "Get a specific user by their ID", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "users" - ], - "summary": "Get user by ID", - "parameters": [ - { - "type": "string", - "description": "User ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/models.UserResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "404": { - "description": "Not Found", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - } - }, - "put": { - "description": "Update an existing user's information", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "users" - ], - "summary": "Update user", - "parameters": [ - { - "type": "string", - "description": "User ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "User update data", - "name": "user", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/models.UpdateUserRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/models.UserResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "404": { - "description": "Not Found", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "409": { - "description": "Conflict", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - } - }, - "delete": { - "description": "Delete a user from the system", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "users" - ], - "summary": "Delete user", - "parameters": [ - { - "type": "string", - "description": "User ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "204": { - "description": "No Content" - }, - "400": { - "description": "Bad Request", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "404": { - "description": "Not Found", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - } - } - }, - "/api/v1/users/{user_id}/orders": { - "get": { - "description": "Get all orders for a specific user", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "orders" - ], - "summary": "Get user orders", - "parameters": [ - { - "type": "string", - "description": "User ID", - "name": "user_id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/models.OrderResponse" - } - } - }, - "400": { - "description": "Bad Request", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "404": { - "description": "Not Found", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - } - } - }, - "/health": { - "get": { - "description": "Get the current health status of the API", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "health" - ], - "summary": "Get system health", - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "object", - "additionalProperties": true - } - } - } - } - } - }, - "definitions": { - "models.Category": { - "type": "object", - "properties": { - "created_at": { - "type": "string" - }, - "id": { - "type": "string" - }, - "name": { - "type": "string" - } - } - }, - "models.CreateOrderItemRequest": { - "type": "object", - "required": [ - "product_id", - "quantity" - ], - "properties": { - "product_id": { - "type": "string" - }, - "quantity": { - "type": "integer" - } - } - }, - "models.CreateOrderRequest": { - "type": "object", - "required": [ - "items" - ], - "properties": { - "items": { - "type": "array", - "minItems": 1, - "items": { - "$ref": "#/definitions/models.CreateOrderItemRequest" - } - } - } - }, - "models.CreateProductRequest": { - "type": "object", - "required": [ - "category_id", - "name", - "price" - ], - "properties": { - "category_id": { - "type": "string" - }, - "description": { - "type": "string", - "maxLength": 500 - }, - "name": { - "type": "string", - "maxLength": 100, - "minLength": 2 - }, - "price": { - "type": "number" - }, - "stock": { - "type": "integer", - "minimum": 0 - } - } - }, - "models.CreateUserRequest": { - "type": "object", - "required": [ - "email", - "first_name", - "last_name" - ], - "properties": { - "email": { - "type": "string" - }, - "first_name": { - "type": "string", - "maxLength": 50, - "minLength": 2 - }, - "last_name": { - "type": "string", - "maxLength": 50, - "minLength": 2 - } - } - }, - "models.OrderItemResponse": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "product": { - "$ref": "#/definitions/models.ProductResponse" - }, - "product_id": { - "type": "string" - }, - "quantity": { - "type": "integer" - }, - "unit_price": { - "type": "number" - } - } - }, - "models.OrderResponse": { - "type": "object", - "properties": { - "created_at": { - "type": "string" - }, - "id": { - "type": "string" - }, - "items": { - "type": "array", - "items": { - "$ref": "#/definitions/models.OrderItemResponse" - } - }, - "status": { - "$ref": "#/definitions/models.OrderStatus" - }, - "total_price": { - "type": "number" - }, - "user_id": { - "type": "string" - } - } - }, - "models.OrderStatus": { - "type": "string", - "enum": [ - "pending", - "confirmed", - "shipped", - "delivered", - "cancelled" - ], - "x-enum-varnames": [ - "OrderStatusPending", - "OrderStatusConfirmed", - "OrderStatusShipped", - "OrderStatusDelivered", - "OrderStatusCancelled" - ] - }, - "models.ProductResponse": { - "type": "object", - "properties": { - "category_id": { - "type": "string" - }, - "created_at": { - "type": "string" - }, - "description": { - "type": "string" - }, - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "price": { - "type": "number" - }, - "stock": { - "type": "integer" - } - } - }, - "models.UpdateOrderStatusRequest": { - "type": "object", - "required": [ - "status" - ], - "properties": { - "status": { - "enum": [ - "pending", - "confirmed", - "shipped", - "delivered", - "cancelled" - ], - "allOf": [ - { - "$ref": "#/definitions/models.OrderStatus" - } - ] - } - } - }, - "models.UpdateProductRequest": { - "type": "object", - "properties": { - "category_id": { - "type": "string" - }, - "description": { - "type": "string", - "maxLength": 500 - }, - "name": { - "type": "string", - "maxLength": 100, - "minLength": 2 - }, - "price": { - "type": "number" - }, - "stock": { - "type": "integer", - "minimum": 0 - } - } - }, - "models.UpdateUserRequest": { - "type": "object", - "properties": { - "email": { - "type": "string" - }, - "first_name": { - "type": "string", - "maxLength": 50, - "minLength": 2 - }, - "last_name": { - "type": "string", - "maxLength": 50, - "minLength": 2 - } - } - }, - "models.UserResponse": { - "type": "object", - "properties": { - "created_at": { - "type": "string" - }, - "email": { - "type": "string" - }, - "first_name": { - "type": "string" - }, - "id": { - "type": "string" - }, - "last_name": { - "type": "string" - } - } - } - }, - "securityDefinitions": { - "BasicAuth": { - "type": "basic" - } - }, - "externalDocs": { - "description": "OpenAPI", - "url": "https://swagger.io/resources/open-api/" - } -}` - -// SwaggerInfo holds exported Swagger Info so clients can modify it -var SwaggerInfo = &swag.Spec{ - Version: "1.0", - Host: "localhost:3000", - BasePath: "", - Schemes: []string{}, - Title: "E-commerce API", - Description: "A sample e-commerce API built with Go, Fiber, and Wire\nGenerated using taskw - Go API Code Generator", - InfoInstanceName: "swagger", - SwaggerTemplate: docTemplate, - LeftDelim: "{{", - RightDelim: "}}", -} - -func init() { - swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) -} diff --git a/example/ent/generate.go b/example/ent/generate.go new file mode 100644 index 0000000..e69de29 diff --git a/example/ent/schema/user.go b/example/ent/schema/user.go new file mode 100644 index 0000000..e69de29 diff --git a/example/go.mod b/example/go.mod index 1d68be9..1c461ea 100644 --- a/example/go.mod +++ b/example/go.mod @@ -1,49 +1,83 @@ -module github.com/example/ecommerce-api +module {{.Module}} -go 1.23.0 - -toolchain go1.24.1 +go 1.25.0 require ( - github.com/gofiber/contrib/swagger v1.3.0 + entgo.io/ent v0.14.5 + github.com/go-playground/validator/v10 v10.27.0 + github.com/go-sql-driver/mysql v1.9.3 github.com/gofiber/fiber/v2 v2.52.9 - github.com/google/uuid v1.6.0 - github.com/google/wire v0.5.0 + github.com/google/wire v0.7.0 + github.com/lib/pq v1.10.9 + github.com/mattn/go-sqlite3 v1.14.17 + github.com/shirou/gopsutil/v3 v3.24.5 + github.com/spf13/viper v1.20.1 github.com/swaggo/swag v1.16.6 - go.uber.org/zap v1.26.0 + go.uber.org/zap v1.21.0 ) require ( + ariga.io/atlas v0.32.1-0.20250325101103-175b25e1c1b9 // indirect + filippo.io/edwards25519 v1.1.0 // indirect github.com/KyleBanks/depth v1.2.1 // indirect - github.com/andybalholm/brotli v1.2.0 // indirect - github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect - github.com/go-openapi/analysis v0.21.4 // indirect - github.com/go-openapi/errors v0.20.4 // indirect - github.com/go-openapi/jsonpointer v0.21.2 // indirect - github.com/go-openapi/jsonreference v0.21.0 // indirect - github.com/go-openapi/loads v0.21.2 // indirect - github.com/go-openapi/runtime v0.26.2 // indirect - github.com/go-openapi/spec v0.21.0 // indirect - github.com/go-openapi/strfmt v0.21.8 // indirect - github.com/go-openapi/swag v0.23.1 // indirect - github.com/go-openapi/validate v0.22.3 // indirect + github.com/agext/levenshtein v1.2.3 // indirect + github.com/andybalholm/brotli v1.1.0 // indirect + github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect + github.com/bmatcuk/doublestar v1.3.4 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/fsnotify/fsnotify v1.8.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.8 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/go-openapi/inflect v0.19.0 // indirect + github.com/go-openapi/jsonpointer v0.20.0 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/spec v0.20.11 // indirect + github.com/go-openapi/swag v0.22.4 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-viper/mapstructure/v2 v2.2.1 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/hashicorp/hcl/v2 v2.18.1 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/klauspost/compress v1.18.0 // indirect - github.com/mailru/easyjson v0.9.0 // indirect - github.com/mattn/go-colorable v0.1.14 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/oklog/ulid v1.3.1 // indirect - github.com/rivo/uniseg v0.4.7 // indirect + github.com/mitchellh/go-wordwrap v1.0.1 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/rogpeppe/go-internal v1.11.0 // indirect + github.com/sagikazarmark/locafero v0.7.0 // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.12.0 // indirect + github.com/spf13/cast v1.7.1 // indirect + github.com/spf13/pflag v1.0.6 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasthttp v1.65.0 // indirect - go.mongodb.org/mongo-driver v1.13.1 // indirect + github.com/valyala/fasthttp v1.51.0 // indirect + github.com/valyala/tcplisten v1.0.0 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + github.com/zclconf/go-cty v1.14.4 // indirect + github.com/zclconf/go-cty-yaml v1.1.0 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/goleak v1.3.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/mod v0.27.0 // indirect - golang.org/x/sync v0.16.0 // indirect - golang.org/x/sys v0.35.0 // indirect - golang.org/x/tools v0.36.0 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect + golang.org/x/crypto v0.38.0 // indirect + golang.org/x/mod v0.23.0 // indirect + golang.org/x/net v0.40.0 // indirect + golang.org/x/sync v0.14.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/text v0.25.0 // indirect + golang.org/x/tools v0.30.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/example/go.sum b/example/go.sum index 8eb2189..e69de29 100644 --- a/example/go.sum +++ b/example/go.sum @@ -1,182 +0,0 @@ -github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= -github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= -github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= -github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= -github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= -github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= -github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/go-openapi/analysis v0.21.4 h1:ZDFLvSNxpDaomuCueM0BlSXxpANBlFYiBvr+GXrvIHc= -github.com/go-openapi/analysis v0.21.4/go.mod h1:4zQ35W4neeZTqh3ol0rv/O8JBbka9QyAgQRPp9y3pfo= -github.com/go-openapi/errors v0.20.2/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= -github.com/go-openapi/errors v0.20.4 h1:unTcVm6PispJsMECE3zWgvG4xTiKda1LIR5rCRWLG6M= -github.com/go-openapi/errors v0.20.4/go.mod h1:Z3FlZ4I8jEGxjUK+bugx3on2mIAk4txuAOhlsB1FSgk= -github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonpointer v0.21.2 h1:AqQaNADVwq/VnkCmQg6ogE+M3FOsKTytwges0JdwVuA= -github.com/go-openapi/jsonpointer v0.21.2/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= -github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= -github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= -github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= -github.com/go-openapi/loads v0.21.2 h1:r2a/xFIYeZ4Qd2TnGpWDIQNcP80dIaZgf704za8enro= -github.com/go-openapi/loads v0.21.2/go.mod h1:Jq58Os6SSGz0rzh62ptiu8Z31I+OTHqmULx5e/gJbNw= -github.com/go-openapi/runtime v0.26.2 h1:elWyB9MacRzvIVgAZCBJmqTi7hBzU0hlKD4IvfX0Zl0= -github.com/go-openapi/runtime v0.26.2/go.mod h1:O034jyRZ557uJKzngbMDJXkcKJVzXJiymdSfgejrcRw= -github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= -github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY= -github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= -github.com/go-openapi/strfmt v0.21.3/go.mod h1:k+RzNO0Da+k3FrrynSNN8F7n/peCmQQqbbXjtDfvmGg= -github.com/go-openapi/strfmt v0.21.8 h1:VYBUoKYRLAlgKDrIxR/I0lKrztDQ0tuTDrbhLVP8Erg= -github.com/go-openapi/strfmt v0.21.8/go.mod h1:adeGTkxE44sPyLk0JV235VQAO/ZXUr8KAzYjclFs3ew= -github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= -github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= -github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= -github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= -github.com/go-openapi/validate v0.22.3 h1:KxG9mu5HBRYbecRb37KRCihvGGtND2aXziBAv0NNfyI= -github.com/go-openapi/validate v0.22.3/go.mod h1:kVxh31KbfsxU8ZyoHaDbLBWU5CnMdqBUEtadQ2G4d5M= -github.com/gofiber/contrib/swagger v1.3.0 h1:J1InCTPUW/DzDlG+QwWcD5QZ4W9HlyCRHLZjKKVZd+g= -github.com/gofiber/contrib/swagger v1.3.0/go.mod h1:zlZljpjIz1VhKR25+Inxl7WaOkgyM10nITUFXn6sV5A= -github.com/gofiber/fiber/v2 v2.52.9 h1:YjKl5DOiyP3j0mO61u3NTmK7or8GzzWzCFzkboyP5cw= -github.com/gofiber/fiber/v2 v2.52.9/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= -github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/subcommands v1.0.1 h1:/eqq+otEXm5vhfBrbREPCSVQbvofip6kIz+mX5TUH7k= -github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/wire v0.5.0 h1:I7ELFeVBr3yfPIcc8+MWvrjk+3VjbcSzoXm3JVa+jD8= -github.com/google/wire v0.5.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU= -github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= -github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= -github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= -github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= -github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= -github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= -github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= -github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= -github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= -github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= -github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.65.0 h1:j/u3uzFEGFfRxw79iYzJN+TteTJwbYkru9uDp3d0Yf8= -github.com/valyala/fasthttp v1.65.0/go.mod h1:P/93/YkKPMsKSnATEeELUCkG8a7Y+k99uxNHVbKINr4= -github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= -github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= -github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= -github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= -github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= -github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= -github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= -github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.mongodb.org/mongo-driver v1.10.0/go.mod h1:wsihk0Kdgv8Kqu1Anit4sfK+22vSFbUrAVEYRhCXrA8= -go.mongodb.org/mongo-driver v1.13.1 h1:YIc7HTYsKndGK4RFzJ3covLz1byri52x0IoMB0Pt/vk= -go.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwDtFu/Z9+eo= -go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= -go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= -go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= -go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= -go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= -golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190422233926-fe54fb35175b/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= -golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/example/internal/api/adapters.go b/example/internal/api/adapters.go deleted file mode 100644 index c1ba927..0000000 --- a/example/internal/api/adapters.go +++ /dev/null @@ -1,49 +0,0 @@ -package api - -import ( - "github.com/example/ecommerce-api/internal/models" - "github.com/example/ecommerce-api/internal/order" - "github.com/example/ecommerce-api/internal/product" - "github.com/example/ecommerce-api/internal/user" - "github.com/google/uuid" -) - -// ProductServiceAdapter adapts product.Service to order.ProductService interface -type ProductServiceAdapter struct { - service *product.Service -} - -// ProvideProductServiceAdapter creates a new adapter -func ProvideProductServiceAdapter(service *product.Service) order.ProductService { - return &ProductServiceAdapter{service: service} -} - -func (a *ProductServiceAdapter) GetProduct(id uuid.UUID) (*models.ProductResponse, error) { - return a.service.GetProduct(id) -} - -func (a *ProductServiceAdapter) CheckStock(id uuid.UUID, quantity int) (bool, error) { - return a.service.CheckStock(id, quantity) -} - -func (a *ProductServiceAdapter) ReserveStock(id uuid.UUID, quantity int) error { - return a.service.ReserveStock(id, quantity) -} - -func (a *ProductServiceAdapter) ReleaseStock(id uuid.UUID, quantity int) error { - return a.service.ReleaseStock(id, quantity) -} - -// UserServiceAdapter adapts user.Service to order.UserService interface -type UserServiceAdapter struct { - service *user.Service -} - -// ProvideUserServiceAdapter creates a new adapter -func ProvideUserServiceAdapter(service *user.Service) order.UserService { - return &UserServiceAdapter{service: service} -} - -func (a *UserServiceAdapter) GetUser(id uuid.UUID) (*models.UserResponse, error) { - return a.service.GetUser(id) -} diff --git a/example/internal/api/server.go b/example/internal/api/server.go index 3c135de..7287b77 100644 --- a/example/internal/api/server.go +++ b/example/internal/api/server.go @@ -1,23 +1,68 @@ package api import ( + "sync" + "time" + + "{{.Module}}/internal/pkg/config" + "github.com/gofiber/fiber/v2" ) -var app = fiber.New(fiber.Config{ - AppName: "E-commerce API", - ErrorHandler: func(c *fiber.Ctx, err error) error { - code := fiber.StatusInternalServerError - if e, ok := err.(*fiber.Error); ok { - code = e.Code +var ( + appInstance *fiber.App + appOnce sync.Once +) + +// ProvideFiberApp provides Fiber application singleton instance +func ProvideFiberApp(cfg *config.Config) *fiber.App { + appOnce.Do(func() { + appInstance = newFiberApp(cfg) + }) + return appInstance +} + +// newFiberApp builds Fiber application based on configuration +func newFiberApp(cfg *config.Config) *fiber.App { + var ( + appName string + readTimeout time.Duration + writeTimeout time.Duration + idleTimeout time.Duration + ) + + if cfg != nil { + if cfg.App.Name != "" { + appName = cfg.App.Name } - return c.Status(code).JSON(fiber.Map{ - "error": err.Error(), - }) - }, -}) - -// ProvideFiberApp creates a new Fiber application -func ProvideFiberApp() *fiber.App { - return app + if cfg.Server.ReadTimeoutSec > 0 { + readTimeout = time.Duration(cfg.Server.ReadTimeoutSec) * time.Second + } + if cfg.Server.WriteTimeoutSec > 0 { + writeTimeout = time.Duration(cfg.Server.WriteTimeoutSec) * time.Second + } + if cfg.Server.IdleTimeoutSec > 0 { + idleTimeout = time.Duration(cfg.Server.IdleTimeoutSec) * time.Second + } + } + + return fiber.New(fiber.Config{ + AppName: appName, + ReadTimeout: readTimeout, + WriteTimeout: writeTimeout, + IdleTimeout: idleTimeout, + ErrorHandler: func(c *fiber.Ctx, err error) error { + code := fiber.StatusInternalServerError + if e, ok := err.(*fiber.Error); ok { + code = e.Code + } + return c.Status(code).JSON(fiber.Map{ + "error": err.Error(), + }) + }, + }) +} + +func (ar *Router) GetApp() *fiber.App { + return ar.app } diff --git a/example/internal/api/wire.go b/example/internal/api/wire.go index 3aef843..1b9d5d2 100644 --- a/example/internal/api/wire.go +++ b/example/internal/api/wire.go @@ -1,21 +1,29 @@ //go:build wireinject +// +build wireinject package api import ( + "{{.Module}}/internal/pkg/config" + "{{.Module}}/internal/pkg/logger" + "github.com/google/wire" ) -// ProviderSet will be augmented by taskw generated dependencies -// This only contains infrastructure providers - taskw will add the rest +// ProviderSet contains infrastructure providers var ProviderSet = wire.NewSet( + // config + config.ProvideConfig, - // Manual providers (If any) + // logger + logger.NewDevelopment, + logger.NewDevelopmentLogger, - // Generated providers added by taskw + // generated providers GeneratedProviderSet, ) +// InitializeRouter initializes Router dependencies func InitializeRouter() (*Router, error) { wire.Build(ProviderSet) return &Router{}, nil diff --git a/example/internal/domain/user/handler.go b/example/internal/domain/user/handler.go new file mode 100644 index 0000000..e69de29 diff --git a/example/internal/domain/user/repository.go b/example/internal/domain/user/repository.go new file mode 100644 index 0000000..e69de29 diff --git a/example/internal/domain/user/service.go b/example/internal/domain/user/service.go new file mode 100644 index 0000000..c024acf --- /dev/null +++ b/example/internal/domain/user/service.go @@ -0,0 +1,32 @@ +package user + +import ( + "context" + + "{{.Module}}/ent" + "{{.Module}}/ent/user" + . "{{.Module}}/internal/pkg/errors" + "{{.Module}}/internal/pkg/logger" +) + +type Service interface { + GetUser(ctx context.Context, id int) (*ent.User, error) + Repository +} + +type ServiceImpl struct { + Repository + logger *logger.Logger +} + +func ProvideService(repo Repository, logger *logger.Logger) Service { + return &ServiceImpl{Repository: repo, logger: logger} +} + +func (s *ServiceImpl) GetUser(ctx context.Context, id int) (*ent.User, error) { + u, err := s.FindOne(ctx, user.IDEQ(id)) + if err != nil { + return nil, ErrNotFound + } + return u, nil +} diff --git a/example/internal/health/handler.go b/example/internal/health/handler.go index 602611f..58e7a01 100644 --- a/example/internal/health/handler.go +++ b/example/internal/health/handler.go @@ -1,39 +1,126 @@ package health import ( + "context" + "runtime" "time" + "{{.Module}}/internal/middleware" + "{{.Module}}/internal/pkg/config" + "{{.Module}}/internal/pkg/db" + "github.com/gofiber/fiber/v2" ) -// Handler handles HTTP requests for health operations -type Handler struct { - service *Service -} +// Handler handles health check requests +type Handler struct{} + +// 记录应用启动时间 +var startTime = time.Now() // ProvideHandler creates a new health handler -func ProvideHandler(service *Service) *Handler { - return &Handler{ - service: service, - } +func ProvideHandler() *Handler { + return &Handler{} } -// GetHealth retrieves system health status -// @Summary Get system health -// @Description Get the current health status of the API -// @Tags health +// @Summary Health check +// @Description Get the health status of the API with system information +// @Tags System Management // @Accept json // @Produce json // @Success 200 {object} map[string]interface{} -// @Router /health [get] +// @Router /api/system/health [get] func (h *Handler) GetHealth(c *fiber.Ctx) error { - health := h.service.GetHealth() - - return c.JSON(fiber.Map{ - "status": health.Status, - "timestamp": time.Now().Unix(), - "service": health.Service, - "uptime": health.Uptime, - "note": "Routes generated by taskw", + return middleware.SuccessResponse(c, fiber.Map{ + "status": "healthy", + "timestamp": time.Now().Format(time.RFC3339), + "uptime": time.Since(startTime).String(), + "goroutines": runtime.NumGoroutine(), + "memory_mb": h.getMemoryMB(), + "database": h.getDatabaseStatus(), + "app": h.getAppInfo(), }) } + +// getMemoryMB 获取内存使用量(MB) +func (h *Handler) getMemoryMB() int { + var m runtime.MemStats + runtime.ReadMemStats(&m) + return int(m.Alloc / 1024 / 1024) +} + +// getDatabaseStatus 获取数据库状态 +func (h *Handler) getDatabaseStatus() fiber.Map { + client := db.GetDBClient() + if client == nil { + return fiber.Map{ + "status": "disconnected", + "error": "client not initialized", + } + } + + // 测试数据库连接 + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + start := time.Now() + _, err := client.User.Query().Count(ctx) + duration := time.Since(start) + + if err != nil { + return fiber.Map{ + "status": "error", + "error": err.Error(), + "duration": duration.Milliseconds(), + } + } + + // 获取连接池信息 + cfg := config.GetConfig() + poolInfo := fiber.Map{} + if cfg != nil { + // 获取当前连接池统计信息 + stats := db.GetDBStats() + poolInfo = fiber.Map{ + "max_open": cfg.DB.MaxOpen, + "max_idle": cfg.DB.MaxIdle, + "max_lifetime": cfg.DB.ConnMaxLifetimeSec, + "max_idle_time": cfg.DB.ConnMaxIdleTimeSec, + } + + if stats != nil { + poolInfo["current"] = fiber.Map{ + "open": stats.OpenConnections, + "in_use": stats.InUse, + "idle": stats.Idle, + "wait": stats.WaitCount, + "wait_duration": stats.WaitDuration.Milliseconds(), + } + } + } + + return fiber.Map{ + "status": "connected", + "duration": duration.Milliseconds(), + "pool": poolInfo, + } +} + +// getAppInfo 获取应用信息 +func (h *Handler) getAppInfo() fiber.Map { + cfg := config.GetConfig() + + info := fiber.Map{ + "name": "unknown", + "version": "unknown", + "env": "unknown", + } + + if cfg != nil { + info["name"] = cfg.App.Name + info["version"] = cfg.App.Version + info["env"] = cfg.App.Env + } + + return info +} diff --git a/example/internal/health/repository.go b/example/internal/health/repository.go deleted file mode 100644 index 30143bf..0000000 --- a/example/internal/health/repository.go +++ /dev/null @@ -1,22 +0,0 @@ -package health - -// Repository handles health data operations -type Repository struct { - // Could be expanded to check database, cache, external services, etc. -} - -// ProvideRepository creates a new health repository -func ProvideRepository() *Repository { - return &Repository{} -} - -// CheckSystemHealth checks if all system components are healthy -func (r *Repository) CheckSystemHealth() bool { - // For now, always return healthy - // This could be extended to check: - // - Database connectivity - // - External service health - // - Memory/CPU usage - // - File system health - return true -} diff --git a/example/internal/health/service.go b/example/internal/health/service.go deleted file mode 100644 index 2a451ba..0000000 --- a/example/internal/health/service.go +++ /dev/null @@ -1,43 +0,0 @@ -package health - -import ( - "time" -) - -// HealthStatus represents the health status of the system -type HealthStatus struct { - Status string `json:"status"` - Service string `json:"service"` - Uptime time.Duration `json:"uptime"` -} - -// Service handles health business logic -type Service struct { - repo *Repository - startTime time.Time -} - -// ProvideService creates a new health service -func ProvideService(repo *Repository) *Service { - return &Service{ - repo: repo, - startTime: time.Now(), - } -} - -// GetHealth returns the current health status of the system -func (s *Service) GetHealth() *HealthStatus { - // Check system components status - isHealthy := s.repo.CheckSystemHealth() - - status := "healthy" - if !isHealthy { - status = "unhealthy" - } - - return &HealthStatus{ - Status: status, - Service: "ecommerce-api", - Uptime: time.Since(s.startTime), - } -} diff --git a/example/internal/logger/service.go b/example/internal/logger/service.go deleted file mode 100644 index 8765d0c..0000000 --- a/example/internal/logger/service.go +++ /dev/null @@ -1,12 +0,0 @@ -package logger - -import "go.uber.org/zap" - -// ProvideLogger creates a new zap logger for development -func ProvideLogger() (*zap.Logger, error) { - logger, err := zap.NewDevelopment() - if err != nil { - return nil, err - } - return logger, nil -} diff --git a/example/internal/middleware/cors.go b/example/internal/middleware/cors.go new file mode 100644 index 0000000..e69de29 diff --git a/example/internal/middleware/logger.go b/example/internal/middleware/logger.go new file mode 100644 index 0000000..e69de29 diff --git a/example/internal/middleware/recovery.go b/example/internal/middleware/recovery.go new file mode 100644 index 0000000..e69de29 diff --git a/example/internal/middleware/response.go b/example/internal/middleware/response.go new file mode 100644 index 0000000..e69de29 diff --git a/example/internal/middleware/scalar.go b/example/internal/middleware/scalar.go new file mode 100644 index 0000000..e69de29 diff --git a/example/internal/middleware/trace.go b/example/internal/middleware/trace.go new file mode 100644 index 0000000..e69de29 diff --git a/example/internal/middleware/validator.go b/example/internal/middleware/validator.go new file mode 100644 index 0000000..e69de29 diff --git a/example/internal/models/order.go b/example/internal/models/order.go deleted file mode 100644 index da10e4b..0000000 --- a/example/internal/models/order.go +++ /dev/null @@ -1,74 +0,0 @@ -package models - -import ( - "time" - - "github.com/google/uuid" -) - -// Order represents an order in the system -type Order struct { - ID uuid.UUID `json:"id"` - UserID uuid.UUID `json:"user_id"` - Status OrderStatus `json:"status"` - TotalPrice float64 `json:"total_price"` - Items []OrderItem `json:"items"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} - -// OrderStatus represents the status of an order -type OrderStatus string - -const ( - OrderStatusPending OrderStatus = "pending" - OrderStatusConfirmed OrderStatus = "confirmed" - OrderStatusShipped OrderStatus = "shipped" - OrderStatusDelivered OrderStatus = "delivered" - OrderStatusCancelled OrderStatus = "cancelled" -) - -// OrderItem represents an item in an order -type OrderItem struct { - ID uuid.UUID `json:"id"` - OrderID uuid.UUID `json:"order_id"` - ProductID uuid.UUID `json:"product_id"` - Quantity int `json:"quantity"` - UnitPrice float64 `json:"unit_price"` - Product *Product `json:"product,omitempty"` -} - -// CreateOrderRequest represents the request payload for creating an order -type CreateOrderRequest struct { - Items []CreateOrderItemRequest `json:"items" validate:"required,min=1"` -} - -// CreateOrderItemRequest represents an item in a create order request -type CreateOrderItemRequest struct { - ProductID uuid.UUID `json:"product_id" validate:"required"` - Quantity int `json:"quantity" validate:"required,gt=0"` -} - -// UpdateOrderStatusRequest represents the request payload for updating order status -type UpdateOrderStatusRequest struct { - Status OrderStatus `json:"status" validate:"required,oneof=pending confirmed shipped delivered cancelled"` -} - -// OrderResponse represents the response payload for order operations -type OrderResponse struct { - ID uuid.UUID `json:"id"` - UserID uuid.UUID `json:"user_id"` - Status OrderStatus `json:"status"` - TotalPrice float64 `json:"total_price"` - Items []OrderItemResponse `json:"items"` - CreatedAt time.Time `json:"created_at"` -} - -// OrderItemResponse represents an order item in responses -type OrderItemResponse struct { - ID uuid.UUID `json:"id"` - ProductID uuid.UUID `json:"product_id"` - Quantity int `json:"quantity"` - UnitPrice float64 `json:"unit_price"` - Product ProductResponse `json:"product"` -} diff --git a/example/internal/models/product.go b/example/internal/models/product.go deleted file mode 100644 index 2e1ad86..0000000 --- a/example/internal/models/product.go +++ /dev/null @@ -1,55 +0,0 @@ -package models - -import ( - "time" - - "github.com/google/uuid" -) - -// Product represents a product in the system -type Product struct { - ID uuid.UUID `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - Price float64 `json:"price"` - Stock int `json:"stock"` - CategoryID uuid.UUID `json:"category_id"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} - -// CreateProductRequest represents the request payload for creating a product -type CreateProductRequest struct { - Name string `json:"name" validate:"required,min=2,max=100"` - Description string `json:"description" validate:"max=500"` - Price float64 `json:"price" validate:"required,gt=0"` - Stock int `json:"stock" validate:"gte=0"` - CategoryID uuid.UUID `json:"category_id" validate:"required"` -} - -// UpdateProductRequest represents the request payload for updating a product -type UpdateProductRequest struct { - Name *string `json:"name,omitempty" validate:"omitempty,min=2,max=100"` - Description *string `json:"description,omitempty" validate:"omitempty,max=500"` - Price *float64 `json:"price,omitempty" validate:"omitempty,gt=0"` - Stock *int `json:"stock,omitempty" validate:"omitempty,gte=0"` - CategoryID *uuid.UUID `json:"category_id,omitempty"` -} - -// ProductResponse represents the response payload for product operations -type ProductResponse struct { - ID uuid.UUID `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - Price float64 `json:"price"` - Stock int `json:"stock"` - CategoryID uuid.UUID `json:"category_id"` - CreatedAt time.Time `json:"created_at"` -} - -// Category represents a product category -type Category struct { - ID uuid.UUID `json:"id"` - Name string `json:"name"` - CreatedAt time.Time `json:"created_at"` -} diff --git a/example/internal/models/user.go b/example/internal/models/user.go deleted file mode 100644 index 373eb56..0000000 --- a/example/internal/models/user.go +++ /dev/null @@ -1,40 +0,0 @@ -package models - -import ( - "time" - - "github.com/google/uuid" -) - -// User represents a user in the system -type User struct { - ID uuid.UUID `json:"id"` - Email string `json:"email"` - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} - -// CreateUserRequest represents the request payload for creating a user -type CreateUserRequest struct { - Email string `json:"email" validate:"required,email"` - FirstName string `json:"first_name" validate:"required,min=2,max=50"` - LastName string `json:"last_name" validate:"required,min=2,max=50"` -} - -// UpdateUserRequest represents the request payload for updating a user -type UpdateUserRequest struct { - Email *string `json:"email,omitempty" validate:"omitempty,email"` - FirstName *string `json:"first_name,omitempty" validate:"omitempty,min=2,max=50"` - LastName *string `json:"last_name,omitempty" validate:"omitempty,min=2,max=50"` -} - -// UserResponse represents the response payload for user operations -type UserResponse struct { - ID uuid.UUID `json:"id"` - Email string `json:"email"` - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - CreatedAt time.Time `json:"created_at"` -} diff --git a/example/internal/order/handler.go b/example/internal/order/handler.go deleted file mode 100644 index 480f1e1..0000000 --- a/example/internal/order/handler.go +++ /dev/null @@ -1,346 +0,0 @@ -package order - -import ( - "github.com/example/ecommerce-api/internal/models" - "github.com/gofiber/fiber/v2" - "github.com/google/uuid" -) - -// Handler handles HTTP requests for order operations -type Handler struct { - service *Service -} - -// ProvideHandler creates a new order handler -func ProvideHandler(service *Service) *Handler { - return &Handler{ - service: service, - } -} - -// GetOrders retrieves orders with optional filters -// @Summary Get orders -// @Description Get orders with optional filtering by user or status -// @Tags orders -// @Accept json -// @Produce json -// @Param user_id query string false "Filter by user ID" -// @Param status query string false "Filter by status (pending, confirmed, shipped, delivered, cancelled)" -// @Success 200 {array} models.OrderResponse -// @Failure 400 {object} map[string]string -// @Failure 500 {object} map[string]string -// @Router /api/v1/orders [get] -func (h *Handler) GetOrders(c *fiber.Ctx) error { - userIDStr := c.Query("user_id") - statusStr := c.Query("status") - - // Filter by user ID - if userIDStr != "" { - userID, err := uuid.Parse(userIDStr) - if err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "invalid user ID format", - }) - } - - orders, err := h.service.GetOrdersByUser(userID) - if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ - "error": err.Error(), - }) - } - - return c.JSON(orders) - } - - // Filter by status - if statusStr != "" { - status := models.OrderStatus(statusStr) - if !isValidOrderStatus(status) { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "invalid status. Valid values: pending, confirmed, shipped, delivered, cancelled", - }) - } - - orders, err := h.service.GetOrdersByStatus(status) - if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ - "error": err.Error(), - }) - } - - return c.JSON(orders) - } - - // Get all orders - orders, err := h.service.GetAllOrders() - if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ - "error": err.Error(), - }) - } - - return c.JSON(orders) -} - -// GetOrder retrieves an order by ID -// @Summary Get order by ID -// @Description Get a specific order by its ID -// @Tags orders -// @Accept json -// @Produce json -// @Param id path string true "Order ID" -// @Success 200 {object} models.OrderResponse -// @Failure 400 {object} map[string]string -// @Failure 404 {object} map[string]string -// @Failure 500 {object} map[string]string -// @Router /api/v1/orders/{id} [get] -func (h *Handler) GetOrder(c *fiber.Ctx) error { - idStr := c.Params("id") - if idStr == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "order ID is required", - }) - } - - id, err := uuid.Parse(idStr) - if err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "invalid order ID format", - }) - } - - order, err := h.service.GetOrder(id) - if err != nil { - return c.Status(fiber.StatusNotFound).JSON(fiber.Map{ - "error": err.Error(), - }) - } - - return c.JSON(order) -} - -// CreateOrder creates a new order -// @Summary Create a new order -// @Description Create a new order for a user -// @Tags orders -// @Accept json -// @Produce json -// @Param user_id query string true "User ID" -// @Param order body models.CreateOrderRequest true "Order creation data" -// @Success 201 {object} models.OrderResponse -// @Failure 400 {object} map[string]string -// @Failure 500 {object} map[string]string -// @Router /api/v1/orders [post] -func (h *Handler) CreateOrder(c *fiber.Ctx) error { - userIDStr := c.Query("user_id") - if userIDStr == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "user_id query parameter is required", - }) - } - - userID, err := uuid.Parse(userIDStr) - if err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "invalid user ID format", - }) - } - - var req models.CreateOrderRequest - if err := c.BodyParser(&req); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "invalid request body", - }) - } - - order, err := h.service.CreateOrder(userID, &req) - if err != nil { - if contains(err.Error(), "insufficient stock") || contains(err.Error(), "invalid") { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": err.Error(), - }) - } - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ - "error": err.Error(), - }) - } - - return c.Status(fiber.StatusCreated).JSON(order) -} - -// UpdateOrderStatus updates the status of an order -// @Summary Update order status -// @Description Update the status of an existing order -// @Tags orders -// @Accept json -// @Produce json -// @Param id path string true "Order ID" -// @Param status body models.UpdateOrderStatusRequest true "Status update data" -// @Success 200 {object} models.OrderResponse -// @Failure 400 {object} map[string]string -// @Failure 404 {object} map[string]string -// @Failure 500 {object} map[string]string -// @Router /api/v1/orders/{id}/status [put] -func (h *Handler) UpdateOrderStatus(c *fiber.Ctx) error { - idStr := c.Params("id") - if idStr == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "order ID is required", - }) - } - - id, err := uuid.Parse(idStr) - if err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "invalid order ID format", - }) - } - - var req models.UpdateOrderStatusRequest - if err := c.BodyParser(&req); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "invalid request body", - }) - } - - if !isValidOrderStatus(req.Status) { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "invalid status. Valid values: pending, confirmed, shipped, delivered, cancelled", - }) - } - - order, err := h.service.UpdateOrderStatus(id, &req) - if err != nil { - if contains(err.Error(), "not found") { - return c.Status(fiber.StatusNotFound).JSON(fiber.Map{ - "error": err.Error(), - }) - } - if contains(err.Error(), "invalid") { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": err.Error(), - }) - } - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ - "error": err.Error(), - }) - } - - return c.JSON(order) -} - -// CancelOrder cancels an order -// @Summary Cancel order -// @Description Cancel an existing order -// @Tags orders -// @Accept json -// @Produce json -// @Param id path string true "Order ID" -// @Success 200 {object} models.OrderResponse -// @Failure 400 {object} map[string]string -// @Failure 404 {object} map[string]string -// @Failure 500 {object} map[string]string -// @Router /api/v1/orders/{id}/cancel [post] -func (h *Handler) CancelOrder(c *fiber.Ctx) error { - idStr := c.Params("id") - if idStr == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "order ID is required", - }) - } - - id, err := uuid.Parse(idStr) - if err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "invalid order ID format", - }) - } - - if err := h.service.CancelOrder(id); err != nil { - if contains(err.Error(), "not found") { - return c.Status(fiber.StatusNotFound).JSON(fiber.Map{ - "error": err.Error(), - }) - } - if contains(err.Error(), "invalid") { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": err.Error(), - }) - } - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ - "error": err.Error(), - }) - } - - return c.JSON(fiber.Map{ - "message": "order cancelled successfully", - }) -} - -// GetUserOrders retrieves all orders for a specific user -// @Summary Get user orders -// @Description Get all orders for a specific user -// @Tags orders -// @Accept json -// @Produce json -// @Param user_id path string true "User ID" -// @Success 200 {array} models.OrderResponse -// @Failure 400 {object} map[string]string -// @Failure 404 {object} map[string]string -// @Failure 500 {object} map[string]string -// @Router /api/v1/users/{user_id}/orders [get] -func (h *Handler) GetUserOrders(c *fiber.Ctx) error { - userIDStr := c.Params("user_id") - if userIDStr == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "user ID is required", - }) - } - - userID, err := uuid.Parse(userIDStr) - if err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "invalid user ID format", - }) - } - - orders, err := h.service.GetOrdersByUser(userID) - if err != nil { - if contains(err.Error(), "invalid user") { - return c.Status(fiber.StatusNotFound).JSON(fiber.Map{ - "error": err.Error(), - }) - } - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ - "error": err.Error(), - }) - } - - return c.JSON(orders) -} - -// Helper functions -func contains(s, substr string) bool { - return len(s) >= len(substr) && (s[len(s)-len(substr):] == substr || - (len(s) > len(substr) && s[:len(substr)] == substr) || - (len(s) > len(substr)*2 && s[len(s)/2-len(substr)/2:len(s)/2+len(substr)/2] == substr)) -} - -func isValidOrderStatus(status models.OrderStatus) bool { - validStatuses := []models.OrderStatus{ - models.OrderStatusPending, - models.OrderStatusConfirmed, - models.OrderStatusShipped, - models.OrderStatusDelivered, - models.OrderStatusCancelled, - } - - for _, valid := range validStatuses { - if status == valid { - return true - } - } - - return false -} diff --git a/example/internal/order/repository.go b/example/internal/order/repository.go deleted file mode 100644 index 3c8f09a..0000000 --- a/example/internal/order/repository.go +++ /dev/null @@ -1,211 +0,0 @@ -package order - -import ( - "fmt" - "sync" - "time" - - "github.com/example/ecommerce-api/internal/models" - "github.com/google/uuid" -) - -// Repository handles order data persistence -type Repository struct { - mu sync.RWMutex - orders map[uuid.UUID]*models.Order - orderItems map[uuid.UUID]*models.OrderItem -} - -// ProvideRepository creates a new order repository -func ProvideRepository() *Repository { - return &Repository{ - orders: make(map[uuid.UUID]*models.Order), - orderItems: make(map[uuid.UUID]*models.OrderItem), - } -} - -// CreateOrder creates a new order with items -func (r *Repository) CreateOrder(order *models.Order) error { - r.mu.Lock() - defer r.mu.Unlock() - - order.ID = uuid.New() - order.CreatedAt = time.Now() - order.UpdatedAt = time.Now() - - // Create order items - for i := range order.Items { - order.Items[i].ID = uuid.New() - order.Items[i].OrderID = order.ID - r.orderItems[order.Items[i].ID] = &order.Items[i] - } - - r.orders[order.ID] = order - return nil -} - -// GetOrderByID retrieves an order by ID with its items -func (r *Repository) GetOrderByID(id uuid.UUID) (*models.Order, error) { - r.mu.RLock() - defer r.mu.RUnlock() - - order, exists := r.orders[id] - if !exists { - return nil, fmt.Errorf("order with ID %s not found", id) - } - - // Load order items - var items []models.OrderItem - for _, item := range r.orderItems { - if item.OrderID == id { - items = append(items, *item) - } - } - - // Create a copy with items - orderWithItems := *order - orderWithItems.Items = items - - return &orderWithItems, nil -} - -// GetOrdersByUserID retrieves all orders for a specific user -func (r *Repository) GetOrdersByUserID(userID uuid.UUID) ([]*models.Order, error) { - r.mu.RLock() - defer r.mu.RUnlock() - - var userOrders []*models.Order - for _, order := range r.orders { - if order.UserID == userID { - // Load order items - var items []models.OrderItem - for _, item := range r.orderItems { - if item.OrderID == order.ID { - items = append(items, *item) - } - } - - // Create a copy with items - orderWithItems := *order - orderWithItems.Items = items - userOrders = append(userOrders, &orderWithItems) - } - } - - return userOrders, nil -} - -// GetAllOrders retrieves all orders -func (r *Repository) GetAllOrders() ([]*models.Order, error) { - r.mu.RLock() - defer r.mu.RUnlock() - - orders := make([]*models.Order, 0, len(r.orders)) - for _, order := range r.orders { - // Load order items - var items []models.OrderItem - for _, item := range r.orderItems { - if item.OrderID == order.ID { - items = append(items, *item) - } - } - - // Create a copy with items - orderWithItems := *order - orderWithItems.Items = items - orders = append(orders, &orderWithItems) - } - - return orders, nil -} - -// UpdateOrderStatus updates the status of an order -func (r *Repository) UpdateOrderStatus(id uuid.UUID, status models.OrderStatus) (*models.Order, error) { - r.mu.Lock() - defer r.mu.Unlock() - - order, exists := r.orders[id] - if !exists { - return nil, fmt.Errorf("order with ID %s not found", id) - } - - // Create a copy to avoid modifying the original - updatedOrder := *order - updatedOrder.Status = status - updatedOrder.UpdatedAt = time.Now() - - r.orders[id] = &updatedOrder - - // Load order items for the response - var items []models.OrderItem - for _, item := range r.orderItems { - if item.OrderID == id { - items = append(items, *item) - } - } - updatedOrder.Items = items - - return &updatedOrder, nil -} - -// DeleteOrder deletes an order and its items -func (r *Repository) DeleteOrder(id uuid.UUID) error { - r.mu.Lock() - defer r.mu.Unlock() - - if _, exists := r.orders[id]; !exists { - return fmt.Errorf("order with ID %s not found", id) - } - - // Delete order items first - for itemID, item := range r.orderItems { - if item.OrderID == id { - delete(r.orderItems, itemID) - } - } - - // Delete the order - delete(r.orders, id) - return nil -} - -// GetOrdersByStatus retrieves orders by status -func (r *Repository) GetOrdersByStatus(status models.OrderStatus) ([]*models.Order, error) { - r.mu.RLock() - defer r.mu.RUnlock() - - var statusOrders []*models.Order - for _, order := range r.orders { - if order.Status == status { - // Load order items - var items []models.OrderItem - for _, item := range r.orderItems { - if item.OrderID == order.ID { - items = append(items, *item) - } - } - - // Create a copy with items - orderWithItems := *order - orderWithItems.Items = items - statusOrders = append(statusOrders, &orderWithItems) - } - } - - return statusOrders, nil -} - -// GetOrderItems retrieves all items for a specific order -func (r *Repository) GetOrderItems(orderID uuid.UUID) ([]models.OrderItem, error) { - r.mu.RLock() - defer r.mu.RUnlock() - - var items []models.OrderItem - for _, item := range r.orderItems { - if item.OrderID == orderID { - items = append(items, *item) - } - } - - return items, nil -} diff --git a/example/internal/order/service.go b/example/internal/order/service.go deleted file mode 100644 index 1aac1cd..0000000 --- a/example/internal/order/service.go +++ /dev/null @@ -1,285 +0,0 @@ -package order - -import ( - "fmt" - - "github.com/example/ecommerce-api/internal/models" - "github.com/google/uuid" -) - -// ProductService interface for interacting with product service -type ProductService interface { - GetProduct(id uuid.UUID) (*models.ProductResponse, error) - CheckStock(id uuid.UUID, quantity int) (bool, error) - ReserveStock(id uuid.UUID, quantity int) error - ReleaseStock(id uuid.UUID, quantity int) error -} - -// UserService interface for interacting with user service -type UserService interface { - GetUser(id uuid.UUID) (*models.UserResponse, error) -} - -// Service handles order business logic -type Service struct { - repo *Repository - productService ProductService - userService UserService -} - -// ProvideService creates a new order service -func ProvideService(repo *Repository, productService ProductService, userService UserService) *Service { - return &Service{ - repo: repo, - productService: productService, - userService: userService, - } -} - -// CreateOrder creates a new order -func (s *Service) CreateOrder(userID uuid.UUID, req *models.CreateOrderRequest) (*models.OrderResponse, error) { - // Validate user exists - if _, err := s.userService.GetUser(userID); err != nil { - return nil, fmt.Errorf("invalid user: %w", err) - } - - // Validate business rules - if err := s.validateCreateOrderRequest(req); err != nil { - return nil, err - } - - // Calculate total price and validate stock - var totalPrice float64 - var orderItems []models.OrderItem - - for _, item := range req.Items { - // Get product details - product, err := s.productService.GetProduct(item.ProductID) - if err != nil { - return nil, fmt.Errorf("invalid product %s: %w", item.ProductID, err) - } - - // Check stock availability - available, err := s.productService.CheckStock(item.ProductID, item.Quantity) - if err != nil { - return nil, fmt.Errorf("failed to check stock for product %s: %w", item.ProductID, err) - } - - if !available { - return nil, fmt.Errorf("insufficient stock for product %s", product.Name) - } - - // Create order item - orderItem := models.OrderItem{ - ProductID: item.ProductID, - Quantity: item.Quantity, - UnitPrice: product.Price, - } - - orderItems = append(orderItems, orderItem) - totalPrice += product.Price * float64(item.Quantity) - } - - // Create order - order := &models.Order{ - UserID: userID, - Status: models.OrderStatusPending, - TotalPrice: totalPrice, - Items: orderItems, - } - - if err := s.repo.CreateOrder(order); err != nil { - return nil, fmt.Errorf("failed to create order: %w", err) - } - - // Reserve stock for all items - for _, item := range order.Items { - if err := s.productService.ReserveStock(item.ProductID, item.Quantity); err != nil { - // Rollback: release already reserved stock and delete order - s.rollbackStockReservation(order.Items, item.ProductID) - s.repo.DeleteOrder(order.ID) - return nil, fmt.Errorf("failed to reserve stock for product %s: %w", item.ProductID, err) - } - } - - return s.toOrderResponse(order), nil -} - -// GetOrder retrieves an order by ID -func (s *Service) GetOrder(id uuid.UUID) (*models.OrderResponse, error) { - order, err := s.repo.GetOrderByID(id) - if err != nil { - return nil, fmt.Errorf("failed to get order: %w", err) - } - - return s.toOrderResponse(order), nil -} - -// GetOrdersByUser retrieves all orders for a user -func (s *Service) GetOrdersByUser(userID uuid.UUID) ([]*models.OrderResponse, error) { - // Validate user exists - if _, err := s.userService.GetUser(userID); err != nil { - return nil, fmt.Errorf("invalid user: %w", err) - } - - orders, err := s.repo.GetOrdersByUserID(userID) - if err != nil { - return nil, fmt.Errorf("failed to get orders for user: %w", err) - } - - responses := make([]*models.OrderResponse, len(orders)) - for i, order := range orders { - responses[i] = s.toOrderResponse(order) - } - - return responses, nil -} - -// GetAllOrders retrieves all orders -func (s *Service) GetAllOrders() ([]*models.OrderResponse, error) { - orders, err := s.repo.GetAllOrders() - if err != nil { - return nil, fmt.Errorf("failed to get orders: %w", err) - } - - responses := make([]*models.OrderResponse, len(orders)) - for i, order := range orders { - responses[i] = s.toOrderResponse(order) - } - - return responses, nil -} - -// UpdateOrderStatus updates the status of an order -func (s *Service) UpdateOrderStatus(id uuid.UUID, req *models.UpdateOrderStatusRequest) (*models.OrderResponse, error) { - // Validate status transition - currentOrder, err := s.repo.GetOrderByID(id) - if err != nil { - return nil, fmt.Errorf("failed to get order: %w", err) - } - - if err := s.validateStatusTransition(currentOrder.Status, req.Status); err != nil { - return nil, err - } - - // Handle stock release if order is cancelled - if req.Status == models.OrderStatusCancelled && currentOrder.Status != models.OrderStatusCancelled { - for _, item := range currentOrder.Items { - if err := s.productService.ReleaseStock(item.ProductID, item.Quantity); err != nil { - return nil, fmt.Errorf("failed to release stock for product %s: %w", item.ProductID, err) - } - } - } - - order, err := s.repo.UpdateOrderStatus(id, req.Status) - if err != nil { - return nil, fmt.Errorf("failed to update order status: %w", err) - } - - return s.toOrderResponse(order), nil -} - -// CancelOrder cancels an order -func (s *Service) CancelOrder(id uuid.UUID) error { - req := &models.UpdateOrderStatusRequest{ - Status: models.OrderStatusCancelled, - } - - _, err := s.UpdateOrderStatus(id, req) - return err -} - -// GetOrdersByStatus retrieves orders by status -func (s *Service) GetOrdersByStatus(status models.OrderStatus) ([]*models.OrderResponse, error) { - orders, err := s.repo.GetOrdersByStatus(status) - if err != nil { - return nil, fmt.Errorf("failed to get orders by status: %w", err) - } - - responses := make([]*models.OrderResponse, len(orders)) - for i, order := range orders { - responses[i] = s.toOrderResponse(order) - } - - return responses, nil -} - -// validateCreateOrderRequest validates create order request -func (s *Service) validateCreateOrderRequest(req *models.CreateOrderRequest) error { - if len(req.Items) == 0 { - return fmt.Errorf("order must contain at least one item") - } - - for i, item := range req.Items { - if item.Quantity <= 0 { - return fmt.Errorf("item %d: quantity must be greater than 0", i) - } - } - - return nil -} - -// validateStatusTransition validates order status transitions -func (s *Service) validateStatusTransition(current, new models.OrderStatus) error { - validTransitions := map[models.OrderStatus][]models.OrderStatus{ - models.OrderStatusPending: {models.OrderStatusConfirmed, models.OrderStatusCancelled}, - models.OrderStatusConfirmed: {models.OrderStatusShipped, models.OrderStatusCancelled}, - models.OrderStatusShipped: {models.OrderStatusDelivered}, - models.OrderStatusDelivered: {}, // Final state - models.OrderStatusCancelled: {}, // Final state - } - - allowed := validTransitions[current] - for _, status := range allowed { - if status == new { - return nil - } - } - - return fmt.Errorf("invalid status transition from %s to %s", current, new) -} - -// rollbackStockReservation releases stock for items up to the failed product -func (s *Service) rollbackStockReservation(items []models.OrderItem, failedProductID uuid.UUID) { - for _, item := range items { - if item.ProductID == failedProductID { - break - } - s.productService.ReleaseStock(item.ProductID, item.Quantity) - } -} - -// toOrderResponse converts an Order model to OrderResponse -func (s *Service) toOrderResponse(order *models.Order) *models.OrderResponse { - items := make([]models.OrderItemResponse, len(order.Items)) - for i, item := range order.Items { - // Try to get product details for the response - var productResponse models.ProductResponse - if product, err := s.productService.GetProduct(item.ProductID); err == nil { - productResponse = *product - } else { - // Fallback if product service is unavailable - productResponse = models.ProductResponse{ - ID: item.ProductID, - Name: "Unknown Product", - } - } - - items[i] = models.OrderItemResponse{ - ID: item.ID, - ProductID: item.ProductID, - Quantity: item.Quantity, - UnitPrice: item.UnitPrice, - Product: productResponse, - } - } - - return &models.OrderResponse{ - ID: order.ID, - UserID: order.UserID, - Status: order.Status, - TotalPrice: order.TotalPrice, - Items: items, - CreatedAt: order.CreatedAt, - } -} diff --git a/example/internal/pkg/config/config.go b/example/internal/pkg/config/config.go new file mode 100644 index 0000000..e69de29 diff --git a/example/internal/pkg/db/db.go b/example/internal/pkg/db/db.go new file mode 100644 index 0000000..e69de29 diff --git a/example/internal/pkg/db/sql_logger.go b/example/internal/pkg/db/sql_logger.go new file mode 100644 index 0000000..e69de29 diff --git a/example/internal/pkg/errors/errors.go b/example/internal/pkg/errors/errors.go new file mode 100644 index 0000000..e69de29 diff --git a/example/internal/pkg/logger/logger.go b/example/internal/pkg/logger/logger.go new file mode 100644 index 0000000..e69de29 diff --git a/example/internal/pkg/types/types.go b/example/internal/pkg/types/types.go new file mode 100644 index 0000000..e69de29 diff --git a/example/internal/product/handler.go b/example/internal/product/handler.go deleted file mode 100644 index c1f8868..0000000 --- a/example/internal/product/handler.go +++ /dev/null @@ -1,313 +0,0 @@ -package product - -import ( - "strconv" - - "github.com/example/ecommerce-api/internal/models" - "github.com/gofiber/fiber/v2" - "github.com/google/uuid" -) - -// Handler handles HTTP requests for product operations -type Handler struct { - service *Service -} - -// ProvideHandler creates a new product handler -func ProvideHandler(service *Service) *Handler { - return &Handler{ - service: service, - } -} - -// GetProducts retrieves all products -// @Summary Get all products -// @Description Get a list of all products in the system -// @Tags products -// @Accept json -// @Produce json -// @Param category query string false "Filter by category ID" -// @Success 200 {array} models.ProductResponse -// @Failure 500 {object} map[string]string -// @Router /api/v1/products [get] -func (h *Handler) GetProducts(c *fiber.Ctx) error { - categoryID := c.Query("category") - - if categoryID != "" { - // Filter by category - catID, err := uuid.Parse(categoryID) - if err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "invalid category ID format", - }) - } - - products, err := h.service.GetProductsByCategory(catID) - if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ - "error": err.Error(), - }) - } - - return c.JSON(products) - } - - products, err := h.service.GetProducts() - if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ - "error": err.Error(), - }) - } - - return c.JSON(products) -} - -// GetProduct retrieves a product by ID -// @Summary Get product by ID -// @Description Get a specific product by its ID -// @Tags products -// @Accept json -// @Produce json -// @Param id path string true "Product ID" -// @Success 200 {object} models.ProductResponse -// @Failure 400 {object} map[string]string -// @Failure 404 {object} map[string]string -// @Failure 500 {object} map[string]string -// @Router /api/v1/products/{id} [get] -func (h *Handler) GetProduct(c *fiber.Ctx) error { - idStr := c.Params("id") - if idStr == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "product ID is required", - }) - } - - id, err := uuid.Parse(idStr) - if err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "invalid product ID format", - }) - } - - product, err := h.service.GetProduct(id) - if err != nil { - return c.Status(fiber.StatusNotFound).JSON(fiber.Map{ - "error": err.Error(), - }) - } - - return c.JSON(product) -} - -// CreateProduct creates a new product -// @Summary Create a new product -// @Description Create a new product in the system -// @Tags products -// @Accept json -// @Produce json -// @Param product body models.CreateProductRequest true "Product creation data" -// @Success 201 {object} models.ProductResponse -// @Failure 400 {object} map[string]string -// @Failure 500 {object} map[string]string -// @Router /api/v1/products [post] -func (h *Handler) CreateProduct(c *fiber.Ctx) error { - var req models.CreateProductRequest - if err := c.BodyParser(&req); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "invalid request body", - }) - } - - product, err := h.service.CreateProduct(&req) - if err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": err.Error(), - }) - } - - return c.Status(fiber.StatusCreated).JSON(product) -} - -// UpdateProduct updates an existing product -// @Summary Update product -// @Description Update an existing product's information -// @Tags products -// @Accept json -// @Produce json -// @Param id path string true "Product ID" -// @Param product body models.UpdateProductRequest true "Product update data" -// @Success 200 {object} models.ProductResponse -// @Failure 400 {object} map[string]string -// @Failure 404 {object} map[string]string -// @Failure 500 {object} map[string]string -// @Router /api/v1/products/{id} [put] -func (h *Handler) UpdateProduct(c *fiber.Ctx) error { - idStr := c.Params("id") - if idStr == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "product ID is required", - }) - } - - id, err := uuid.Parse(idStr) - if err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "invalid product ID format", - }) - } - - var req models.UpdateProductRequest - if err := c.BodyParser(&req); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "invalid request body", - }) - } - - product, err := h.service.UpdateProduct(id, &req) - if err != nil { - if contains(err.Error(), "not found") { - return c.Status(fiber.StatusNotFound).JSON(fiber.Map{ - "error": err.Error(), - }) - } - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": err.Error(), - }) - } - - return c.JSON(product) -} - -// DeleteProduct deletes a product -// @Summary Delete product -// @Description Delete a product from the system -// @Tags products -// @Accept json -// @Produce json -// @Param id path string true "Product ID" -// @Success 204 -// @Failure 400 {object} map[string]string -// @Failure 404 {object} map[string]string -// @Failure 500 {object} map[string]string -// @Router /api/v1/products/{id} [delete] -func (h *Handler) DeleteProduct(c *fiber.Ctx) error { - idStr := c.Params("id") - if idStr == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "product ID is required", - }) - } - - id, err := uuid.Parse(idStr) - if err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "invalid product ID format", - }) - } - - if err := h.service.DeleteProduct(id); err != nil { - if contains(err.Error(), "not found") { - return c.Status(fiber.StatusNotFound).JSON(fiber.Map{ - "error": err.Error(), - }) - } - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ - "error": err.Error(), - }) - } - - return c.SendStatus(fiber.StatusNoContent) -} - -// GetCategories retrieves all product categories -// @Summary Get all categories -// @Description Get a list of all product categories -// @Tags products -// @Accept json -// @Produce json -// @Success 200 {array} models.Category -// @Failure 500 {object} map[string]string -// @Router /api/v1/categories [get] -func (h *Handler) GetCategories(c *fiber.Ctx) error { - categories, err := h.service.GetCategories() - if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ - "error": err.Error(), - }) - } - - return c.JSON(categories) -} - -// CheckStock checks product stock availability -// @Summary Check product stock -// @Description Check if enough stock is available for a product -// @Tags products -// @Accept json -// @Produce json -// @Param id path string true "Product ID" -// @Param quantity query int true "Quantity to check" -// @Success 200 {object} map[string]bool -// @Failure 400 {object} map[string]string -// @Failure 404 {object} map[string]string -// @Failure 500 {object} map[string]string -// @Router /api/v1/products/{id}/stock [get] -func (h *Handler) CheckStock(c *fiber.Ctx) error { - idStr := c.Params("id") - if idStr == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "product ID is required", - }) - } - - id, err := uuid.Parse(idStr) - if err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "invalid product ID format", - }) - } - - quantityStr := c.Query("quantity") - if quantityStr == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "quantity parameter is required", - }) - } - - quantity, err := strconv.Atoi(quantityStr) - if err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "invalid quantity format", - }) - } - - if quantity <= 0 { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "quantity must be greater than 0", - }) - } - - available, err := h.service.CheckStock(id, quantity) - if err != nil { - if contains(err.Error(), "not found") { - return c.Status(fiber.StatusNotFound).JSON(fiber.Map{ - "error": err.Error(), - }) - } - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ - "error": err.Error(), - }) - } - - return c.JSON(fiber.Map{ - "available": available, - "requested": quantity, - }) -} - -// Helper function to check if string contains substring -func contains(s, substr string) bool { - return len(s) >= len(substr) && s[len(s)-len(substr):] == substr || - len(s) > len(substr) && s[:len(substr)] == substr || - len(s) > len(substr)*2 && s[len(s)/2-len(substr)/2:len(s)/2+len(substr)/2] == substr -} diff --git a/example/internal/product/repository.go b/example/internal/product/repository.go deleted file mode 100644 index 1e6e566..0000000 --- a/example/internal/product/repository.go +++ /dev/null @@ -1,221 +0,0 @@ -package product - -import ( - "fmt" - "sync" - "time" - - "github.com/example/ecommerce-api/internal/models" - "github.com/google/uuid" -) - -// Repository handles product data persistence -type Repository struct { - mu sync.RWMutex - products map[uuid.UUID]*models.Product - categories map[uuid.UUID]*models.Category -} - -// ProvideRepository creates a new product repository -func ProvideRepository() *Repository { - repo := &Repository{ - products: make(map[uuid.UUID]*models.Product), - categories: make(map[uuid.UUID]*models.Category), - } - - // Add some default categories - repo.seedCategories() - return repo -} - -// seedCategories adds some default categories -func (r *Repository) seedCategories() { - categories := []*models.Category{ - { - ID: uuid.MustParse("550e8400-e29b-41d4-a716-446655440001"), - Name: "Electronics", - CreatedAt: time.Now(), - }, - { - ID: uuid.MustParse("550e8400-e29b-41d4-a716-446655440002"), - Name: "Clothing", - CreatedAt: time.Now(), - }, - { - ID: uuid.MustParse("550e8400-e29b-41d4-a716-446655440003"), - Name: "Books", - CreatedAt: time.Now(), - }, - { - ID: uuid.MustParse("550e8400-e29b-41d4-a716-446655440004"), - Name: "Home & Garden", - CreatedAt: time.Now(), - }, - } - - for _, category := range categories { - r.categories[category.ID] = category - } -} - -// CreateProduct creates a new product -func (r *Repository) CreateProduct(product *models.Product) error { - r.mu.Lock() - defer r.mu.Unlock() - - // Check if category exists - if _, exists := r.categories[product.CategoryID]; !exists { - return fmt.Errorf("category with ID %s not found", product.CategoryID) - } - - product.ID = uuid.New() - product.CreatedAt = time.Now() - product.UpdatedAt = time.Now() - - r.products[product.ID] = product - return nil -} - -// GetProductByID retrieves a product by ID -func (r *Repository) GetProductByID(id uuid.UUID) (*models.Product, error) { - r.mu.RLock() - defer r.mu.RUnlock() - - product, exists := r.products[id] - if !exists { - return nil, fmt.Errorf("product with ID %s not found", id) - } - - return product, nil -} - -// GetAllProducts retrieves all products -func (r *Repository) GetAllProducts() ([]*models.Product, error) { - r.mu.RLock() - defer r.mu.RUnlock() - - products := make([]*models.Product, 0, len(r.products)) - for _, product := range r.products { - products = append(products, product) - } - - return products, nil -} - -// GetProductsByCategory retrieves products by category ID -func (r *Repository) GetProductsByCategory(categoryID uuid.UUID) ([]*models.Product, error) { - r.mu.RLock() - defer r.mu.RUnlock() - - var products []*models.Product - for _, product := range r.products { - if product.CategoryID == categoryID { - products = append(products, product) - } - } - - return products, nil -} - -// UpdateProduct updates a product -func (r *Repository) UpdateProduct(id uuid.UUID, updates map[string]interface{}) (*models.Product, error) { - r.mu.Lock() - defer r.mu.Unlock() - - product, exists := r.products[id] - if !exists { - return nil, fmt.Errorf("product with ID %s not found", id) - } - - // Create a copy to avoid modifying the original - updatedProduct := *product - - // Apply updates - if name, ok := updates["name"].(string); ok { - updatedProduct.Name = name - } - - if description, ok := updates["description"].(string); ok { - updatedProduct.Description = description - } - - if price, ok := updates["price"].(float64); ok { - updatedProduct.Price = price - } - - if stock, ok := updates["stock"].(int); ok { - updatedProduct.Stock = stock - } - - if categoryID, ok := updates["category_id"].(uuid.UUID); ok { - // Check if category exists - if _, exists := r.categories[categoryID]; !exists { - return nil, fmt.Errorf("category with ID %s not found", categoryID) - } - updatedProduct.CategoryID = categoryID - } - - updatedProduct.UpdatedAt = time.Now() - r.products[id] = &updatedProduct - - return &updatedProduct, nil -} - -// DeleteProduct deletes a product -func (r *Repository) DeleteProduct(id uuid.UUID) error { - r.mu.Lock() - defer r.mu.Unlock() - - if _, exists := r.products[id]; !exists { - return fmt.Errorf("product with ID %s not found", id) - } - - delete(r.products, id) - return nil -} - -// GetAllCategories retrieves all categories -func (r *Repository) GetAllCategories() ([]*models.Category, error) { - r.mu.RLock() - defer r.mu.RUnlock() - - categories := make([]*models.Category, 0, len(r.categories)) - for _, category := range r.categories { - categories = append(categories, category) - } - - return categories, nil -} - -// GetCategoryByID retrieves a category by ID -func (r *Repository) GetCategoryByID(id uuid.UUID) (*models.Category, error) { - r.mu.RLock() - defer r.mu.RUnlock() - - category, exists := r.categories[id] - if !exists { - return nil, fmt.Errorf("category with ID %s not found", id) - } - - return category, nil -} - -// UpdateStock updates product stock -func (r *Repository) UpdateStock(id uuid.UUID, quantity int) error { - r.mu.Lock() - defer r.mu.Unlock() - - product, exists := r.products[id] - if !exists { - return fmt.Errorf("product with ID %s not found", id) - } - - if product.Stock+quantity < 0 { - return fmt.Errorf("insufficient stock for product %s", id) - } - - product.Stock += quantity - product.UpdatedAt = time.Now() - - return nil -} diff --git a/example/internal/product/service.go b/example/internal/product/service.go deleted file mode 100644 index 0316599..0000000 --- a/example/internal/product/service.go +++ /dev/null @@ -1,212 +0,0 @@ -package product - -import ( - "fmt" - - "github.com/example/ecommerce-api/internal/models" - "github.com/google/uuid" -) - -// Service handles product business logic -type Service struct { - repo *Repository -} - -// ProvideService creates a new product service -func ProvideService(repo *Repository) *Service { - return &Service{ - repo: repo, - } -} - -// CreateProduct creates a new product -func (s *Service) CreateProduct(req *models.CreateProductRequest) (*models.ProductResponse, error) { - // Validate business rules - if err := s.validateCreateProductRequest(req); err != nil { - return nil, err - } - - product := &models.Product{ - Name: req.Name, - Description: req.Description, - Price: req.Price, - Stock: req.Stock, - CategoryID: req.CategoryID, - } - - if err := s.repo.CreateProduct(product); err != nil { - return nil, fmt.Errorf("failed to create product: %w", err) - } - - return s.toProductResponse(product), nil -} - -// GetProduct retrieves a product by ID -func (s *Service) GetProduct(id uuid.UUID) (*models.ProductResponse, error) { - product, err := s.repo.GetProductByID(id) - if err != nil { - return nil, fmt.Errorf("failed to get product: %w", err) - } - - return s.toProductResponse(product), nil -} - -// GetProducts retrieves all products -func (s *Service) GetProducts() ([]*models.ProductResponse, error) { - products, err := s.repo.GetAllProducts() - if err != nil { - return nil, fmt.Errorf("failed to get products: %w", err) - } - - responses := make([]*models.ProductResponse, len(products)) - for i, product := range products { - responses[i] = s.toProductResponse(product) - } - - return responses, nil -} - -// GetProductsByCategory retrieves products by category -func (s *Service) GetProductsByCategory(categoryID uuid.UUID) ([]*models.ProductResponse, error) { - products, err := s.repo.GetProductsByCategory(categoryID) - if err != nil { - return nil, fmt.Errorf("failed to get products by category: %w", err) - } - - responses := make([]*models.ProductResponse, len(products)) - for i, product := range products { - responses[i] = s.toProductResponse(product) - } - - return responses, nil -} - -// UpdateProduct updates a product -func (s *Service) UpdateProduct(id uuid.UUID, req *models.UpdateProductRequest) (*models.ProductResponse, error) { - // Validate business rules - if err := s.validateUpdateProductRequest(req); err != nil { - return nil, err - } - - updates := make(map[string]interface{}) - - if req.Name != nil { - updates["name"] = *req.Name - } - if req.Description != nil { - updates["description"] = *req.Description - } - if req.Price != nil { - updates["price"] = *req.Price - } - if req.Stock != nil { - updates["stock"] = *req.Stock - } - if req.CategoryID != nil { - updates["category_id"] = *req.CategoryID - } - - product, err := s.repo.UpdateProduct(id, updates) - if err != nil { - return nil, fmt.Errorf("failed to update product: %w", err) - } - - return s.toProductResponse(product), nil -} - -// DeleteProduct deletes a product -func (s *Service) DeleteProduct(id uuid.UUID) error { - if err := s.repo.DeleteProduct(id); err != nil { - return fmt.Errorf("failed to delete product: %w", err) - } - - return nil -} - -// GetCategories retrieves all categories -func (s *Service) GetCategories() ([]*models.Category, error) { - categories, err := s.repo.GetAllCategories() - if err != nil { - return nil, fmt.Errorf("failed to get categories: %w", err) - } - - return categories, nil -} - -// CheckStock checks if enough stock is available -func (s *Service) CheckStock(id uuid.UUID, quantity int) (bool, error) { - product, err := s.repo.GetProductByID(id) - if err != nil { - return false, fmt.Errorf("failed to get product: %w", err) - } - - return product.Stock >= quantity, nil -} - -// ReserveStock reduces product stock (for order processing) -func (s *Service) ReserveStock(id uuid.UUID, quantity int) error { - if err := s.repo.UpdateStock(id, -quantity); err != nil { - return fmt.Errorf("failed to reserve stock: %w", err) - } - - return nil -} - -// ReleaseStock increases product stock (for order cancellation) -func (s *Service) ReleaseStock(id uuid.UUID, quantity int) error { - if err := s.repo.UpdateStock(id, quantity); err != nil { - return fmt.Errorf("failed to release stock: %w", err) - } - - return nil -} - -// validateCreateProductRequest validates create product request -func (s *Service) validateCreateProductRequest(req *models.CreateProductRequest) error { - if req.Name == "" { - return fmt.Errorf("name is required") - } - if len(req.Name) < 2 || len(req.Name) > 100 { - return fmt.Errorf("name must be between 2 and 100 characters") - } - if len(req.Description) > 500 { - return fmt.Errorf("description must not exceed 500 characters") - } - if req.Price <= 0 { - return fmt.Errorf("price must be greater than 0") - } - if req.Stock < 0 { - return fmt.Errorf("stock must be greater than or equal to 0") - } - return nil -} - -// validateUpdateProductRequest validates update product request -func (s *Service) validateUpdateProductRequest(req *models.UpdateProductRequest) error { - if req.Name != nil && (len(*req.Name) < 2 || len(*req.Name) > 100) { - return fmt.Errorf("name must be between 2 and 100 characters") - } - if req.Description != nil && len(*req.Description) > 500 { - return fmt.Errorf("description must not exceed 500 characters") - } - if req.Price != nil && *req.Price <= 0 { - return fmt.Errorf("price must be greater than 0") - } - if req.Stock != nil && *req.Stock < 0 { - return fmt.Errorf("stock must be greater than or equal to 0") - } - return nil -} - -// toProductResponse converts a Product model to ProductResponse -func (s *Service) toProductResponse(product *models.Product) *models.ProductResponse { - return &models.ProductResponse{ - ID: product.ID, - Name: product.Name, - Description: product.Description, - Price: product.Price, - Stock: product.Stock, - CategoryID: product.CategoryID, - CreatedAt: product.CreatedAt, - } -} diff --git a/example/internal/user/handler.go b/example/internal/user/handler.go deleted file mode 100644 index ffb7dc0..0000000 --- a/example/internal/user/handler.go +++ /dev/null @@ -1,245 +0,0 @@ -package user - -import ( - "github.com/example/ecommerce-api/internal/models" - "github.com/gofiber/fiber/v2" - "github.com/google/uuid" -) - -// Handler handles HTTP requests for user operations -type Handler struct { - service *Service -} - -// ProvideHandler creates a new user handler -func ProvideHandler(service *Service) *Handler { - return &Handler{ - service: service, - } -} - -// GetUsers retrieves all users -// @Summary Get all users -// @Description Get a list of all users in the system -// @Tags users -// @Accept json -// @Produce json -// @Success 200 {array} models.UserResponse -// @Failure 500 {object} map[string]string -// @Router /api/v1/users [get] -func (h *Handler) GetUsers(c *fiber.Ctx) error { - users, err := h.service.GetUsers() - if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ - "error": err.Error(), - }) - } - - return c.JSON(users) -} - -// GetUser retrieves a user by ID -// @Summary Get user by ID -// @Description Get a specific user by their ID -// @Tags users -// @Accept json -// @Produce json -// @Param id path string true "User ID" -// @Success 200 {object} models.UserResponse -// @Failure 400 {object} map[string]string -// @Failure 404 {object} map[string]string -// @Failure 500 {object} map[string]string -// @Router /api/v1/users/{id} [get] -func (h *Handler) GetUser(c *fiber.Ctx) error { - idStr := c.Params("id") - if idStr == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "user ID is required", - }) - } - - id, err := uuid.Parse(idStr) - if err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "invalid user ID format", - }) - } - - user, err := h.service.GetUser(id) - if err != nil { - return c.Status(fiber.StatusNotFound).JSON(fiber.Map{ - "error": err.Error(), - }) - } - - return c.JSON(user) -} - -// CreateUser creates a new user -// @Summary Create a new user -// @Description Create a new user in the system -// @Tags users -// @Accept json -// @Produce json -// @Param user body models.CreateUserRequest true "User creation data" -// @Success 201 {object} models.UserResponse -// @Failure 400 {object} map[string]string -// @Failure 409 {object} map[string]string -// @Failure 500 {object} map[string]string -// @Router /api/v1/users [post] -func (h *Handler) CreateUser(c *fiber.Ctx) error { - var req models.CreateUserRequest - if err := c.BodyParser(&req); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "invalid request body", - }) - } - - user, err := h.service.CreateUser(&req) - if err != nil { - if contains(err.Error(), "already exists") { - return c.Status(fiber.StatusConflict).JSON(fiber.Map{ - "error": err.Error(), - }) - } - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": err.Error(), - }) - } - - return c.Status(fiber.StatusCreated).JSON(user) -} - -// UpdateUser updates an existing user -// @Summary Update user -// @Description Update an existing user's information -// @Tags users -// @Accept json -// @Produce json -// @Param id path string true "User ID" -// @Param user body models.UpdateUserRequest true "User update data" -// @Success 200 {object} models.UserResponse -// @Failure 400 {object} map[string]string -// @Failure 404 {object} map[string]string -// @Failure 409 {object} map[string]string -// @Failure 500 {object} map[string]string -// @Router /api/v1/users/{id} [put] -func (h *Handler) UpdateUser(c *fiber.Ctx) error { - idStr := c.Params("id") - if idStr == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "user ID is required", - }) - } - - id, err := uuid.Parse(idStr) - if err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "invalid user ID format", - }) - } - - var req models.UpdateUserRequest - if err := c.BodyParser(&req); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "invalid request body", - }) - } - - user, err := h.service.UpdateUser(id, &req) - if err != nil { - if contains(err.Error(), "not found") { - return c.Status(fiber.StatusNotFound).JSON(fiber.Map{ - "error": err.Error(), - }) - } - if contains(err.Error(), "already exists") { - return c.Status(fiber.StatusConflict).JSON(fiber.Map{ - "error": err.Error(), - }) - } - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": err.Error(), - }) - } - - return c.JSON(user) -} - -// DeleteUser deletes a user -// @Summary Delete user -// @Description Delete a user from the system -// @Tags users -// @Accept json -// @Produce json -// @Param id path string true "User ID" -// @Success 204 -// @Failure 400 {object} map[string]string -// @Failure 404 {object} map[string]string -// @Failure 500 {object} map[string]string -// @Router /api/v1/users/{id} [delete] -func (h *Handler) DeleteUser(c *fiber.Ctx) error { - idStr := c.Params("id") - if idStr == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "user ID is required", - }) - } - - id, err := uuid.Parse(idStr) - if err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "invalid user ID format", - }) - } - - if err := h.service.DeleteUser(id); err != nil { - if contains(err.Error(), "not found") { - return c.Status(fiber.StatusNotFound).JSON(fiber.Map{ - "error": err.Error(), - }) - } - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ - "error": err.Error(), - }) - } - - return c.SendStatus(fiber.StatusNoContent) -} - -// GetUserByEmail retrieves a user by email -// @Summary Get user by email -// @Description Get a specific user by their email address -// @Tags users -// @Accept json -// @Produce json -// @Param email query string true "User email" -// @Success 200 {object} models.UserResponse -// @Failure 400 {object} map[string]string -// @Failure 404 {object} map[string]string -// @Failure 500 {object} map[string]string -// @Router /api/v1/users/by-email [get] -func (h *Handler) GetUserByEmail(c *fiber.Ctx) error { - email := c.Query("email") - if email == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "email parameter is required", - }) - } - - user, err := h.service.GetUserByEmail(email) - if err != nil { - return c.Status(fiber.StatusNotFound).JSON(fiber.Map{ - "error": err.Error(), - }) - } - - return c.JSON(user) -} - -// Helper function to check if string contains substring -func contains(s, substr string) bool { - return len(s) >= len(substr) && s[len(s)-len(substr):] == substr || - len(s) > len(substr) && s[:len(substr)] == substr || - len(s) > len(substr)*2 && s[len(s)/2-len(substr)/2:len(s)/2+len(substr)/2] == substr -} diff --git a/example/internal/user/repository.go b/example/internal/user/repository.go deleted file mode 100644 index e055b97..0000000 --- a/example/internal/user/repository.go +++ /dev/null @@ -1,134 +0,0 @@ -package user - -import ( - "fmt" - "sync" - "time" - - "github.com/example/ecommerce-api/internal/models" - "github.com/google/uuid" -) - -// Repository handles user data persistence -type Repository struct { - mu sync.RWMutex - users map[uuid.UUID]*models.User -} - -// ProvideRepository creates a new user repository -func ProvideRepository() *Repository { - return &Repository{ - users: make(map[uuid.UUID]*models.User), - } -} - -// Create creates a new user -func (r *Repository) Create(user *models.User) error { - r.mu.Lock() - defer r.mu.Unlock() - - // Check if email already exists - for _, existingUser := range r.users { - if existingUser.Email == user.Email { - return fmt.Errorf("user with email %s already exists", user.Email) - } - } - - user.ID = uuid.New() - user.CreatedAt = time.Now() - user.UpdatedAt = time.Now() - - r.users[user.ID] = user - return nil -} - -// GetByID retrieves a user by ID -func (r *Repository) GetByID(id uuid.UUID) (*models.User, error) { - r.mu.RLock() - defer r.mu.RUnlock() - - user, exists := r.users[id] - if !exists { - return nil, fmt.Errorf("user with ID %s not found", id) - } - - return user, nil -} - -// GetByEmail retrieves a user by email -func (r *Repository) GetByEmail(email string) (*models.User, error) { - r.mu.RLock() - defer r.mu.RUnlock() - - for _, user := range r.users { - if user.Email == email { - return user, nil - } - } - - return nil, fmt.Errorf("user with email %s not found", email) -} - -// GetAll retrieves all users -func (r *Repository) GetAll() ([]*models.User, error) { - r.mu.RLock() - defer r.mu.RUnlock() - - users := make([]*models.User, 0, len(r.users)) - for _, user := range r.users { - users = append(users, user) - } - - return users, nil -} - -// Update updates a user -func (r *Repository) Update(id uuid.UUID, updates map[string]interface{}) (*models.User, error) { - r.mu.Lock() - defer r.mu.Unlock() - - user, exists := r.users[id] - if !exists { - return nil, fmt.Errorf("user with ID %s not found", id) - } - - // Create a copy to avoid modifying the original - updatedUser := *user - - // Apply updates - if email, ok := updates["email"].(string); ok { - // Check if new email already exists - for _, existingUser := range r.users { - if existingUser.ID != id && existingUser.Email == email { - return nil, fmt.Errorf("user with email %s already exists", email) - } - } - updatedUser.Email = email - } - - if firstName, ok := updates["first_name"].(string); ok { - updatedUser.FirstName = firstName - } - - if lastName, ok := updates["last_name"].(string); ok { - updatedUser.LastName = lastName - } - - updatedUser.UpdatedAt = time.Now() - r.users[id] = &updatedUser - - return &updatedUser, nil -} - -// Delete deletes a user -func (r *Repository) Delete(id uuid.UUID) error { - r.mu.Lock() - defer r.mu.Unlock() - - if _, exists := r.users[id]; !exists { - return fmt.Errorf("user with ID %s not found", id) - } - - delete(r.users, id) - return nil -} diff --git a/example/internal/user/service.go b/example/internal/user/service.go deleted file mode 100644 index 9d37994..0000000 --- a/example/internal/user/service.go +++ /dev/null @@ -1,153 +0,0 @@ -package user - -import ( - "fmt" - - "github.com/example/ecommerce-api/internal/models" - "github.com/google/uuid" -) - -// Service handles user business logic -type Service struct { - repo *Repository -} - -// ProvideService creates a new user service -func ProvideService(repo *Repository) *Service { - return &Service{ - repo: repo, - } -} - -// CreateUser creates a new user -func (s *Service) CreateUser(req *models.CreateUserRequest) (*models.UserResponse, error) { - // Validate business rules - if err := s.validateCreateUserRequest(req); err != nil { - return nil, err - } - - user := &models.User{ - Email: req.Email, - FirstName: req.FirstName, - LastName: req.LastName, - } - - if err := s.repo.Create(user); err != nil { - return nil, fmt.Errorf("failed to create user: %w", err) - } - - return s.toUserResponse(user), nil -} - -// GetUser retrieves a user by ID -func (s *Service) GetUser(id uuid.UUID) (*models.UserResponse, error) { - user, err := s.repo.GetByID(id) - if err != nil { - return nil, fmt.Errorf("failed to get user: %w", err) - } - - return s.toUserResponse(user), nil -} - -// GetUsers retrieves all users -func (s *Service) GetUsers() ([]*models.UserResponse, error) { - users, err := s.repo.GetAll() - if err != nil { - return nil, fmt.Errorf("failed to get users: %w", err) - } - - responses := make([]*models.UserResponse, len(users)) - for i, user := range users { - responses[i] = s.toUserResponse(user) - } - - return responses, nil -} - -// UpdateUser updates a user -func (s *Service) UpdateUser(id uuid.UUID, req *models.UpdateUserRequest) (*models.UserResponse, error) { - // Validate business rules - if err := s.validateUpdateUserRequest(req); err != nil { - return nil, err - } - - updates := make(map[string]interface{}) - - if req.Email != nil { - updates["email"] = *req.Email - } - if req.FirstName != nil { - updates["first_name"] = *req.FirstName - } - if req.LastName != nil { - updates["last_name"] = *req.LastName - } - - user, err := s.repo.Update(id, updates) - if err != nil { - return nil, fmt.Errorf("failed to update user: %w", err) - } - - return s.toUserResponse(user), nil -} - -// DeleteUser deletes a user -func (s *Service) DeleteUser(id uuid.UUID) error { - if err := s.repo.Delete(id); err != nil { - return fmt.Errorf("failed to delete user: %w", err) - } - - return nil -} - -// GetUserByEmail retrieves a user by email -func (s *Service) GetUserByEmail(email string) (*models.UserResponse, error) { - user, err := s.repo.GetByEmail(email) - if err != nil { - return nil, fmt.Errorf("failed to get user by email: %w", err) - } - - return s.toUserResponse(user), nil -} - -// validateCreateUserRequest validates create user request -func (s *Service) validateCreateUserRequest(req *models.CreateUserRequest) error { - if req.Email == "" { - return fmt.Errorf("email is required") - } - if req.FirstName == "" { - return fmt.Errorf("first name is required") - } - if req.LastName == "" { - return fmt.Errorf("last name is required") - } - if len(req.FirstName) < 2 || len(req.FirstName) > 50 { - return fmt.Errorf("first name must be between 2 and 50 characters") - } - if len(req.LastName) < 2 || len(req.LastName) > 50 { - return fmt.Errorf("last name must be between 2 and 50 characters") - } - return nil -} - -// validateUpdateUserRequest validates update user request -func (s *Service) validateUpdateUserRequest(req *models.UpdateUserRequest) error { - if req.FirstName != nil && (len(*req.FirstName) < 2 || len(*req.FirstName) > 50) { - return fmt.Errorf("first name must be between 2 and 50 characters") - } - if req.LastName != nil && (len(*req.LastName) < 2 || len(*req.LastName) > 50) { - return fmt.Errorf("last name must be between 2 and 50 characters") - } - return nil -} - -// toUserResponse converts a User model to UserResponse -func (s *Service) toUserResponse(user *models.User) *models.UserResponse { - return &models.UserResponse{ - ID: user.ID, - Email: user.Email, - FirstName: user.FirstName, - LastName: user.LastName, - CreatedAt: user.CreatedAt, - } -} diff --git a/example/taskw.yaml b/example/taskw.yaml index a3b1e3b..e34ea37 100644 --- a/example/taskw.yaml +++ b/example/taskw.yaml @@ -1,6 +1,6 @@ version: "1.0" project: - module: "github.com/example/ecommerce-api" + module: "{{.Module}}" paths: scan_dirs: ["."] output_dir: "./internal/api" diff --git a/example/templates/entity.template b/example/templates/entity.template new file mode 100644 index 0000000..e69de29 diff --git a/test/e2e/01_init_test.go b/test/e2e/01_init_test.go deleted file mode 100644 index 3ee91c1..0000000 --- a/test/e2e/01_init_test.go +++ /dev/null @@ -1,269 +0,0 @@ -package e2e - -import ( - "os" - "os/exec" - "path/filepath" - "strings" - "testing" -) - -// TestProjectInitialization tests the complete project initialization workflow -func TestProjectInitialization(t *testing.T) { - // Setup: Create temporary directory for test - testDir := filepath.Join(os.TempDir(), "taskw-e2e-init-test") - if err := os.RemoveAll(testDir); err != nil { - t.Fatalf("Failed to clean test directory: %v", err) - } - if err := os.MkdirAll(testDir, 0755); err != nil { - t.Fatalf("Failed to create test directory: %v", err) - } - defer os.RemoveAll(testDir) // Cleanup - - originalDir, err := os.Getwd() - if err != nil { - t.Fatalf("Failed to get current directory: %v", err) - } - defer os.Chdir(originalDir) // Restore working directory - - // Get path to taskw binary BEFORE changing directories - taskwBin := getTaskwBinary(t) - - if err := os.Chdir(testDir); err != nil { - t.Fatalf("Failed to change to test directory: %v", err) - } - module := "github.com/test/e2e-init-project" - - t.Run("01_initialize_project", func(t *testing.T) { - // Run: taskw init with module - cmd := exec.Command(taskwBin, "init", module) - output, err := cmd.CombinedOutput() - if err != nil { - t.Fatalf("taskw init failed: %v \n Output: %s", err, string(output)) - } - - t.Logf("✅ taskw init output: %s", string(output)) - - // Verify: Project directory was created - projectName := "e2e-init-project" - projectDir := filepath.Join(testDir, projectName) - if _, err := os.Stat(projectDir); os.IsNotExist(err) { - t.Fatalf("Project directory was not created: %s", projectDir) - } - - t.Logf("✅ Project directory created: %s", projectDir) - }) - - projectName := "e2e-init-project" - projectDir := filepath.Join(testDir, projectName) - - t.Run("02_verify_scaffolded_files", func(t *testing.T) { - // Verify: All expected files exist - expectedFiles := []string{ - "cmd/server/main.go", - "internal/api/server.go", - "internal/api/wire.go", - "internal/health/handler.go", - ".air.toml", - "Taskfile.yml", - "taskw.yaml", - "go.mod", - ".taskwignore", - } - - for _, file := range expectedFiles { - filePath := filepath.Join(projectDir, file) - if _, err := os.Stat(filePath); os.IsNotExist(err) { - t.Errorf("Expected file not found: %s", file) - } else { - t.Logf("✅ File exists: %s", file) - } - } - - // Verify: Directories exist - expectedDirs := []string{ - "bin", - "docs", - "cmd/server", - "internal/api", - "internal/health", - } - - for _, dir := range expectedDirs { - dirPath := filepath.Join(projectDir, dir) - if stat, err := os.Stat(dirPath); os.IsNotExist(err) || !stat.IsDir() { - t.Errorf("Expected directory not found: %s", dir) - } else { - t.Logf("✅ Directory exists: %s", dir) - } - } - }) - - t.Run("03_verify_go_mod_content", func(t *testing.T) { - // Verify: go.mod has correct module - goModPath := filepath.Join(projectDir, "go.mod") - content, err := os.ReadFile(goModPath) - if err != nil { - t.Fatalf("Failed to read go.mod: %v", err) - } - - if !strings.Contains(string(content), module) { - t.Errorf("go.mod does not contain expected module %s\nContent:\n%s", module, string(content)) - } else { - t.Logf("✅ go.mod contains correct module: %s", module) - } - - // Verify: go.mod has expected dependencies - expectedDeps := []string{ - "github.com/gofiber/fiber/v2", - "github.com/google/wire", - "github.com/gofiber/contrib/swagger", - } - - for _, dep := range expectedDeps { - if !strings.Contains(string(content), dep) { - t.Errorf("go.mod missing expected dependency: %s", dep) - } else { - t.Logf("✅ go.mod contains dependency: %s", dep) - } - } - }) - - t.Run("04_verify_taskw_config", func(t *testing.T) { - // Verify: taskw.yaml has correct configuration - taskwConfigPath := filepath.Join(projectDir, "taskw.yaml") - content, err := os.ReadFile(taskwConfigPath) - if err != nil { - t.Fatalf("Failed to read taskw.yaml: %v", err) - } - - configContent := string(content) - if !strings.Contains(configContent, module) { - t.Errorf("taskw.yaml does not contain expected module %s\nContent:\n%s", module, configContent) - } else { - t.Logf("✅ taskw.yaml contains correct module: %s", module) - } - - // Check for expected configuration sections - expectedConfig := []string{ - "version:", - "project:", - "paths:", - "generation:", - "routes:", - "dependencies:", - } - - for _, config := range expectedConfig { - if !strings.Contains(configContent, config) { - t.Errorf("taskw.yaml missing configuration section: %s", config) - } else { - t.Logf("✅ taskw.yaml contains config: %s", config) - } - } - }) - - t.Run("05_verify_health_handler", func(t *testing.T) { - // Verify: Health handler contains correct content - handlerPath := filepath.Join(projectDir, "internal/health/handler.go") - content, err := os.ReadFile(handlerPath) - if err != nil { - t.Fatalf("Failed to read health handler: %v", err) - } - - handlerContent := string(content) - expectedContent := []string{ - "package health", - "ProvideHandler", - "GetHealth", - "@Router /health [get]", - projectName + " API is running successfully", - } - - for _, expected := range expectedContent { - if !strings.Contains(handlerContent, expected) { - t.Errorf("Health handler missing expected content: %s", expected) - } else { - t.Logf("✅ Health handler contains: %s", expected) - } - } - }) - - t.Run("06_verify_taskwignore", func(t *testing.T) { - // Verify: .taskwignore exists and has sensible patterns - taskwIgnorePath := filepath.Join(projectDir, ".taskwignore") - content, err := os.ReadFile(taskwIgnorePath) - if err != nil { - t.Fatalf("Failed to read .taskwignore: %v", err) - } - - ignoreContent := string(content) - expectedPatterns := []string{ - "**/*_test.go", - "**/vendor/**", - "**/bin/**", - "**/*_gen.go", - "!routes_gen.go", - "!dependencies_gen.go", - } - - for _, pattern := range expectedPatterns { - if !strings.Contains(ignoreContent, pattern) { - t.Errorf(".taskwignore missing expected pattern: %s", pattern) - } else { - t.Logf("✅ .taskwignore contains pattern: %s", pattern) - } - } - }) - - t.Run("07_verify_initialization_completed", func(t *testing.T) { - // Since taskw init now automatically runs go mod tidy and task generate, - // we should verify that the initialization process completed successfully - - // Check if generated files exist - generatedFiles := []string{ - "internal/api/routes_gen.go", - "internal/api/dependencies_gen.go", - } - - for _, file := range generatedFiles { - filePath := filepath.Join(projectDir, file) - if _, err := os.Stat(filePath); os.IsNotExist(err) { - t.Errorf("Generated file not found: %s (init should have run task generate)", file) - } else { - t.Logf("✅ Generated file exists: %s", file) - } - } - }) - - t.Run("08_project_compiles", func(t *testing.T) { - // Test: Project has valid Go syntax (may not compile due to missing deps) - if err := os.Chdir(projectDir); err != nil { - t.Fatalf("Failed to change to project directory: %v", err) - } - - // Run go build to check syntax - cmd := exec.Command("go", "build", "./...") - output, err := cmd.CombinedOutput() - if err != nil { - buildOutput := string(output) - t.Logf("Build output: %s", buildOutput) - - // Check for syntax errors vs dependency errors - if strings.Contains(buildOutput, "syntax error") { - t.Errorf("Scaffolded code has syntax errors: %v", err) - } else if strings.Contains(buildOutput, "missing go.sum entry") || - strings.Contains(buildOutput, "GeneratedProviderSet") { - t.Logf("✅ Build fails as expected due to missing dependencies/generated code") - } else { - t.Logf("⚠️ Build failed with: %v (might be expected)", err) - } - } else { - t.Logf("✅ Project compiles successfully") - } - }) - - t.Logf("✅ Project initialization e2e test completed successfully") -} - -// Note: getTaskwBinary is now defined in utils.go diff --git a/test/e2e/02_route_test.go b/test/e2e/02_route_test.go deleted file mode 100644 index 5e8d2a8..0000000 --- a/test/e2e/02_route_test.go +++ /dev/null @@ -1,574 +0,0 @@ -package e2e - -import ( - "os" - "os/exec" - "path/filepath" - "strings" - "testing" -) - -// TestAddingNewRoute tests the workflow when adding new route handlers -func TestAddingNewRoute(t *testing.T) { - // Setup: Create temporary directory for test - testDir := filepath.Join(os.TempDir(), "taskw-e2e-route-test") - if err := os.RemoveAll(testDir); err != nil { - t.Fatalf("Failed to clean test directory: %v", err) - } - if err := os.MkdirAll(testDir, 0755); err != nil { - t.Fatalf("Failed to create test directory: %v", err) - } - defer os.RemoveAll(testDir) // Cleanup - - originalDir, err := os.Getwd() - if err != nil { - t.Fatalf("Failed to get current directory: %v", err) - } - defer os.Chdir(originalDir) // Restore working directory - - // Get path to taskw binary BEFORE changing directories - taskwBin := getTaskwBinary(t) - - if err := os.Chdir(testDir); err != nil { - t.Fatalf("Failed to change to test directory: %v", err) - } - module := "github.com/test/e2e-route-project" - projectName := "e2e-route-project" - projectDir := filepath.Join(testDir, projectName) - - t.Run("01_setup_project_with_service", func(t *testing.T) { - // Initialize project - cmd := exec.Command(taskwBin, "init", module) - output, err := cmd.CombinedOutput() - if err != nil { - t.Fatalf("taskw init failed: %v\nOutput: %s", err, string(output)) - } - - // Change to project directory - if err := os.Chdir(projectDir); err != nil { - t.Fatalf("Failed to change to project directory: %v", err) - } - - // Note: go mod tidy is now automatically run by taskw init - - // Create a basic user service for our routes - userDir := filepath.Join(projectDir, "internal", "user") - if err := os.MkdirAll(userDir, 0755); err != nil { - t.Fatalf("Failed to create user service directory: %v", err) - } - - // User service - userServiceCode := `package user - -import ( - "fmt" - "github.com/google/uuid" -) - -// User represents a user in the system -type User struct { - ID string ` + "`json:\"id\"`" + ` - Name string ` + "`json:\"name\"`" + ` - Email string ` + "`json:\"email\"`" + ` -} - -// Service handles user business logic -type Service struct { - users map[string]*User // Mock storage -} - -// ProvideService creates a new user service -func ProvideService() *Service { - return &Service{ - users: make(map[string]*User), - } -} - -// CreateUser creates a new user -func (s *Service) CreateUser(name, email string) (*User, error) { - user := &User{ - ID: uuid.New().String(), - Name: name, - Email: email, - } - - s.users[user.ID] = user - return user, nil -} - -// GetUser retrieves a user by ID -func (s *Service) GetUser(id string) (*User, error) { - user, exists := s.users[id] - if !exists { - return nil, fmt.Errorf("user not found: %s", id) - } - return user, nil -} - -// ListUsers returns all users -func (s *Service) ListUsers() []*User { - users := make([]*User, 0, len(s.users)) - for _, user := range s.users { - users = append(users, user) - } - return users -} - -// UpdateUser updates an existing user -func (s *Service) UpdateUser(id, name, email string) (*User, error) { - user, exists := s.users[id] - if !exists { - return nil, fmt.Errorf("user not found: %s", id) - } - - user.Name = name - user.Email = email - return user, nil -} - -// DeleteUser removes a user -func (s *Service) DeleteUser(id string) error { - if _, exists := s.users[id]; !exists { - return fmt.Errorf("user not found: %s", id) - } - - delete(s.users, id) - return nil -} -` - - serviceFile := filepath.Join(userDir, "service.go") - if err := os.WriteFile(serviceFile, []byte(userServiceCode), 0644); err != nil { - t.Fatalf("Failed to create user service: %v", err) - } - - t.Logf("✅ Project setup with user service completed") - }) - - t.Run("02_create_initial_handler_with_basic_routes", func(t *testing.T) { - // Create initial handler with basic CRUD routes - userDir := filepath.Join(projectDir, "internal", "user") - - handlerCode := `package user - -import ( - "github.com/gofiber/fiber/v2" -) - -// Handler handles user HTTP requests -type Handler struct { - service *Service -} - -// ProvideHandler creates a new user handler -func ProvideHandler(service *Service) *Handler { - return &Handler{ - service: service, - } -} - -// @Summary Create a new user -// @Description Create a new user with name and email -// @Tags users -// @Accept json -// @Produce json -// @Param request body CreateUserRequest true "User creation request" -// @Success 201 {object} User -// @Router /api/v1/users [post] -func (h *Handler) CreateUser(c *fiber.Ctx) error { - var req CreateUserRequest - if err := c.BodyParser(&req); err != nil { - return c.Status(400).JSON(fiber.Map{"error": "Invalid request body"}) - } - - user, err := h.service.CreateUser(req.Name, req.Email) - if err != nil { - return c.Status(500).JSON(fiber.Map{"error": err.Error()}) - } - - return c.Status(201).JSON(user) -} - -// @Summary Get user by ID -// @Description Get a single user by their ID -// @Tags users -// @Accept json -// @Produce json -// @Param id path string true "User ID" -// @Success 200 {object} User -// @Router /api/v1/users/{id} [get] -func (h *Handler) GetUser(c *fiber.Ctx) error { - id := c.Params("id") - - user, err := h.service.GetUser(id) - if err != nil { - return c.Status(404).JSON(fiber.Map{"error": err.Error()}) - } - - return c.JSON(user) -} - -// CreateUserRequest represents a user creation request -type CreateUserRequest struct { - Name string ` + "`json:\"name\" validate:\"required\"`" + ` - Email string ` + "`json:\"email\" validate:\"required,email\"`" + ` -} -` - - handlerFile := filepath.Join(userDir, "handler.go") - if err := os.WriteFile(handlerFile, []byte(handlerCode), 0644); err != nil { - t.Fatalf("Failed to create user handler: %v", err) - } - - t.Logf("✅ Initial handler created with 2 routes") - }) - - t.Run("03_update_router_generation", func(t *testing.T) { - - // Generate dependencies to include user service - cmd := exec.Command(taskwBin, "generate", "dependencies") - output, err := cmd.CombinedOutput() - if err != nil { - t.Fatalf("Failed to generate dependencies: %v\nOutput: %s", err, string(output)) - } - - t.Logf("✅ Dependencies generated to include user handler") - }) - - t.Run("04_generate_initial_routes", func(t *testing.T) { - // Generate routes for the initial handler - cmd := exec.Command(taskwBin, "generate", "routes") - output, err := cmd.CombinedOutput() - if err != nil { - t.Fatalf("taskw generate routes failed: %v\nOutput: %s", err, string(output)) - } - - // Verify initial routes were generated - routesFile := filepath.Join(projectDir, "internal", "api", "routes_gen.go") - content, err := os.ReadFile(routesFile) - if err != nil { - t.Fatalf("Failed to read generated routes file: %v", err) - } - - routesContent := string(content) - - // Check for initial routes - expectedRoutes := []string{ - "ar.app.Post(\"/api/v1/users\"", - "ar.app.Get(\"/api/v1/users/:id\"", - "ar.userHandler.CreateUser", - "ar.userHandler.GetUser", - } - - for _, route := range expectedRoutes { - if !strings.Contains(routesContent, route) { - t.Errorf("Expected initial route not found: %s", route) - } else { - t.Logf("✅ Initial route found: %s", route) - } - } - - t.Logf("✅ Initial routes generated successfully") - - // Now regenerate wire after routes are updated - wireCmd := exec.Command("go", "generate", "./internal/api") - _, err = wireCmd.CombinedOutput() - if err != nil { - // Wire regeneration may fail due to missing dependencies, but that's okay - // The important part is that it updates wire_gen.go with the new signature - } - t.Logf("✅ Wire regenerated with updated routes") - }) - - t.Run("05_add_new_routes_to_handler", func(t *testing.T) { - // Add new routes to the existing handler - userDir := filepath.Join(projectDir, "internal", "user") - handlerFile := filepath.Join(userDir, "handler.go") - - // Read existing handler content - existingContent, err := os.ReadFile(handlerFile) - if err != nil { - t.Fatalf("Failed to read existing handler: %v", err) - } - - // Add new handler methods with @Router annotations - newHandlerMethods := ` - -// @Summary List all users -// @Description Get a list of all users in the system -// @Tags users -// @Accept json -// @Produce json -// @Success 200 {array} User -// @Router /api/v1/users [get] -func (h *Handler) ListUsers(c *fiber.Ctx) error { - users := h.service.ListUsers() - return c.JSON(fiber.Map{ - "users": users, - "total": len(users), - }) -} - -// @Summary Update user -// @Description Update an existing user's information -// @Tags users -// @Accept json -// @Produce json -// @Param id path string true "User ID" -// @Param request body UpdateUserRequest true "User update request" -// @Success 200 {object} User -// @Router /api/v1/users/{id} [put] -func (h *Handler) UpdateUser(c *fiber.Ctx) error { - id := c.Params("id") - - var req UpdateUserRequest - if err := c.BodyParser(&req); err != nil { - return c.Status(400).JSON(fiber.Map{"error": "Invalid request body"}) - } - - user, err := h.service.UpdateUser(id, req.Name, req.Email) - if err != nil { - return c.Status(404).JSON(fiber.Map{"error": err.Error()}) - } - - return c.JSON(user) -} - -// @Summary Delete user -// @Description Delete a user from the system -// @Tags users -// @Accept json -// @Produce json -// @Param id path string true "User ID" -// @Success 204 -// @Router /api/v1/users/{id} [delete] -func (h *Handler) DeleteUser(c *fiber.Ctx) error { - id := c.Params("id") - - if err := h.service.DeleteUser(id); err != nil { - return c.Status(404).JSON(fiber.Map{"error": err.Error()}) - } - - return c.SendStatus(204) -} - -// @Summary Search users -// @Description Search users by name or email -// @Tags users -// @Accept json -// @Produce json -// @Param q query string true "Search query" -// @Success 200 {array} User -// @Router /api/v1/users/search [get] -func (h *Handler) SearchUsers(c *fiber.Ctx) error { - query := c.Query("q") - if query == "" { - return c.Status(400).JSON(fiber.Map{"error": "Search query is required"}) - } - - // Mock search implementation - allUsers := h.service.ListUsers() - var matchedUsers []*User - - for _, user := range allUsers { - if strings.Contains(strings.ToLower(user.Name), strings.ToLower(query)) || - strings.Contains(strings.ToLower(user.Email), strings.ToLower(query)) { - matchedUsers = append(matchedUsers, user) - } - } - - return c.JSON(fiber.Map{ - "users": matchedUsers, - "total": len(matchedUsers), - "query": query, - }) -} - -// UpdateUserRequest represents a user update request -type UpdateUserRequest struct { - Name string ` + "`json:\"name\" validate:\"required\"`" + ` - Email string ` + "`json:\"email\" validate:\"required,email\"`" + ` -} -` - - // Add necessary import for strings - updatedContent := strings.Replace(string(existingContent), - `import ( - "github.com/gofiber/fiber/v2" -)`, - `import ( - "strings" - "github.com/gofiber/fiber/v2" -)`, 1) - - // Append new methods - updatedContent += newHandlerMethods - - if err := os.WriteFile(handlerFile, []byte(updatedContent), 0644); err != nil { - t.Fatalf("Failed to update handler with new routes: %v", err) - } - - t.Logf("✅ Added 4 new routes to existing handler") - }) - - t.Run("06_scan_for_new_routes", func(t *testing.T) { - // Run taskw scan to see the new routes - cmd := exec.Command(taskwBin, "scan") - output, err := cmd.CombinedOutput() - if err != nil { - t.Fatalf("taskw scan failed: %v\nOutput: %s", err, string(output)) - } - - scanOutput := string(output) - - // Verify all routes are detected (original + new) - expectedRoutes := []string{ - "POST /api/v1/users", // Original - "GET /api/v1/users/:id", // Original - "GET /api/v1/users", // New - ListUsers - "PUT /api/v1/users/:id", // New - UpdateUser - "DELETE /api/v1/users/:id", // New - DeleteUser - "GET /api/v1/users/search", // New - SearchUsers - } - - foundRoutes := 0 - for _, route := range expectedRoutes { - if strings.Contains(scanOutput, route) { - foundRoutes++ - t.Logf("✅ Route detected: %s", route) - } else { - t.Errorf("Expected route not found in scan: %s", route) - } - } - - if foundRoutes != len(expectedRoutes) { - t.Errorf("Expected %d routes, found %d", len(expectedRoutes), foundRoutes) - } - - t.Logf("✅ Scan detected %d routes total", foundRoutes) - }) - - t.Run("07_regenerate_routes", func(t *testing.T) { - // Regenerate routes with new handlers - cmd := exec.Command(taskwBin, "generate", "routes") - output, err := cmd.CombinedOutput() - if err != nil { - t.Fatalf("taskw generate routes failed: %v\nOutput: %s", err, string(output)) - } - - // Verify routes_gen.go contains all routes - routesFile := filepath.Join(projectDir, "internal", "api", "routes_gen.go") - content, err := os.ReadFile(routesFile) - if err != nil { - t.Fatalf("Failed to read regenerated routes file: %v", err) - } - - routesContent := string(content) - - // Check for all route registrations - expectedRegistrations := []string{ - // Original routes - "ar.app.Post(\"/api/v1/users\", ar.userHandler.CreateUser)", - "ar.app.Get(\"/api/v1/users/:id\", ar.userHandler.GetUser)", - // New routes - "ar.app.Get(\"/api/v1/users\", ar.userHandler.ListUsers)", - "ar.app.Put(\"/api/v1/users/:id\", ar.userHandler.UpdateUser)", - "ar.app.Delete(\"/api/v1/users/:id\", ar.userHandler.DeleteUser)", - "ar.app.Get(\"/api/v1/users/search\", ar.userHandler.SearchUsers)", - } - - foundRegistrations := 0 - for _, registration := range expectedRegistrations { - if strings.Contains(routesContent, registration) { - foundRegistrations++ - t.Logf("✅ Route registration found: %s", registration) - } else { - t.Errorf("Expected route registration not found: %s", registration) - } - } - - if foundRegistrations != len(expectedRegistrations) { - t.Errorf("Expected %d route registrations, found %d", len(expectedRegistrations), foundRegistrations) - } - - t.Logf("✅ All %d route registrations generated successfully", foundRegistrations) - - // Regenerate wire after adding new routes - wireCmd := exec.Command("go", "generate", "./internal/api") - _, err = wireCmd.CombinedOutput() - if err != nil { - // Wire regeneration may fail due to missing dependencies, but that's okay - } - t.Logf("✅ Wire regenerated after adding new routes") - }) - - t.Run("08_verify_route_ordering", func(t *testing.T) { - // Verify routes are properly ordered (more specific routes before general ones) - routesFile := filepath.Join(projectDir, "internal", "api", "routes_gen.go") - content, err := os.ReadFile(routesFile) - if err != nil { - t.Fatalf("Failed to read routes file: %v", err) - } - - routesContent := string(content) - - // The search route should come before the general get route - searchIndex := strings.Index(routesContent, "/users/search") - generalGetIndex := strings.Index(routesContent, "Get(\"/api/v1/users\", s.userHandler.ListUsers)") - - if searchIndex != -1 && generalGetIndex != -1 && searchIndex > generalGetIndex { - t.Errorf("Route ordering issue: /users/search should come before general /users route") - } else { - t.Logf("✅ Routes are properly ordered") - } - }) - - t.Run("09_test_route_conflicts", func(t *testing.T) { - // Verify there are no route conflicts by checking the generated routes - routesFile := filepath.Join(projectDir, "internal", "api", "routes_gen.go") - content, err := os.ReadFile(routesFile) - if err != nil { - t.Fatalf("Failed to read routes file: %v", err) - } - - routesContent := string(content) - - // Count occurrences of potentially conflicting routes - conflicts := []struct { - route string - count int - }{ - {"ar.app.Get(\"/api/v1/users\"", 0}, - {"ar.app.Get(\"/api/v1/users/:id\"", 0}, - {"ar.app.Post(\"/api/v1/users\"", 0}, - } - - for i, conflict := range conflicts { - conflicts[i].count = strings.Count(routesContent, conflict.route) - if conflicts[i].count > 1 { - t.Errorf("Route conflict detected: %s appears %d times", conflict.route, conflicts[i].count) - } else { - t.Logf("✅ No conflict for route: %s (appears %d times)", conflict.route, conflicts[i].count) - } - } - }) - - t.Run("10_verify_project_builds", func(t *testing.T) { - // Try to build the project - cmd := exec.Command("go", "build", "./...") - output, err := cmd.CombinedOutput() - if err != nil { - t.Logf("Build output: %s", string(output)) - // Some build errors might be expected - if strings.Contains(string(output), "syntax error") { - t.Errorf("Syntax errors in generated code: %v", err) - } else { - t.Logf("✅ Build issues are related to missing RegisterRoutes method (expected)") - } - } else { - t.Logf("✅ Project builds successfully with new routes") - } - }) - - t.Logf("✅ Adding new route e2e test completed successfully") -} diff --git a/test/e2e/README.md b/test/e2e/README.md deleted file mode 100644 index 6c59b26..0000000 --- a/test/e2e/README.md +++ /dev/null @@ -1,192 +0,0 @@ -# Taskw End-to-End Tests - -This directory contains comprehensive end-to-end tests for Taskw that verify the complete workflows developers will experience when using the tool. - -## Test Cases - -### 1. Project Initialization (`01_init_test.go`) - -**Scenario**: Developer creates a new Taskw project from scratch - -**Test Steps**: - -1. Run `taskw init` with a Go module path -2. Verify all scaffolded files are created correctly -3. Check go.mod has correct module and dependencies -4. Validate taskw.yaml configuration -5. Verify health handler template content -6. Check .taskwignore patterns -7. Ensure `go mod tidy` works -8. Verify project has correct structure and compiles - -**Expected Outcome**: A fully functional Taskw project ready for development - -### 2. Adding New Dependency (`02_dependency_test.go`) - -**Scenario**: Developer adds a new service with provider functions - -**Test Steps**: - -1. Start with initialized project -2. Add new notification service module with multiple providers -3. Run `taskw scan` to detect new providers -4. Generate dependencies to include new providers -5. Generate routes for new handlers -6. Update server struct to include new handlers -7. Run wire generation -8. Verify project still compiles - -**Expected Outcome**: New providers are automatically detected and included in dependency injection - -### 3. Adding New Route (`03_route_test.go`) - -**Scenario**: Developer adds new handler methods with @Router annotations - -**Test Steps**: - -1. Start with project + basic user service -2. Create initial handler with 2 routes -3. Generate initial routes -4. Add 4 new handler methods with @Router annotations -5. Scan to detect new routes -6. Regenerate routes -7. Verify route ordering and conflict resolution -8. Update server to include handlers -9. Verify project builds with new routes - -**Expected Outcome**: New routes are automatically detected and added to route registration - -## Running the Tests - -### Prerequisites - -```bash -# Build Taskw binary -cd /path/to/taskw -go build -o bin/taskw cmd/taskw/main.go - -# Ensure Go and basic tools are installed -go version # Should be Go 1.21+ -``` - -### Run All E2E Tests - -```bash -cd test/e2e -go test -v ./... -``` - -### Run Specific Test - -```bash -cd test/e2e -go test -v -run TestProjectInitialization -go test -v -run TestAddingNewDependency -go test -v -run TestAddingNewRoute -``` - -### Run with Verbose Output - -```bash -cd test/e2e -go test -v -run TestProjectInitialization 2>&1 | tee init_test.log -``` - -## Test Environment - -Each test creates its own temporary directory under `/tmp/taskw-e2e-*-test` and cleans up automatically. Tests are designed to be: - -- **Isolated**: Each test runs in its own directory -- **Self-contained**: No dependencies on external services -- **Fast**: Focus on critical path verification -- **Comprehensive**: Cover the major user workflows - -## Expected Test Results - -### Successful Run Example - -```bash -=== RUN TestProjectInitialization -=== RUN TestProjectInitialization/01_initialize_project - 01_init_test.go:45: ✅ Project directory created: /tmp/taskw-e2e-init-test/e2e-init-project -=== RUN TestProjectInitialization/02_verify_scaffolded_files - 01_init_test.go:60: ✅ File exists: cmd/server/main.go - 01_init_test.go:60: ✅ File exists: internal/api/server.go - 01_init_test.go:60: ✅ File exists: internal/api/wire.go - # ... more verification steps -=== RUN TestProjectInitialization/08_project_compiles - 01_init_test.go:180: ✅ Build issues are related to missing RegisterRoutes method (expected) ---- PASS: TestProjectInitialization (2.34s) -``` - -## Troubleshooting - -### Test Failures - -**"Could not find taskw binary"** - -```bash -# Ensure taskw is built -go build -o bin/taskw cmd/taskw/main.go -``` - -**"go mod tidy failed"** - -```bash -# Check Go version and module setup -go version -go env GOMOD -``` - -**"Wire generation failed"** - -```bash -# Install wire (optional for tests) -go install github.com/google/wire/cmd/wire@latest -``` - -### Debug Mode - -Set environment variable for more detailed output: - -```bash -export TASKW_E2E_DEBUG=1 -go test -v ./... -``` - -## Adding New E2E Tests - -When adding new test scenarios: - -1. **Create new file**: `XX_feature_test.go` -2. **Follow naming**: `TestFeatureName` -3. **Use subtests**: Break into logical steps -4. **Clean up**: Use `defer os.RemoveAll(testDir)` -5. **Verify thoroughly**: Check generated files, compilation, functionality -6. **Document**: Add to this README - -### Test Template - -```go -func TestNewFeature(t *testing.T) { - // Setup - testDir := filepath.Join(os.TempDir(), "taskw-e2e-feature-test") - defer os.RemoveAll(testDir) - - t.Run("01_setup", func(t *testing.T) { - // Test setup - }) - - t.Run("02_main_functionality", func(t *testing.T) { - // Core test logic - }) - - t.Run("03_verification", func(t *testing.T) { - // Verify expected outcomes - }) - - t.Logf("✅ Feature e2e test completed successfully") -} -``` - -This ensures consistency and maintainability across all e2e tests. diff --git a/test/e2e/utils.go b/test/e2e/utils.go deleted file mode 100644 index b3145b2..0000000 --- a/test/e2e/utils.go +++ /dev/null @@ -1,17 +0,0 @@ -package e2e - -import ( - "os/exec" - "testing" -) - -// getTaskwBinary returns the path to the taskw-dev binary for testing -func getTaskwBinary(t *testing.T) string { - // Look for taskw-dev in PATH - if path, err := exec.LookPath("taskw"); err == nil { - return path - } - - t.Fatalf("Could not find taskw binary in PATH. Please ensure it is built and available.") - return "" -}