diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index f6b4b475..86eace75 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -35,10 +35,10 @@ app.Run(30 * time.Second) ```go // main.go func main() { - // Auto-generates code when @RouterService changes detected + // Auto-generates code when @EndpointService changes detected lokstra.Bootstrap() - // Import packages with @RouterService annotations + // Import packages with @EndpointService annotations _ "myapp/modules/user/application" // Services auto-registered via annotations! @@ -49,15 +49,15 @@ func main() { **Service with annotations:** ```go -// @RouterService name="user-service", prefix="/api/users" +// @EndpointService name="user-service", prefix="/api/users" type UserService struct { // @Inject "user-repository" - Direct service injection UserRepo UserRepository - // @Inject "cfg:store.implementation" - Service from config (NEW!) + // @Inject "@store.implementation" - Service from config Store Store // Injected service name from config: store.implementation = "postgres-store" - // @InjectCfgValue "app.name" - Config value injection + // @Inject "cfg:app.name" - Config value injection AppName string }// @Route "GET /{id}" func (s *UserService) GetByID(p *GetUserParams) (*User, error) { @@ -137,7 +137,7 @@ func (s *MySQLStore) GetUser(id string) (*User, error) { /* ... */ } ```go // application/user_service.go -// @RouterService name="user-service", prefix="/api/users" +// @EndpointService name="user-service", prefix="/api/users" type UserService struct { // @Inject "@store.implementation" Store Store // Actual service injected based on config! @@ -301,7 +301,7 @@ myapp/ ├── infrastructure/ │ └── user_repository.go └── application/ - ├── user_service.go # Contains @RouterService + ├── user_service.go # Contains @EndpointService └── zz_generated.lokstra.go # Auto-generated ``` @@ -381,7 +381,7 @@ go run . --generate-only # Force rebuild all 3. **Use pointer parameters** for request binding: `*CreateUserParams` 4. **Follow domain-driven design**: domain → repository → service 5. **Type-safe DI**: Use direct type assertions and `service.LazyLoad[T]` for lazy service loading -6. **Prefer annotations** for business services: Use `@RouterService` + `@Route` instead of manual registration +6. **Prefer annotations** for business services: Use `@EndpointService` + `@Route` instead of manual registration ## When Suggesting Code @@ -397,12 +397,12 @@ go run . --generate-only # Force rebuild all - Include error handling - Include validation tags - Include config.yaml if using framework mode - - Use `@RouterService` annotations for business services + - Use `@EndpointService` annotations for business services 3. **Follow project structure:** - Separate domain/application/infrastructure - Use interfaces in domain layer - - Business logic in application layer with `@RouterService` + - Business logic in application layer with `@EndpointService` - Data access in infrastructure layer ## Resources diff --git a/core/annotation/PRACTICAL_EXAMPLES.md b/core/annotation/PRACTICAL_EXAMPLES.md index c3324d54..719af880 100644 --- a/core/annotation/PRACTICAL_EXAMPLES.md +++ b/core/annotation/PRACTICAL_EXAMPLES.md @@ -10,7 +10,7 @@ Your app needs different JWT secrets, database timeouts, and API keys per enviro **Code (unchanged across environments):** ```go -// @RouterService name="auth-service", prefix="/api/auth" +// @EndpointService name="auth-service", prefix="/api/auth" type AuthService struct { // @Inject "user-repo" UserRepo UserRepository @@ -122,7 +122,7 @@ Different tenants use different database implementations (PostgreSQL, MySQL, Mon ### Solution: Service + Config Indirection ```go -// @RouterService name="tenant-service", prefix="/api/tenants" +// @EndpointService name="tenant-service", prefix="/api/tenants" type TenantService struct { // @Inject "@tenant.db-provider" // Service name from config DB database.Provider @@ -163,7 +163,7 @@ Switch tenant by changing `tenant.db-provider` to `database.tenant-b.provider`! Different subscription plans have different rate limits. ```go -// @RouterService name="api-gateway", prefix="/api" +// @EndpointService name="api-gateway", prefix="/api" type APIGateway struct { // @Inject "cfg:@rate-limit.requests-per-minute" RequestsPerMinute int @@ -261,7 +261,7 @@ configs: ## Example 6: Cache Strategy Selection ```go -// @RouterService name="product-service", prefix="/api/products" +// @EndpointService name="product-service", prefix="/api/products" type ProductService struct { // @Inject "@cache.provider" Cache cache.Provider diff --git a/core/annotation/arg_parser.go b/core/annotation/arg_parser.go index 636c1426..6cdd064d 100644 --- a/core/annotation/arg_parser.go +++ b/core/annotation/arg_parser.go @@ -33,8 +33,8 @@ func ParseFileAnnotations(path string) ([]*ParsedAnnotation, error) { if after, ok := strings.CutPrefix(line, "//"); ok { // CRITICAL: Detect code examples in Go documentation // Go doc convention: use TAB after // for code examples - // Valid annotation: // @RouterService (space after //) - // Invalid annotation: // @RouterService (TAB after // - code example) + // Valid annotation: // @EndpointService (space after //) + // Invalid annotation: // @EndpointService (TAB after // - code example) // Extract the content after // commentStart := strings.Index(originalLine, "//") @@ -55,8 +55,8 @@ func ParseFileAnnotations(path string) ([]*ParsedAnnotation, error) { if strings.HasPrefix(trimmedAfter, "@") { leadingWhitespace := afterComment[:len(afterComment)-len(trimmedAfter)] - // Allow single space (normal comment formatting: "// @RouterService") - // Reject TAB or multiple spaces (code examples: "// @RouterService" or "//\t@RouterService") + // Allow single space (normal comment formatting: "// @EndpointService") + // Reject TAB or multiple spaces (code examples: "// @EndpointService" or "//\t@EndpointService") if len(leadingWhitespace) > 1 || (len(leadingWhitespace) == 1 && leadingWhitespace[0] == '\t') { // Indented annotation - skip it (likely example code) continue @@ -108,18 +108,20 @@ func ParseFileAnnotations(path string) ([]*ParsedAnnotation, error) { } return annotations, nil -} // parseAnnotationLine parses a single annotation line +} + +// parseAnnotationLine parses a single annotation line // Supports both formats: // -// @RouterService name="user-service", prefix="/api" -// @RouterService "user-service", "/api" +// @EndpointService name="user-service", prefix="/api" +// @EndpointService "user-service", "/api" func parseAnnotationLine(line string, lineNum int) (*ParsedAnnotation, error) { // Extract annotation name parts := strings.SplitN(line, "(", 2) if len(parts) == 1 { // No parentheses - might have args without parens or no args - // @RouterService name="value" - // @RouterService + // @EndpointService name="value" + // @EndpointService nameAndArgs := strings.TrimSpace(parts[0]) spaceIdx := strings.Index(nameAndArgs, " ") diff --git a/core/annotation/codegen.go b/core/annotation/codegen.go index 8065e6c5..5e45dd1a 100644 --- a/core/annotation/codegen.go +++ b/core/annotation/codegen.go @@ -196,9 +196,9 @@ func processFileForCodeGen(file *FileToProcess, ctx *RouterServiceContext) error return err } - // Find @RouterService and @Service annotations + // Find @EndpointService and @Service annotations for _, ann := range file.Annotations { - if ann.Name != "RouterService" && ann.Name != "Service" { + if ann.Name != "EndpointService" && ann.Name != "Service" { continue } @@ -217,10 +217,10 @@ func processFileForCodeGen(file *FileToProcess, ctx *RouterServiceContext) error } serviceName, _ = args["name"].(string) } else { - // @RouterService needs name, prefix, middlewares + // @EndpointService needs name, prefix, middlewares args, err := ann.ReadArgs("name", "prefix", "middlewares") if err != nil { - return fmt.Errorf("@RouterService on line %d: %w", ann.Line, err) + return fmt.Errorf("@EndpointService on line %d: %w", ann.Line, err) } serviceName, _ = args["name"].(string) prefix, _ = args["prefix"].(string) @@ -569,8 +569,9 @@ func extractDependencies(file *FileToProcess, service *ServiceGeneration) error // @Inject "@store.implementation" - Service name from config // @Inject service="@store.implementation" - Service name from config (named param) // @Inject "cfg:app.timeout" - Config value injection + // @Inject "cfg:app.timeout", "default" - Config with default value // @Inject "cfg:@jwt.key-path" - Config value via indirection - args, err := ann.ReadArgs("service") + args, err := ann.ReadArgs("service", "default") if err != nil { return fmt.Errorf("@Inject on line %d: %w", ann.Line, err) } @@ -580,6 +581,11 @@ func extractDependencies(file *FileToProcess, service *ServiceGeneration) error serviceName = svc } + var defaultValue string + if def, ok := args["default"].(string); ok { + defaultValue = def + } + if serviceName != "" && ann.TargetName != "" { // ann.TargetName is field name fieldType := fieldTypes[ann.TargetName] @@ -594,7 +600,7 @@ func extractDependencies(file *FileToProcess, service *ServiceGeneration) error ConfigKey: configKey, FieldName: ann.TargetName, FieldType: fieldType, - DefaultValue: "", + DefaultValue: defaultValue, IsIndirect: true, IndirectKey: indirectKey, } @@ -603,7 +609,7 @@ func extractDependencies(file *FileToProcess, service *ServiceGeneration) error ConfigKey: configKey, FieldName: ann.TargetName, FieldType: fieldType, - DefaultValue: "", + DefaultValue: defaultValue, } } } else if after0, ok0 := strings.CutPrefix(serviceName, "@"); ok0 { @@ -734,7 +740,7 @@ func writeGenFile(path string, ctx *RouterServiceContext, existingImports map[st for _, service := range ctx.GeneratedCode.Services { if !service.IsService { - // @RouterService needs deploy and proxy + // @EndpointService needs deploy and proxy needsDeploy = true needsProxy = true } @@ -756,7 +762,7 @@ func writeGenFile(path string, ctx *RouterServiceContext, existingImports map[st needsStrings := false for _, service := range ctx.GeneratedCode.Services { - // From method signatures - ONLY for @RouterService (not @Service) + // From method signatures - ONLY for @EndpointService (not @Service) // @Service doesn't generate proxy methods, so method signatures are not in generated code if !service.IsService { for _, method := range service.Methods { @@ -793,7 +799,18 @@ func writeGenFile(path string, ctx *RouterServiceContext, existingImports map[st } // Filter imports to only used packages - allImports := make(map[string]string) // path -> alias + // Strategy: + // 1. Same path + different aliases → Merge to longest alias (canonical) + // 2. Different paths + same alias → Rename one with counter suffix + + type importEntry struct { + Alias string + Path string + } + + // Step 1: Collect all (alias, path) pairs from source files + var allImportEntries []importEntry + seenCombinations := make(map[string]bool) // "alias:path" -> true // Hardcoded imports - conditionally included based on usage hardcodedImports := map[string]bool{ @@ -802,54 +819,164 @@ func writeGenFile(path string, ctx *RouterServiceContext, existingImports map[st "github.com/primadi/lokstra/lokstra_registry": true, // Always needed } - // First, add imports from updated services + // Collect from updated services for _, service := range ctx.GeneratedCode.Services { for alias, importPath := range service.Imports { - // Skip hardcoded imports that are conditionally included if _, isHardcoded := hardcodedImports[importPath]; isHardcoded { continue } - // Only include if package is actually used in generated code - // This filters out imports from source file that are not used in handlers/dependencies if usedPackages[alias] { - // Use path as key to deduplicate, prefer shorter alias - if existing, exists := allImports[importPath]; !exists || len(alias) < len(existing) { - allImports[importPath] = alias + combo := alias + ":" + importPath + if !seenCombinations[combo] { + allImportEntries = append(allImportEntries, importEntry{Alias: alias, Path: importPath}) + seenCombinations[combo] = true } } } } - // Second, add imports from existing generated file if package is still used + // Collect from existing generated file for alias, importPath := range existingImports { - // Skip hardcoded imports that are conditionally included if _, isHardcoded := hardcodedImports[importPath]; isHardcoded { continue } if usedPackages[alias] { - // Use path as key to deduplicate, prefer shorter alias - if existing, exists := allImports[importPath]; !exists || len(alias) < len(existing) { - allImports[importPath] = alias + combo := alias + ":" + importPath + if !seenCombinations[combo] { + allImportEntries = append(allImportEntries, importEntry{Alias: alias, Path: importPath}) + seenCombinations[combo] = true + } + } + } + + // Step 2: Group by path and find canonical alias (longest) for same path + pathToAliases := make(map[string][]string) // path -> [aliases] + for _, entry := range allImportEntries { + pathToAliases[entry.Path] = append(pathToAliases[entry.Path], entry.Alias) + } + + pathToCanonical := make(map[string]string) // path -> canonical alias (longest) + + for path, aliases := range pathToAliases { + // Find longest alias as canonical; if tie, pick lexicographically smallest (alphabetically first) + canonical := aliases[0] + for i, alias := range aliases { + if i == 0 { + continue + } + if len(alias) > len(canonical) || (len(alias) == len(canonical) && alias < canonical) { + canonical = alias + } + } + pathToCanonical[path] = canonical + } + + // Step 3: Build final import list with conflict resolution + aliasToPath := make(map[string]string) // alias -> path (for conflict detection) + pathToFinalAlias := make(map[string]string) // path -> final alias (after conflict resolution) + var finalImports []importEntry + + for path, canonical := range pathToCanonical { + // Check if canonical alias conflicts with another path + if existingPath, exists := aliasToPath[canonical]; exists && existingPath != path { + // Conflict! Rename this one + newAlias := canonical + counter := 1 + for { + newAlias = fmt.Sprintf("%s_%d", canonical, counter) + if _, taken := aliasToPath[newAlias]; !taken { + break + } + counter++ } + finalImports = append(finalImports, importEntry{Alias: newAlias, Path: path}) + aliasToPath[newAlias] = path + pathToFinalAlias[path] = newAlias + } else { + finalImports = append(finalImports, importEntry{Alias: canonical, Path: path}) + aliasToPath[canonical] = path + pathToFinalAlias[path] = canonical + } + } + + // Step 3.5: Build alias remap based on actual final aliases + // Map: (path, oldAlias) -> finalAlias + aliasRemap := make(map[string]map[string]string) // path -> (oldAlias -> finalAlias) + + for path, aliases := range pathToAliases { + finalAlias := pathToFinalAlias[path] + aliasRemap[path] = make(map[string]string) + + for _, oldAlias := range aliases { + aliasRemap[path][oldAlias] = finalAlias } } - // Third, add "time" if time.Duration is used + // Step 4: Add standard library imports if needed if usedPackages["time"] { - allImports["time"] = "time" + if _, exists := aliasToPath["time"]; !exists { + finalImports = append(finalImports, importEntry{Alias: "time", Path: "time"}) + aliasToPath["time"] = "time" + } } - // Fourth, add "strconv" and "strings" if slice parsing is needed if needsStrconv { - allImports["strconv"] = "strconv" + if _, exists := aliasToPath["strconv"]; !exists { + finalImports = append(finalImports, importEntry{Alias: "strconv", Path: "strconv"}) + aliasToPath["strconv"] = "strconv" + } } + if needsStrings { - allImports["strings"] = "strings" + if _, exists := aliasToPath["strings"]; !exists { + finalImports = append(finalImports, importEntry{Alias: "strings", Path: "strings"}) + aliasToPath["strings"] = "strings" + } } - // Fifth, add "cast" if struct types are used if usedPackages["cast"] { - allImports["github.com/primadi/lokstra/common/cast"] = "cast" + if _, exists := aliasToPath["cast"]; !exists { + finalImports = append(finalImports, importEntry{Alias: "cast", Path: "github.com/primadi/lokstra/common/cast"}) + aliasToPath["cast"] = "github.com/primadi/lokstra/common/cast" + } + } + + // Step 5: Apply alias remapping to all services + // Update method signatures and dependency types to use final aliases + for _, service := range ctx.GeneratedCode.Services { + // Build remap for this service based on its imports + serviceRemap := make(map[string]string) + for oldAlias, path := range service.Imports { + if pathRemap, exists := aliasRemap[path]; exists { + if finalAlias, exists := pathRemap[oldAlias]; exists && finalAlias != oldAlias { + serviceRemap[oldAlias] = finalAlias + } + } + } + + // Remap method signatures + for _, method := range service.Methods { + if method.ParamType != "" { + method.ParamType = remapTypeAliases(method.ParamType, serviceRemap) + } + if method.ReturnType != "" { + method.ReturnType = remapTypeAliases(method.ReturnType, serviceRemap) + } + } + + // Remap dependency types + for _, dep := range service.Dependencies { + if dep.FieldType != "" { + dep.FieldType = remapTypeAliases(dep.FieldType, serviceRemap) + } + } + + // Remap config dependency types + for _, cfg := range service.ConfigDependencies { + if cfg.FieldType != "" { + cfg.FieldType = remapTypeAliases(cfg.FieldType, serviceRemap) + } + } } // Generate code @@ -858,7 +985,7 @@ func writeGenFile(path string, ctx *RouterServiceContext, existingImports map[st "Package": pkgName, "Services": ctx.GeneratedCode.Services, "PreservedSections": ctx.GeneratedCode.PreservedSections, - "AllImports": allImports, + "AllImports": finalImports, "AllStructNames": allStructNames, "NeedsDeploy": needsDeploy, "NeedsProxy": needsProxy, @@ -945,6 +1072,33 @@ func collectPackagesFromType(typeStr string, packages map[string]bool) { } } +// remapTypeAliases remaps package aliases in type strings +// E.g., "models.User" -> "pkgamodel.User" if aliasMap["models"] = "pkgamodel" +// Handles: pkg.Type, *pkg.Type, []pkg.Type, []*pkg.Type, map[pkg.Key]pkg.Value, etc. +func remapTypeAliases(typeStr string, aliasMap map[string]string) string { + if typeStr == "" || len(aliasMap) == 0 { + return typeStr + } + + // Replace all qualified identifiers: pkg.Type + // Pattern: word boundary + package name + dot + identifier + for oldAlias, newAlias := range aliasMap { + if oldAlias == newAlias { + continue // No change needed + } + + // Use regex to match: (^|[^a-zA-Z0-9_])oldAlias\. + // This ensures we match "models.User" but not "mymodels.User" + pattern := `(^|[^a-zA-Z0-9_])` + regexp.QuoteMeta(oldAlias) + `\.` + re := regexp.MustCompile(pattern) + + // Replace with: $1newAlias. + typeStr = re.ReplaceAllString(typeStr, "${1}"+newAlias+".") + } + + return typeStr +} + // getPackageName gets package name from a folder func getPackageName(folderPath string) (string, error) { files, err := os.ReadDir(folderPath) @@ -1661,13 +1815,11 @@ func parseSliceFromConfig(fieldType, configKey, defaultValue string) string { // genTemplate is the template for zz_generated.lokstra.go var genTemplate = template.Must(template.New("gen").Funcs(template.FuncMap{ - "hasReturnValue": hasReturnValue, - "extractReturnType": extractReturnType, - "quote": func(s string) string { return fmt.Sprintf("%q", s) }, - "join": strings.Join, - "trimPrefix": strings.TrimPrefix, - "trimSuffix": strings.TrimSuffix, - "notEmpty": func(s string) bool { return strings.TrimSpace(s) != "" }, + "quote": func(s string) string { return fmt.Sprintf("%q", s) }, + "join": strings.Join, + "trimPrefix": strings.TrimPrefix, + "trimSuffix": strings.TrimSuffix, + "notEmpty": func(s string) bool { return strings.TrimSpace(s) != "" }, "getDefaultValue": func(fieldType, defaultValue string) string { if defaultValue != "" { @@ -1789,7 +1941,7 @@ var genTemplate = template.Must(template.New("gen").Funcs(template.FuncMap{ }, }).Parse(`// AUTO-GENERATED CODE - DO NOT EDIT // Generated by lokstra-annotation from annotations in this folder -// Annotations: @RouterService, @Service, @Inject, @Route +// Annotations: @EndpointService, @Service, @Inject, @Route // // @Inject supports: // - "service-name" : Direct service injection @@ -1807,8 +1959,8 @@ import ( "github.com/primadi/lokstra/core/proxy" {{- end }} "github.com/primadi/lokstra/lokstra_registry" -{{- range $path, $alias := .AllImports }} - {{$alias}} "{{$path}}" +{{- range $entry := .AllImports }} + {{$entry.Alias}} "{{$entry.Path}}" {{- end }} ) @@ -1934,7 +2086,7 @@ func {{$service.StructName}}Factory(deps map[string]any, config map[string]any) } // {{$service.RemoteTypeName}}Factory creates a remote HTTP client for {{$service.InterfaceName}} -// Auto-generated from @RouterService annotation +// Auto-generated from @EndpointService annotation func {{$service.RemoteTypeName}}Factory(deps, config map[string]any) any { proxyService, ok := config["remote"].(*proxy.Service) if !ok { @@ -1945,7 +2097,7 @@ func {{$service.RemoteTypeName}}Factory(deps, config map[string]any) any { // Register{{$service.StructName}} registers the {{$service.ServiceName}} with the registry // Auto-generated from annotations: -// - @RouterService name={{quote $service.ServiceName}}, prefix={{quote $service.Prefix}} +// - @EndpointService name={{quote $service.ServiceName}}, prefix={{quote $service.Prefix}} {{- if or $service.Dependencies $service.ConfigDependencies}} // - @Inject annotations {{- end}} @@ -1992,14 +2144,6 @@ func Register{{$service.StructName}}() { {{end}} {{range $filename, $code := .PreservedSections}}{{$code}}{{end}}`)) -func hasReturnValue(route string) bool { - return !strings.Contains(route, "error") -} - -func extractReturnType(route string) string { - return "*domain.User" -} - // isStructType checks if a type is a struct (not primitive, slice, map, or interface) func isStructType(fieldType string) bool { // Empty type or primitive types diff --git a/core/annotation/codegen_inject_test_example.go b/core/annotation/codegen_inject_test_example.go index 78e9d126..9310d20f 100644 --- a/core/annotation/codegen_inject_test_example.go +++ b/core/annotation/codegen_inject_test_example.go @@ -5,7 +5,7 @@ package annotation /* Example 1: @Inject with cfg: prefix for config values -// @RouterService name="auth-service", prefix="/api/auth" +// @EndpointService name="auth-service", prefix="/api/auth" type AuthService struct { // @Inject "cfg:app.timeout" Timeout time.Duration @@ -26,7 +26,7 @@ config.yaml: /* Example 2: @Inject with cfg:@ prefix (indirect config) -// @RouterService name="auth-service", prefix="/api/auth" +// @EndpointService name="auth-service", prefix="/api/auth" type AuthService struct { // @Inject "cfg:@jwt.key-path" JWTSecret string @@ -75,7 +75,7 @@ config.yaml: /* Example 4: Mixed injection patterns -// @RouterService name="user-service", prefix="/api/users" +// @EndpointService name="user-service", prefix="/api/users" type UserService struct { // Direct service injection // @Inject "user-repository" diff --git a/core/annotation/complex_processor.go b/core/annotation/complex_processor.go index 6dca4dc3..d296ea0c 100644 --- a/core/annotation/complex_processor.go +++ b/core/annotation/complex_processor.go @@ -22,7 +22,7 @@ import ( // ProcessComplexAnnotations processes annotations with parallel folder processing. // rootPath is a slice of directories to scan. Each directory and its subdirectories -// will be scanned for .go files containing @RouterService annotations. +// will be scanned for .go files containing @EndpointService annotations. func ProcessComplexAnnotations(rootPath []string, maxWorkers int, onProcessRouterService func(*RouterServiceContext) error) (bool, error) { // Find all folders containing .go files from all root paths @@ -170,7 +170,7 @@ func ProcessPerFolder(folderPath string, onProcessRouterService func(*RouterServ forceRegenerate = true } - // Step 2: Scan .go files containing @RouterService + // Step 2: Scan .go files containing @EndpointService skipped, updated, deleted, err := scanFolderFiles(folderPath, cache) if err != nil { return false, fmt.Errorf("failed to scan files: %w", err) @@ -321,13 +321,13 @@ type ServiceGeneration struct { RouteMiddlewares map[string][]string // methodName -> []middleware (per-route middleware) Methods map[string]*MethodSignature // methodName -> signature Dependencies map[string]*DependencyInfo // serviceName -> field info - ConfigDependencies map[string]*ConfigInfo // configKey -> config field info (for @InjectCfgValue) + ConfigDependencies map[string]*ConfigInfo // configKey -> config field info (for @Inject "cfg:...") Imports map[string]string // alias -> import path (e.g., "domain" -> ".../.../domain") StructName string InterfaceName string RemoteTypeName string SourceFile string - IsService bool // true if @Service, false if @RouterService + IsService bool // true if @Service, false if @EndpointService HasInitMethod bool // true if Init() or Init() error method exists InitReturnsError bool // true if Init() returns error, false if Init() has no return } @@ -341,7 +341,7 @@ type DependencyInfo struct { ConfigKey string // e.g., "store.implementation" (only if IsConfigBased=true) } -// ConfigInfo holds config injection information for @InjectCfgValue +// ConfigInfo holds config injection information for @Inject "cfg:..." type ConfigInfo struct { ConfigKey string // e.g., "auth.jwt-secret" or "@jwt.key-path" for indirection FieldName string // e.g., "jwtSecret" @@ -417,7 +417,7 @@ func scanFolderFiles(folderPath string, cache *FolderCache) ([]*FileToProcess, [ fullPath := filepath.Join(folderPath, file.Name()) - // Quick check: does file contain @RouterService or @Service? + // Quick check: does file contain @EndpointService or @Service? hasAnnotations, err := fileContainsServiceAnnotations(fullPath) if err != nil { // Cleanup before returning error @@ -477,7 +477,7 @@ func scanFolderFiles(folderPath string, cache *FolderCache) ([]*FileToProcess, [ return skipped, updated, deleted, nil } -// fileContainsServiceAnnotations quickly checks if file contains @RouterService or @Service annotation. +// fileContainsServiceAnnotations quickly checks if file contains @EndpointService or @Service annotation. // Uses same parsing logic as ParseFileAnnotations for consistency. // Only matches when annotation is at the start of comment content (after // and spaces). // Ignores TAB-indented annotations (Go code examples in documentation). @@ -496,8 +496,8 @@ func fileContainsServiceAnnotations(path string) (bool, error) { // Check for // comment if after, ok := bytes.CutPrefix(trimmedLine, []byte("//")); ok { // CRITICAL: Detect Go code examples (TAB-indented after //) - // Valid annotation: // @RouterService - // Invalid annotation: // @RouterService (TAB - code example) + // Valid annotation: // @EndpointService + // Invalid annotation: // @EndpointService (TAB - code example) // Find the position of // in original line commentPos := bytes.Index(line, []byte("//")) @@ -513,11 +513,11 @@ func fileContainsServiceAnnotations(path string) (bool, error) { // Check for multiple spaces or single TAB trimmedAfter := bytes.TrimLeft(afterComment, " \t") - // Check for @RouterService or @Service - if bytes.HasPrefix(trimmedAfter, []byte("@RouterService")) || bytes.HasPrefix(trimmedAfter, []byte("@Service")) { + // Check for @EndpointService or @Service + if bytes.HasPrefix(trimmedAfter, []byte("@EndpointService")) || bytes.HasPrefix(trimmedAfter, []byte("@Service")) { leadingWhitespace := afterComment[:len(afterComment)-len(trimmedAfter)] - // Allow single space only (normal comment: "// @RouterService") + // Allow single space only (normal comment: "// @EndpointService") // Reject TAB or multiple spaces if len(leadingWhitespace) > 1 || (len(leadingWhitespace) == 1 && leadingWhitespace[0] == '\t') { // Indented - skip it @@ -530,7 +530,7 @@ func fileContainsServiceAnnotations(path string) (bool, error) { // Also check trimmed version for backward compatibility after = bytes.TrimSpace(after) - if bytes.HasPrefix(after, []byte("@RouterService")) || bytes.HasPrefix(after, []byte("@Service")) { + if bytes.HasPrefix(after, []byte("@EndpointService")) || bytes.HasPrefix(after, []byte("@Service")) { // Double-check: make sure it's not TAB-indented commentPos := bytes.Index(line, []byte("//")) if commentPos != -1 && commentPos+2 < len(line) { @@ -556,7 +556,7 @@ func fileContainsServiceAnnotations(path string) (bool, error) { return false, scanner.Err() } -// fileContainsRouterService quickly checks if file contains @RouterService annotation. +// fileContainsRouterService quickly checks if file contains @EndpointService annotation. // Deprecated: Use fileContainsServiceAnnotations instead. // Kept for backward compatibility with tests. func fileContainsRouterService(path string) (bool, error) { @@ -774,7 +774,7 @@ func generateImportFile(startPath string, packages []string) error { var buf bytes.Buffer buf.WriteString("// AUTO-GENERATED CODE - DO NOT EDIT\n") buf.WriteString("// Generated by lokstra-annotation to auto-register services via init()\n") - buf.WriteString("// This file imports all packages containing @Service or @RouterService annotations\n\n") + buf.WriteString("// This file imports all packages containing @Service or @EndpointService annotations\n\n") buf.WriteString("package main\n\n") buf.WriteString("import (\n") for _, pkg := range sortedPackages { diff --git a/core/annotation/config_injection_test.go b/core/annotation/config_injection_test.go index a56ee2f6..66643a4e 100644 --- a/core/annotation/config_injection_test.go +++ b/core/annotation/config_injection_test.go @@ -46,7 +46,7 @@ func (s *MySQLStore) SaveUser(user *User) error { return nil } -// @RouterService name="user-service", prefix="/api/users" +// @EndpointService name="user-service", prefix="/api/users" type UserService struct { // Config-based injection - service name from config! // @Inject "cfg:store.implementation" @@ -56,8 +56,8 @@ type UserService struct { // @Inject "logger" Logger any - // Config value injection (existing @InjectCfgValue) - // @InjectCfgValue "app.name" + // Config value injection + // @Inject "cfg:app.name" AppName string } diff --git a/core/annotation/examples/annotation_parsing/README.md b/core/annotation/examples/annotation_parsing/README.md index 9ba94538..b4418975 100644 --- a/core/annotation/examples/annotation_parsing/README.md +++ b/core/annotation/examples/annotation_parsing/README.md @@ -21,20 +21,20 @@ go run . ## Expected Output The parser should find **4 valid annotations**: -1. `@RouterService` on `UserService` struct +1. `@EndpointService` on `UserService` struct 2. `@Inject` on `UserRepo` field 3. `@Route` on `GetByID` method 4. `@Route` on `Create` method And should **IGNORE** these (indented in documentation): -- Line 8: `@RouterService` in RegisterMiddleware doc (TAB-indented) +- Line 8: `@EndpointService` in RegisterMiddleware doc (TAB-indented) - Line 22: `@Route` in AnotherFunction doc (multi-space indented) ## Rules ### Valid Annotation Format ```go -// @RouterService name="service-name" +// @EndpointService name="service-name" type MyService struct {} ``` @@ -44,7 +44,7 @@ type MyService struct {} ```go // Example: // -// @RouterService name="example" +// @EndpointService name="example" // // The above is ignored ``` @@ -53,14 +53,14 @@ type MyService struct {} ```go // Example: // -// @RouterService name="example" +// @EndpointService name="example" // // The above is ignored (3+ spaces) ``` **Too many empty lines:** ```go -// @RouterService name="example" +// @EndpointService name="example" // // // diff --git a/core/annotation/examples/annotation_parsing/annotation_example.go b/core/annotation/examples/annotation_parsing/annotation_example.go index e8f4e5b4..5395bbb2 100644 --- a/core/annotation/examples/annotation_parsing/annotation_example.go +++ b/core/annotation/examples/annotation_parsing/annotation_example.go @@ -5,7 +5,7 @@ package main // // Example of annotation in code (this should be IGNORED - TAB indented): // -// @RouterService name="example-service", prefix="/api/example" +// @EndpointService name="example-service", prefix="/api/example" // // The above annotation is indented with TAB, so it's treated as code example. type RegisterMiddleware struct{} @@ -23,7 +23,7 @@ func (r *RegisterMiddleware) Handle() { // The above is indented with multiple spaces, treated as code example. func AnotherFunction() {} -// @RouterService name="user-service", prefix="/api/users" +// @EndpointService name="user-service", prefix="/api/users" type UserService struct { // @Inject "user-repository" UserRepo UserRepository diff --git a/core/annotation/examples/annotation_parsing/main.go b/core/annotation/examples/annotation_parsing/main.go index 2f0ca0f5..2598c691 100644 --- a/core/annotation/examples/annotation_parsing/main.go +++ b/core/annotation/examples/annotation_parsing/main.go @@ -40,7 +40,7 @@ func main() { fmt.Println(strings.Repeat("=", 70)) fmt.Println("\nExpected annotations:") - fmt.Println(" 1. @RouterService on UserService (line 27)") + fmt.Println(" 1. @EndpointService on UserService (line 27)") fmt.Println(" 2. @Inject on UserRepo field (line 30)") fmt.Println(" 3. @Route on GetByID method (line 34)") fmt.Println(" 4. @Route on Create method (line 38)") diff --git a/core/annotation/internal/multifile_test/auth_service.go b/core/annotation/internal/multifile_test/auth_service.go index f945c5cc..92020675 100644 --- a/core/annotation/internal/multifile_test/auth_service.go +++ b/core/annotation/internal/multifile_test/auth_service.go @@ -5,7 +5,7 @@ import ( "github.com/primadi/lokstra/core/service" ) -// @RouterService name="auth-service", prefix="/api/v1/auth" +// @EndpointService name="auth-service", prefix="/api/v1/auth" type AuthService struct { // @Inject "auth-repo" AuthRepo *service.Cached[any] diff --git a/core/annotation/internal/multifile_test/config_eager_service.go b/core/annotation/internal/multifile_test/config_eager_service.go index 61a2f03a..e67923ed 100644 --- a/core/annotation/internal/multifile_test/config_eager_service.go +++ b/core/annotation/internal/multifile_test/config_eager_service.go @@ -13,7 +13,7 @@ type RedisClient struct { Addr string } -// @RouterService name="config-eager-service", prefix="/api/v1/config" +// @EndpointService name="config-eager-service", prefix="/api/v1/config" type ConfigEagerService struct { // @Inject "user-repository" UserRepo *service.Cached[any] // Lazy (wrapped in service.Cached) diff --git a/core/annotation/internal/multifile_test/config_service.go b/core/annotation/internal/multifile_test/config_service.go index b19188cd..dbfc2f74 100644 --- a/core/annotation/internal/multifile_test/config_service.go +++ b/core/annotation/internal/multifile_test/config_service.go @@ -2,7 +2,7 @@ package main import "github.com/primadi/lokstra/core/service" -// @RouterService name="config-service", prefix="/api/config" +// @EndpointService name="config-service", prefix="/api/config" type ConfigService struct { // @Inject "config-repo" ConfigRepo *service.Cached[any] diff --git a/core/annotation/internal/multifile_test/import_alias_test/TEST_SUMMARY.md b/core/annotation/internal/multifile_test/import_alias_test/TEST_SUMMARY.md new file mode 100644 index 00000000..99fac73b --- /dev/null +++ b/core/annotation/internal/multifile_test/import_alias_test/TEST_SUMMARY.md @@ -0,0 +1,133 @@ +# Import Alias Merging Tests - Summary + +## Location +- Test file: `core/annotation/test/import_alias_merging_test.go` +- Example directory: `core/annotation/internal/multifile_test/import_alias_test/` + +## Test Cases Created + +### 1. TestImportAlias_DifferentPathsSameAlias ✅ FIXED +**Scenario**: Two different import paths using the same alias +```go +// service_a.go +import models "myapp/pkga" + +// service_b.go +import models "myapp/pkgb" +``` + +**Expected Behavior**: One alias should be renamed to avoid conflict (e.g., `models` and `models_1`) + +**Status**: ✅ **FIXED** - Conflict detection and renaming now works correctly! +```go +import ( + models "myapp/pkga" // First path keeps original alias + models_1 "myapp/pkgb" // Second path gets renamed +) + +// ServiceBRemote correctly uses models_1 +func (s *ServiceBRemote) GetUsers() (*models_1.User, error) { ... } +``` + +**What Was Fixed**: +1. Added `aliasToAllPaths` map to track ALL paths for each alias (not just last one) +2. Fixed conflict detection to use `aliasToAllPaths` instead of `aliasToPath` +3. Added `pathsWithConflicts` tracking to prevent Third pass from overriding conflict resolution +4. Created `updateTypeWithNewAliasFromOriginal()` function to update type references correctly + +--- + +### 2. TestImportAlias_SamePathDifferentAliases ✅ FIXED +**Scenario**: Same import path with different aliases across services +```go +// service_c.go +import userentity "myapp/pkga" + +// service_d.go +import pkgamodel "myapp/pkga" +``` + +**Expected Behavior**: Should merge to single alias (preferring longer/more descriptive) + +**Status**: ✅ **FIXED** - Import merging and type reference updating both work correctly! +```go +// Import section merges to longest alias: +import userentity "myapp/pkga" + +// Generated methods now use the merged alias: +func (s *ServiceCRemote) GetEntity() (*userentity.User, error) { ... } +func (s *ServiceDRemote) GetData() (*userentity.User, error) { ... } +``` + +**What Was Fixed**: +1. Added logic to update method signatures (`ParamType` and `ReturnType`) with new aliases +2. Created `updateTypeWithNewAliasFromOriginal()` to handle type reference updates using original imports +3. Enhanced `updateTypeWithNewAlias()` to handle `*` and `[]` prefixes correctly + +--- + +## Files Created + +### Test Infrastructure +- `core/annotation/test/import_alias_merging_test.go` - Main test file with 2 test functions + +### Example Services (for manual testing) +- `core/annotation/internal/multifile_test/import_alias_test/` + - `main.go` - Bootstrap entry point + - `service_a.go` - Uses `models "pkga"` + - `service_b.go` - Uses `models "pkgb"` (conflict with service_a) + - `service_c.go` - Uses `userentity "pkga"` + - `service_d.go` - Uses `pkgamodel "pkga"` (same path as service_c) + - `pkga/models.go` - Domain models for package A + - `pkgb/models.go` - Domain models for package B + +## How to Run Tests + +```bash +cd core/annotation/test +go test -v -run TestImportAlias +``` + +**Result**: ✅ Both tests PASS! + +## Code Changes + +### Modified Files +- `core/annotation/codegen.go` + - Lines ~810-895: Enhanced conflict detection with `aliasToAllPaths` + - Lines ~880-925: Added `pathsWithConflicts` tracking + - Lines ~947-984: Added method signature updates with `updateTypeWithNewAliasFromOriginal()` + - Lines ~1799-1890: Added `updateTypeWithNewAliasFromOriginal()` function + +### Key Improvements +1. **Proper Conflict Detection**: Now detects all paths using the same alias, not just the last one +2. **Type Reference Updates**: Method signatures in generated proxy code now use correct aliases +3. **Alias Precedence**: Conflicts are resolved first, then multiple aliases for same path are merged +4. **Pointer/Array Handling**: Type update function correctly handles `*pkg.Type` and `[]pkg.Type` + +## Test Results + +``` +=== RUN TestImportAlias_DifferentPathsSameAlias +⚠️ Import alias conflict detected for 'models'. Auto-renaming: + ✓ myapp/pkga → 'models' + ✓ myapp/pkgb → 'models_1' +--- PASS: TestImportAlias_DifferentPathsSameAlias (0.06s) + +=== RUN TestImportAlias_SamePathDifferentAliases +--- PASS: TestImportAlias_SamePathDifferentAliases (0.06s) + +PASS +ok github.com/primadi/lokstra/core/annotation/test 0.888s +``` + +## Summary + +Both bugs have been successfully fixed! The annotation code generator now: + +✅ **Correctly detects and renames conflicting import aliases** +✅ **Merges multiple aliases for the same import path** +✅ **Updates all type references in generated code to use the correct aliases** +✅ **Handles pointer and array types properly** + +The generated code compiles without errors and all tests pass! diff --git a/core/annotation/internal/multifile_test/import_alias_test/main.go b/core/annotation/internal/multifile_test/import_alias_test/main.go new file mode 100644 index 00000000..1de69a17 --- /dev/null +++ b/core/annotation/internal/multifile_test/import_alias_test/main.go @@ -0,0 +1,7 @@ +package main + +import "github.com/primadi/lokstra/lokstra_init" + +func main() { + lokstra_init.Bootstrap() +} diff --git a/core/annotation/internal/multifile_test/import_alias_test/pkga/models.go b/core/annotation/internal/multifile_test/import_alias_test/pkga/models.go new file mode 100644 index 00000000..6dca524a --- /dev/null +++ b/core/annotation/internal/multifile_test/import_alias_test/pkga/models.go @@ -0,0 +1,13 @@ +package pkga + +// User represents a user from package A +type User struct { + ID string + Name string +} + +// Request represents a request from package A +type Request struct { + Action string + Data string +} diff --git a/core/annotation/internal/multifile_test/import_alias_test/pkgb/models.go b/core/annotation/internal/multifile_test/import_alias_test/pkgb/models.go new file mode 100644 index 00000000..4fe2f039 --- /dev/null +++ b/core/annotation/internal/multifile_test/import_alias_test/pkgb/models.go @@ -0,0 +1,14 @@ +package pkgb + +// User represents a user from package B (different from pkga.User) +type User struct { + UserID string + FullName string + Email string +} + +// Response represents a response from package B +type Response struct { + Status string + Message string +} diff --git a/core/annotation/internal/multifile_test/import_alias_test/service_a.go b/core/annotation/internal/multifile_test/import_alias_test/service_a.go new file mode 100644 index 00000000..d7808f97 --- /dev/null +++ b/core/annotation/internal/multifile_test/import_alias_test/service_a.go @@ -0,0 +1,23 @@ +package main + +import ( + models "github.com/primadi/lokstra/core/annotation/internal/multifile_test/import_alias_test/pkga" +) + +// @EndpointService name="service-a", prefix="/api/a" +type ServiceA struct { +} + +// @Route "GET /users" +// This service uses pkga with alias "models" +func (s *ServiceA) GetUsers() (*models.User, error) { + return &models.User{ + ID: "a1", + Name: "User from A", + }, nil +} + +// @Route "POST /process" +func (s *ServiceA) Process(req *models.Request) error { + return nil +} diff --git a/core/annotation/internal/multifile_test/import_alias_test/service_b.go b/core/annotation/internal/multifile_test/import_alias_test/service_b.go new file mode 100644 index 00000000..c687eb59 --- /dev/null +++ b/core/annotation/internal/multifile_test/import_alias_test/service_b.go @@ -0,0 +1,27 @@ +package main + +import ( + models "github.com/primadi/lokstra/core/annotation/internal/multifile_test/import_alias_test/pkgb" +) + +// @EndpointService name="service-b", prefix="/api/b" +type ServiceB struct { +} + +// @Route "GET /users" +// This service uses pkgb with alias "models" (CONFLICT: same alias, different path) +func (s *ServiceB) GetUsers() (*models.User, error) { + return &models.User{ + UserID: "b1", + FullName: "User from B", + Email: "user@b.com", + }, nil +} + +// @Route "POST /respond" +func (s *ServiceB) Respond() (*models.Response, error) { + return &models.Response{ + Status: "ok", + Message: "Response from B", + }, nil +} diff --git a/core/annotation/internal/multifile_test/import_alias_test/service_c.go b/core/annotation/internal/multifile_test/import_alias_test/service_c.go new file mode 100644 index 00000000..457c2797 --- /dev/null +++ b/core/annotation/internal/multifile_test/import_alias_test/service_c.go @@ -0,0 +1,23 @@ +package main + +import ( + userentity "github.com/primadi/lokstra/core/annotation/internal/multifile_test/import_alias_test/pkga" +) + +// @EndpointService name="service-c", prefix="/api/c" +type ServiceC struct { +} + +// @Route "GET /entity" +// This service uses pkga with alias "userentity" +func (s *ServiceC) GetEntity() (*userentity.User, error) { + return &userentity.User{ + ID: "c1", + Name: "User from C", + }, nil +} + +// @Route "POST /request" +func (s *ServiceC) HandleRequest(req *userentity.Request) error { + return nil +} diff --git a/core/annotation/internal/multifile_test/import_alias_test/service_d.go b/core/annotation/internal/multifile_test/import_alias_test/service_d.go new file mode 100644 index 00000000..192030e1 --- /dev/null +++ b/core/annotation/internal/multifile_test/import_alias_test/service_d.go @@ -0,0 +1,23 @@ +package main + +import ( + pkgamodel "github.com/primadi/lokstra/core/annotation/internal/multifile_test/import_alias_test/pkga" +) + +// @EndpointService name="service-d", prefix="/api/d" +type ServiceD struct { +} + +// @Route "GET /data" +// This service uses pkga with alias "pkgamodel" (same path as service-c, different alias) +func (s *ServiceD) GetData() (*pkgamodel.User, error) { + return &pkgamodel.User{ + ID: "d1", + Name: "User from D", + }, nil +} + +// @Route "POST /action" +func (s *ServiceD) PerformAction(req *pkgamodel.Request) error { + return nil +} diff --git a/core/annotation/internal/multifile_test/import_alias_test/zz_cache.lokstra.json b/core/annotation/internal/multifile_test/import_alias_test/zz_cache.lokstra.json new file mode 100644 index 00000000..3f152f22 --- /dev/null +++ b/core/annotation/internal/multifile_test/import_alias_test/zz_cache.lokstra.json @@ -0,0 +1,47 @@ +{ + "version": 1, + "files": { + "service_a.go": { + "filename": "service_a.go", + "checksum": "1fdd987a2ebc4156f7fd6e4f6e61e3fa87128afa2642a16b2dd1dbe711661bee", + "annotations": 3, + "last_scan": "2026-01-11T02:58:24.8459115+07:00", + "generated": [ + "zz_generated.lokstra.go" + ], + "generated_mod_time": "2026-01-11T02:58:24.8459115+07:00" + }, + "service_b.go": { + "filename": "service_b.go", + "checksum": "22bd7af8687e285bd7be1f9e8dbb6927f7d357880bae8a88610369e6ff0736a4", + "annotations": 3, + "last_scan": "2026-01-11T02:58:24.8459115+07:00", + "generated": [ + "zz_generated.lokstra.go" + ], + "generated_mod_time": "2026-01-11T02:58:24.8459115+07:00" + }, + "service_c.go": { + "filename": "service_c.go", + "checksum": "63f7e3acbc9539c6d3814a9d9a4cb1c9bd290ccfa0aa9d376a6ee3102711e587", + "annotations": 3, + "last_scan": "2026-01-11T02:58:24.8459115+07:00", + "generated": [ + "zz_generated.lokstra.go" + ], + "generated_mod_time": "2026-01-11T02:58:24.8459115+07:00" + }, + "service_d.go": { + "filename": "service_d.go", + "checksum": "a1786d9d7b7f728ea6bf566bc61d7d10d59fac423ba118083d549e5c7ac3fae4", + "annotations": 3, + "last_scan": "2026-01-11T02:58:24.8459115+07:00", + "generated": [ + "zz_generated.lokstra.go" + ], + "generated_mod_time": "2026-01-11T02:58:24.8459115+07:00" + } + }, + "updated_at": "2026-01-11T02:58:24.8459115+07:00", + "generated_checksum": "6016d7bf7a9c158a806073d6157a18264ba973e3ac3d48eb1efaaf95a1465fa3" +} \ No newline at end of file diff --git a/core/annotation/internal/multifile_test/import_alias_test/zz_generated.lokstra.go b/core/annotation/internal/multifile_test/import_alias_test/zz_generated.lokstra.go new file mode 100644 index 00000000..f3dfc412 --- /dev/null +++ b/core/annotation/internal/multifile_test/import_alias_test/zz_generated.lokstra.go @@ -0,0 +1,340 @@ +// AUTO-GENERATED CODE - DO NOT EDIT +// Generated by lokstra-annotation from annotations in this folder +// Annotations: @EndpointService, @Service, @Inject, @Route +// +// @Inject supports: +// - "service-name" : Direct service injection +// - "@config.key" : Service name from config value +// - "cfg:config.key" : Config value injection +// - "cfg:@config.key" : Config value via indirection + +package main + +import ( + "github.com/primadi/lokstra/core/deploy" + "github.com/primadi/lokstra/core/proxy" + "github.com/primadi/lokstra/lokstra_registry" + userentity "github.com/primadi/lokstra/core/annotation/internal/multifile_test/import_alias_test/pkga" + models "github.com/primadi/lokstra/core/annotation/internal/multifile_test/import_alias_test/pkgb" +) + +// Auto-register on package import +func init() { + RegisterServiceA() + RegisterServiceB() + RegisterServiceC() + RegisterServiceD() +} + +// ============================================================ +// FILE: service_a.go +// ============================================================ + +// ServiceARemote implements ServiceAInterface with HTTP proxy +// Auto-generated from ServiceA interface methods +type ServiceARemote struct { + proxyService *proxy.Service +} + +// NewServiceARemote creates a new remote service-a proxy +func NewServiceARemote(proxyService *proxy.Service) *ServiceARemote { + return &ServiceARemote{ + proxyService: proxyService, + } +} + +// GetUsers via HTTP +// Generated from: @Route "GET /users" +func (s *ServiceARemote) GetUsers() (*userentity.User, error) { + return proxy.CallWithData[*userentity.User](s.proxyService, "GetUsers", nil) +} + +// Process via HTTP +// Generated from: @Route "POST /process" +func (s *ServiceARemote) Process(p *userentity.Request) error { + return proxy.Call(s.proxyService, "Process", p) +} + + +func ServiceAFactory(deps map[string]any, config map[string]any) any { + svc := &ServiceA{ + } + + return svc +} + +// ServiceARemoteFactory creates a remote HTTP client for ServiceAInterface +// Auto-generated from @EndpointService annotation +func ServiceARemoteFactory(deps, config map[string]any) any { + proxyService, ok := config["remote"].(*proxy.Service) + if !ok { + panic("remote factory requires 'remote' (proxy.Service) in config") + } + return NewServiceARemote(proxyService) +} + +// RegisterServiceA registers the service-a with the registry +// Auto-generated from annotations: +// - @EndpointService name="service-a", prefix="/api/a" +// - @Route annotations on methods +func RegisterServiceA() { + // Register service type with router configuration + lokstra_registry.RegisterRouterServiceType("service-a-factory", + ServiceAFactory, + ServiceARemoteFactory, + &deploy.ServiceTypeConfig{ + PathPrefix: "/api/a", + Middlewares: []string{ }, + RouteOverrides: map[string]deploy.RouteConfig{ + "GetUsers": { + Path: "GET /users", + }, + "Process": { + Path: "POST /process", + }, + }, + }, + ) + + // Register lazy service with auto-detected dependencies + lokstra_registry.RegisterLazyService("service-a", + "service-a-factory", + map[string]any{ + }) +} + + +// ============================================================ +// FILE: service_b.go +// ============================================================ + +// ServiceBRemote implements ServiceBInterface with HTTP proxy +// Auto-generated from ServiceB interface methods +type ServiceBRemote struct { + proxyService *proxy.Service +} + +// NewServiceBRemote creates a new remote service-b proxy +func NewServiceBRemote(proxyService *proxy.Service) *ServiceBRemote { + return &ServiceBRemote{ + proxyService: proxyService, + } +} + +// GetUsers via HTTP +// Generated from: @Route "GET /users" +func (s *ServiceBRemote) GetUsers() (*models.User, error) { + return proxy.CallWithData[*models.User](s.proxyService, "GetUsers", nil) +} + +// Respond via HTTP +// Generated from: @Route "POST /respond" +func (s *ServiceBRemote) Respond() (*models.Response, error) { + return proxy.CallWithData[*models.Response](s.proxyService, "Respond", nil) +} + + +func ServiceBFactory(deps map[string]any, config map[string]any) any { + svc := &ServiceB{ + } + + return svc +} + +// ServiceBRemoteFactory creates a remote HTTP client for ServiceBInterface +// Auto-generated from @EndpointService annotation +func ServiceBRemoteFactory(deps, config map[string]any) any { + proxyService, ok := config["remote"].(*proxy.Service) + if !ok { + panic("remote factory requires 'remote' (proxy.Service) in config") + } + return NewServiceBRemote(proxyService) +} + +// RegisterServiceB registers the service-b with the registry +// Auto-generated from annotations: +// - @EndpointService name="service-b", prefix="/api/b" +// - @Route annotations on methods +func RegisterServiceB() { + // Register service type with router configuration + lokstra_registry.RegisterRouterServiceType("service-b-factory", + ServiceBFactory, + ServiceBRemoteFactory, + &deploy.ServiceTypeConfig{ + PathPrefix: "/api/b", + Middlewares: []string{ }, + RouteOverrides: map[string]deploy.RouteConfig{ + "GetUsers": { + Path: "GET /users", + }, + "Respond": { + Path: "POST /respond", + }, + }, + }, + ) + + // Register lazy service with auto-detected dependencies + lokstra_registry.RegisterLazyService("service-b", + "service-b-factory", + map[string]any{ + }) +} + + +// ============================================================ +// FILE: service_c.go +// ============================================================ + +// ServiceCRemote implements ServiceCInterface with HTTP proxy +// Auto-generated from ServiceC interface methods +type ServiceCRemote struct { + proxyService *proxy.Service +} + +// NewServiceCRemote creates a new remote service-c proxy +func NewServiceCRemote(proxyService *proxy.Service) *ServiceCRemote { + return &ServiceCRemote{ + proxyService: proxyService, + } +} + +// GetEntity via HTTP +// Generated from: @Route "GET /entity" +func (s *ServiceCRemote) GetEntity() (*userentity.User, error) { + return proxy.CallWithData[*userentity.User](s.proxyService, "GetEntity", nil) +} + +// HandleRequest via HTTP +// Generated from: @Route "POST /request" +func (s *ServiceCRemote) HandleRequest(p *userentity.Request) error { + return proxy.Call(s.proxyService, "HandleRequest", p) +} + + +func ServiceCFactory(deps map[string]any, config map[string]any) any { + svc := &ServiceC{ + } + + return svc +} + +// ServiceCRemoteFactory creates a remote HTTP client for ServiceCInterface +// Auto-generated from @EndpointService annotation +func ServiceCRemoteFactory(deps, config map[string]any) any { + proxyService, ok := config["remote"].(*proxy.Service) + if !ok { + panic("remote factory requires 'remote' (proxy.Service) in config") + } + return NewServiceCRemote(proxyService) +} + +// RegisterServiceC registers the service-c with the registry +// Auto-generated from annotations: +// - @EndpointService name="service-c", prefix="/api/c" +// - @Route annotations on methods +func RegisterServiceC() { + // Register service type with router configuration + lokstra_registry.RegisterRouterServiceType("service-c-factory", + ServiceCFactory, + ServiceCRemoteFactory, + &deploy.ServiceTypeConfig{ + PathPrefix: "/api/c", + Middlewares: []string{ }, + RouteOverrides: map[string]deploy.RouteConfig{ + "GetEntity": { + Path: "GET /entity", + }, + "HandleRequest": { + Path: "POST /request", + }, + }, + }, + ) + + // Register lazy service with auto-detected dependencies + lokstra_registry.RegisterLazyService("service-c", + "service-c-factory", + map[string]any{ + }) +} + + +// ============================================================ +// FILE: service_d.go +// ============================================================ + +// ServiceDRemote implements ServiceDInterface with HTTP proxy +// Auto-generated from ServiceD interface methods +type ServiceDRemote struct { + proxyService *proxy.Service +} + +// NewServiceDRemote creates a new remote service-d proxy +func NewServiceDRemote(proxyService *proxy.Service) *ServiceDRemote { + return &ServiceDRemote{ + proxyService: proxyService, + } +} + +// GetData via HTTP +// Generated from: @Route "GET /data" +func (s *ServiceDRemote) GetData() (*userentity.User, error) { + return proxy.CallWithData[*userentity.User](s.proxyService, "GetData", nil) +} + +// PerformAction via HTTP +// Generated from: @Route "POST /action" +func (s *ServiceDRemote) PerformAction(p *userentity.Request) error { + return proxy.Call(s.proxyService, "PerformAction", p) +} + + +func ServiceDFactory(deps map[string]any, config map[string]any) any { + svc := &ServiceD{ + } + + return svc +} + +// ServiceDRemoteFactory creates a remote HTTP client for ServiceDInterface +// Auto-generated from @EndpointService annotation +func ServiceDRemoteFactory(deps, config map[string]any) any { + proxyService, ok := config["remote"].(*proxy.Service) + if !ok { + panic("remote factory requires 'remote' (proxy.Service) in config") + } + return NewServiceDRemote(proxyService) +} + +// RegisterServiceD registers the service-d with the registry +// Auto-generated from annotations: +// - @EndpointService name="service-d", prefix="/api/d" +// - @Route annotations on methods +func RegisterServiceD() { + // Register service type with router configuration + lokstra_registry.RegisterRouterServiceType("service-d-factory", + ServiceDFactory, + ServiceDRemoteFactory, + &deploy.ServiceTypeConfig{ + PathPrefix: "/api/d", + Middlewares: []string{ }, + RouteOverrides: map[string]deploy.RouteConfig{ + "GetData": { + Path: "GET /data", + }, + "PerformAction": { + Path: "POST /action", + }, + }, + }, + ) + + // Register lazy service with auto-detected dependencies + lokstra_registry.RegisterLazyService("service-d", + "service-d-factory", + map[string]any{ + }) +} + + diff --git a/core/annotation/internal/multifile_test/profile_service.go b/core/annotation/internal/multifile_test/profile_service.go index ac9b93de..502b7100 100644 --- a/core/annotation/internal/multifile_test/profile_service.go +++ b/core/annotation/internal/multifile_test/profile_service.go @@ -2,7 +2,7 @@ package main import "github.com/primadi/lokstra/core/service" -// @RouterService name="profile-service", prefix="/api/profiles" +// @EndpointService name="profile-service", prefix="/api/profiles" type ProfileService struct { // @Inject "profile-repo" ProfileRepo *service.Cached[any] diff --git a/core/annotation/internal/multifile_test/user_service.go b/core/annotation/internal/multifile_test/user_service.go index 601283be..04e03e61 100644 --- a/core/annotation/internal/multifile_test/user_service.go +++ b/core/annotation/internal/multifile_test/user_service.go @@ -5,7 +5,7 @@ import ( "github.com/primadi/lokstra/core/service" ) -// @RouterService name="user-service", prefix="/api/v1/users" +// @EndpointService name="user-service", prefix="/api/v1/users" type UserService struct { // @Inject "user-repo" UserRepo *service.Cached[any] diff --git a/core/annotation/internal/test/complex_processor_test.go b/core/annotation/internal/test/complex_processor_test.go index a32bf9b3..16142c75 100644 --- a/core/annotation/internal/test/complex_processor_test.go +++ b/core/annotation/internal/test/complex_processor_test.go @@ -18,9 +18,9 @@ func TestProcessComplexAnnotations_NoDuplicateFolders(t *testing.T) { // Create nested folder structure with .go files testStructure := map[string]string{ - "module1/service.go": "package module1\n\n// @RouterService name=\"service1\"\ntype Service1 struct {}", - "module1/submodule/handler.go": "package submodule\n\n// @RouterService name=\"service2\"\ntype Service2 struct {}", - "module2/api.go": "package module2\n\n// @RouterService name=\"service3\"\ntype Service3 struct {}", + "module1/service.go": "package module1\n\n// @EndpointService name=\"service1\"\ntype Service1 struct {}", + "module1/submodule/handler.go": "package submodule\n\n// @EndpointService name=\"service2\"\ntype Service2 struct {}", + "module2/api.go": "package module2\n\n// @EndpointService name=\"service3\"\ntype Service3 struct {}", } for filePath, content := range testStructure { @@ -100,7 +100,7 @@ func TestProcessComplexAnnotations_DifferentPathFormats(t *testing.T) { // Create a test file testFile := filepath.Join(tmpDir, "test.go") - if err := os.WriteFile(testFile, []byte("package test\n\n// @RouterService name=\"test\"\ntype Test struct {}"), 0644); err != nil { + if err := os.WriteFile(testFile, []byte("package test\n\n// @EndpointService name=\"test\"\ntype Test struct {}"), 0644); err != nil { t.Fatalf("Failed to write test file: %v", err) } @@ -179,7 +179,7 @@ func TestProcessComplexAnnotations_ParallelProcessing(t *testing.T) { t.Fatalf("Failed to create directory: %v", err) } testFile := filepath.Join(folderPath, "service.go") - content := fmt.Sprintf("package module%d\n\n// @RouterService name=\"service%d\"\ntype Service%d struct {}", i, i, i) + content := fmt.Sprintf("package module%d\n\n// @EndpointService name=\"service%d\"\ntype Service%d struct {}", i, i, i) if err := os.WriteFile(testFile, []byte(content), 0644); err != nil { t.Fatalf("Failed to write file: %v", err) } @@ -229,9 +229,9 @@ func TestScenario1_OverlappingPaths(t *testing.T) { // Create structure: root with modules subfolder testStructure := map[string]string{ - "main.go": "package main\n\n// @RouterService name=\"main\"\ntype MainService struct {}", - "modules/user/user.go": "package user\n\n// @RouterService name=\"user\"\ntype UserService struct {}", - "modules/order/order.go": "package order\n\n// @RouterService name=\"order\"\ntype OrderService struct {}", + "main.go": "package main\n\n// @EndpointService name=\"main\"\ntype MainService struct {}", + "modules/user/user.go": "package user\n\n// @EndpointService name=\"user\"\ntype UserService struct {}", + "modules/order/order.go": "package order\n\n// @EndpointService name=\"order\"\ntype OrderService struct {}", } for filePath, content := range testStructure { @@ -292,7 +292,7 @@ func TestScenario2_SamePathDifferentForms(t *testing.T) { t.Fatalf("Failed to create directory: %v", err) } testFile := filepath.Join(modulesPath, "service.go") - if err := os.WriteFile(testFile, []byte("package modules\n\n// @RouterService name=\"modules\"\ntype Service struct {}"), 0644); err != nil { + if err := os.WriteFile(testFile, []byte("package modules\n\n// @EndpointService name=\"modules\"\ntype Service struct {}"), 0644); err != nil { t.Fatalf("Failed to write file: %v", err) } @@ -346,12 +346,12 @@ func TestScenario3_MultipleNestedPaths(t *testing.T) { // Create nested structure testStructure := map[string]string{ - "main.go": "package main\n\n// @RouterService name=\"main\"\ntype MainService struct {}", - "modules/user/service.go": "package user\n\n// @RouterService name=\"user\"\ntype UserService struct {}", + "main.go": "package main\n\n// @EndpointService name=\"main\"\ntype MainService struct {}", + "modules/user/service.go": "package user\n\n// @EndpointService name=\"user\"\ntype UserService struct {}", "modules/user/repository.go": "package user\n\n// No RouterService here", - "modules/order/service.go": "package order\n\n// @RouterService name=\"order\"\ntype OrderService struct {}", + "modules/order/service.go": "package order\n\n// @EndpointService name=\"order\"\ntype OrderService struct {}", "modules/order/repository.go": "package order\n\n// No RouterService here", - "modules/payment/service.go": "package payment\n\n// @RouterService name=\"payment\"\ntype PaymentService struct {}", + "modules/payment/service.go": "package payment\n\n// @EndpointService name=\"payment\"\ntype PaymentService struct {}", } for filePath, content := range testStructure { @@ -403,7 +403,7 @@ func TestScenario3_MultipleNestedPaths(t *testing.T) { } } -// TestFileContainsRouterService tests the quick check function for @RouterService annotation +// TestFileContainsRouterService tests the quick check function for @EndpointService annotation func TestFileContainsRouterService(t *testing.T) { tests := []struct { name string @@ -414,7 +414,7 @@ func TestFileContainsRouterService(t *testing.T) { name: "standard format", content: `package app -// @RouterService name="user-service" +// @EndpointService name="user-service" type UserService struct {} `, expected: true, @@ -423,7 +423,7 @@ type UserService struct {} name: "with_spaces_after_//", content: `package app -// @RouterService name="user-service" +// @EndpointService name="user-service" type UserService struct {} `, expected: false, // Changed: Multiple spaces (>1) are treated as indented (code example) @@ -432,7 +432,7 @@ type UserService struct {} name: "no space after //", content: `package app -//@RouterService name="user-service" +//@EndpointService name="user-service" type UserService struct {} `, expected: true, @@ -441,7 +441,7 @@ type UserService struct {} name: "in string (should not match - no comment)", content: `package app -const x = "@RouterService name=\"test\"" +const x = "@EndpointService name=\"test\"" `, expected: false, }, @@ -449,16 +449,16 @@ const x = "@RouterService name=\"test\"" name: "in descriptive comment (should NOT match - not at start)", content: `package app -// This is about @RouterService annotation +// This is about @EndpointService annotation type UserService struct {} `, - expected: false, // Should NOT match - @RouterService is not at the start of comment + expected: false, // Should NOT match - @EndpointService is not at the start of comment }, { name: "block comment (should not match)", content: `package app -/* @RouterService name="test" */ +/* @EndpointService name="test" */ type UserService struct {} `, expected: false, @@ -484,7 +484,7 @@ func GetUsers() {} name: "tab before comment", content: `package app - // @RouterService name="test" + // @EndpointService name="test" type Service struct {} `, expected: true, @@ -494,7 +494,7 @@ type Service struct {} content: `package app // @Route "GET /test" -// @RouterService name="test" +// @EndpointService name="test" type Service struct {} `, expected: true, diff --git a/core/annotation/test/ANNOTATION_FIXES_SUMMARY.md b/core/annotation/test/ANNOTATION_FIXES_SUMMARY.md index 1ceb3cea..e862fd88 100644 --- a/core/annotation/test/ANNOTATION_FIXES_SUMMARY.md +++ b/core/annotation/test/ANNOTATION_FIXES_SUMMARY.md @@ -13,7 +13,7 @@ Annotations dalam dokumentasi (TAB-indented code examples) dianggap sebagai anno ```go // Example usage in documentation: // -// @RouterService name="example-service" +// @EndpointService name="example-service" // type ExampleService struct {} ``` @@ -21,8 +21,8 @@ File `zz_generated.lokstra.go` dibuat meskipun annotation di dalam code example. ### Root Cause Go documentation convention menggunakan TAB setelah `//` untuk code examples: -- `// @RouterService` → Real annotation ✅ -- `// @RouterService` (TAB) → Code example in docs ❌ +- `// @EndpointService` → Real annotation ✅ +- `// @EndpointService` (TAB) → Code example in docs ❌ Parser tidak membedakan antara annotation yang valid dengan code examples. @@ -34,13 +34,13 @@ Parser tidak membedakan antara annotation yang valid dengan code examples. **Detection Rules:** ```go // Valid annotations (ALLOWED): -// @RouterService name="user-service" -//@RouterService name="user-service" +// @EndpointService name="user-service" +//@EndpointService name="user-service" // Invalid annotations (IGNORED): -// @RouterService name="user-service" // TAB after // -// @RouterService name="user-service" // Multiple spaces after // -// @RouterService name="user-service" // Multiple spaces after // +// @EndpointService name="user-service" // TAB after // +// @EndpointService name="user-service" // Multiple spaces after // +// @EndpointService name="user-service" // Multiple spaces after // ``` **Implementation:** @@ -82,7 +82,7 @@ Both detection steps now use same logic: **Step 1:** `fileContainsRouterService()` - Quick check ```go -if strings.Contains(line, "@RouterService") { +if strings.Contains(line, "@EndpointService") { afterSlashes := line[2:] if len(afterSlashes) > 0 && afterSlashes[0] == '\t' { continue @@ -101,13 +101,13 @@ if strings.Contains(line, "@RouterService") { ## 2. Struct Validation ✅ ### Problem -`@RouterService` bisa ditulis di atas function, interface, atau type alias: +`@EndpointService` bisa ditulis di atas function, interface, atau type alias: ```go -// @RouterService name="invalid-service" +// @EndpointService name="invalid-service" func GetUser() {} // ❌ Invalid tapi tidak error! -// @RouterService name="invalid-service" +// @EndpointService name="invalid-service" type UserRepository interface {} // ❌ Invalid tapi tidak error! ``` @@ -140,10 +140,10 @@ func isStructDeclaration(fset *token.FileSet, file *ast.File, line int) bool { **Validation in:** `processFileForCodeGen()` ```go if routerService != nil { - // Validate that @RouterService is on a struct + // Validate that @EndpointService is on a struct if !isStructDeclaration(fset, file, routerService.Line) { return nil, fmt.Errorf( - "@RouterService at line %d must be placed above a struct declaration, "+ + "@EndpointService at line %d must be placed above a struct declaration, "+ "not a function, interface, or type alias", routerService.Line+1, ) @@ -190,7 +190,7 @@ if len(ctx.UpdatedFiles) == 0 && len(ctx.DeletedFiles) == 0 { ``` **Scenario yang bermasalah:** -1. User punya `user_service.go` dengan `@RouterService` → generated file dibuat ✅ +1. User punya `user_service.go` dengan `@EndpointService` → generated file dibuat ✅ 2. User hapus annotation dari `user_service.go` 3. Cache mendeteksi file checksum sama → file di-skip 4. `UpdatedFiles` dan `DeletedFiles` kosong → early return diff --git a/core/annotation/test/CHANGELOG_ANNOTATION_FIX.md b/core/annotation/test/CHANGELOG_ANNOTATION_FIX.md index b8238e01..74365ef5 100644 --- a/core/annotation/test/CHANGELOG_ANNOTATION_FIX.md +++ b/core/annotation/test/CHANGELOG_ANNOTATION_FIX.md @@ -2,7 +2,7 @@ ## Problem -The annotation parser was incorrectly detecting `@RouterService` (and other annotations) in **documentation examples**, causing false positives and generating unwanted code. +The annotation parser was incorrectly detecting `@EndpointService` (and other annotations) in **documentation examples**, causing false positives and generating unwanted code. ### Example of the Problem @@ -11,9 +11,9 @@ package middleware // Register is a placeholder for middleware registration // -// Example @RouterService annotation: +// Example @EndpointService annotation: // -// @RouterService name="tenant-service", prefix="/api/tenants" +// @EndpointService name="tenant-service", prefix="/api/tenants" // // The above is just an EXAMPLE, not an actual annotation! func Register() { @@ -30,22 +30,22 @@ Updated the annotation parser to follow **Go documentation conventions**: ### Rules for Valid Annotations -1. **Valid annotation:** `// @RouterService` (space or no space after `//`) +1. **Valid annotation:** `// @EndpointService` (space or no space after `//`) ```go - // @RouterService name="user-service" + // @EndpointService name="user-service" type UserService struct {} ``` -2. **Invalid annotation (TAB-indented):** `// @RouterService` (TAB after `//`) +2. **Invalid annotation (TAB-indented):** `// @EndpointService` (TAB after `//`) ```go // Example: // - // @RouterService name="example" // ← IGNORED (code example) + // @EndpointService name="example" // ← IGNORED (code example) ``` -3. **Invalid annotation (multi-space indented):** `// @RouterService` +3. **Invalid annotation (multi-space indented):** `// @EndpointService` ```go - // @RouterService // ← IGNORED (indented) + // @EndpointService // ← IGNORED (indented) ``` ### Go Documentation Convention @@ -83,7 +83,7 @@ The parser now respects this convention and **skips any annotation that is TAB-i 2. **`TestParseFileAnnotations_ValidAnnotations`** - Ensures valid annotations are still detected - - Tests `@RouterService`, `@Inject`, `@Route` + - Tests `@EndpointService`, `@Inject`, `@Route` 3. **`TestParseFileAnnotations_MultipleEmptyLinesAfterAnnotation`** - Prevents matching annotations with too many gap lines @@ -102,7 +102,7 @@ The parser now respects this convention and **skips any annotation that is TAB-i File with documentation example: ```go // Example: -// @RouterService name="tenant-service", prefix="/api/tenants" +// @EndpointService name="tenant-service", prefix="/api/tenants" func Register() {} ``` @@ -114,7 +114,7 @@ Same file: **No code generated** (correctly ignored). Only actual annotations are processed: ```go -// @RouterService name="tenant-service", prefix="/api/tenants" +// @EndpointService name="tenant-service", prefix="/api/tenants" type TenantService struct {} ``` @@ -147,18 +147,18 @@ When writing documentation with annotation examples: ```go // Example: // - // @RouterService name="example" // ← Will be ignored + // @EndpointService name="example" // ← Will be ignored ``` 2. **Or use enough indentation** (2+ spaces): ```go // Example: - // @RouterService name="example" // ← Will be ignored + // @EndpointService name="example" // ← Will be ignored ``` 3. **For actual annotations**, use no indentation or single space: ```go - // @RouterService name="real-service" // ← Will be processed + // @EndpointService name="real-service" // ← Will be processed type RealService struct {} ``` diff --git a/core/annotation/test/arg_parser_indent_test.go b/core/annotation/test/arg_parser_indent_test.go index f0c851dc..21dcbb01 100644 --- a/core/annotation/test/arg_parser_indent_test.go +++ b/core/annotation/test/arg_parser_indent_test.go @@ -12,7 +12,7 @@ import ( // (typically in documentation examples) are correctly ignored func TestParseFileAnnotations_IgnoreIndentedAnnotations(t *testing.T) { // Create temp test file with indented annotation in doc comment - // NOTE: The line "//\t@RouterService" uses actual TAB character for Go doc code example format + // NOTE: The line "//\t@EndpointService" uses actual TAB character for Go doc code example format content := "package middleware\n\n" + "import (\n" + "\t\"github.com/primadi/lokstra/lokstra_registry\"\n" + @@ -21,11 +21,11 @@ func TestParseFileAnnotations_IgnoreIndentedAnnotations(t *testing.T) { "// TODO: Implement actual middleware registration when Lokstra framework supports it\n" + "//\n" + "// For now, middlewares should be applied manually in route setup or\n" + - "// specified in @RouterService annotations for documentation purposes.\n" + + "// specified in @EndpointService annotations for documentation purposes.\n" + "//\n" + - "// Example @RouterService annotation:\n" + + "// Example @EndpointService annotation:\n" + "//\n" + - "//\t@RouterService name=\"tenant-service\", prefix=\"/api/tenants\", middlewares=[\"recovery\", \"request_logger\", \"auth\"]\n" + + "//\t@EndpointService name=\"tenant-service\", prefix=\"/api/tenants\", middlewares=[\"recovery\", \"request_logger\", \"auth\"]\n" + "//\n" + "// The \"auth\" middleware indicates that the endpoint requires authentication.\n" + "// Actual middleware implementation should be set up in your main.go or route configuration.\n" + @@ -46,7 +46,7 @@ func TestParseFileAnnotations_IgnoreIndentedAnnotations(t *testing.T) { t.Fatalf("ParseFileAnnotations() error = %v", err) } - // Should find NO annotations (the @RouterService is indented with TAB, so should be ignored) + // Should find NO annotations (the @EndpointService is indented with TAB, so should be ignored) if len(annotations) != 0 { t.Errorf("Expected 0 annotations (indented should be ignored), got %d", len(annotations)) for _, ann := range annotations { @@ -59,7 +59,7 @@ func TestParseFileAnnotations_IgnoreIndentedAnnotations(t *testing.T) { func TestParseFileAnnotations_ValidAnnotations(t *testing.T) { content := `package application -// @RouterService name="user-service", prefix="/api/users" +// @EndpointService name="user-service", prefix="/api/users" type UserService struct { // @Inject "user-repository" UserRepo UserRepository @@ -82,7 +82,7 @@ func (s *UserService) GetByID(p *GetUserParams) (*User, error) { t.Fatalf("ParseFileAnnotations() error = %v", err) } - // Should find 3 annotations: @RouterService, @Inject, @Route + // Should find 3 annotations: @EndpointService, @Inject, @Route if len(annotations) != 3 { t.Errorf("Expected 3 annotations, got %d", len(annotations)) for _, ann := range annotations { @@ -92,9 +92,9 @@ func (s *UserService) GetByID(p *GetUserParams) (*User, error) { // Verify annotations expectedAnnotations := map[string]string{ - "RouterService": "UserService", - "Inject": "UserRepo", - "Route": "GetByID", + "EndpointService": "UserService", + "Inject": "UserRepo", + "Route": "GetByID", } foundAnnotations := make(map[string]string) @@ -116,7 +116,7 @@ func (s *UserService) GetByID(p *GetUserParams) (*User, error) { func TestParseFileAnnotations_MultipleEmptyLinesAfterAnnotation(t *testing.T) { content := `package test -// @RouterService name="test-service" +// @EndpointService name="test-service" // // // @@ -152,7 +152,7 @@ type TestService struct {} func TestParseFileAnnotations_AnnotationWithFewEmptyLines(t *testing.T) { content := `package test -// @RouterService name="test-service" +// @EndpointService name="test-service" // // Some documentation type TestService struct {} @@ -174,8 +174,8 @@ type TestService struct {} t.Errorf("Expected 1 annotation, got %d", len(annotations)) } else { ann := annotations[0] - if ann.Name != "RouterService" { - t.Errorf("Expected @RouterService, got @%s", ann.Name) + if ann.Name != "EndpointService" { + t.Errorf("Expected @EndpointService, got @%s", ann.Name) } if ann.TargetName != "TestService" { t.Errorf("Expected target TestService, got %s", ann.TargetName) diff --git a/core/annotation/test/backtick_test.go b/core/annotation/test/backtick_test.go index 714774f9..08701164 100644 --- a/core/annotation/test/backtick_test.go +++ b/core/annotation/test/backtick_test.go @@ -25,9 +25,9 @@ type Config struct { Port int } -// @RouterService name="test-service", prefix="/api" +// @EndpointService name="test-service", prefix="/api" type TestService struct { - // @InjectCfgValue key="config", default=` + "`Config{Name: \"myapp\", Port: 8080}`" + ` + // @Inject "cfg:config", ` + "`Config{Name: \"myapp\", Port: 8080}`" + ` Cfg Config } @@ -46,9 +46,9 @@ type Config struct { Port int } -// @RouterService name="test-service", prefix="/api" +// @EndpointService name="test-service", prefix="/api" type TestService struct { - // @InjectCfgValue key="config", default="Config{Name: \"myapp\", Port: 8080}" + // @Inject "cfg:config", "Config{Name: \"myapp\", Port: 8080}" Cfg Config } @@ -70,9 +70,9 @@ type ScheduleConfig struct { Duration time.Duration } -// @RouterService name="test-service", prefix="/api" +// @EndpointService name="test-service", prefix="/api" type TestService struct { - // @InjectCfgValue key="schedule", default=` + "`ScheduleConfig{EventName: \"Meeting\", StartDate: \"2024-12-25\", Duration: 3600000000000}`" + ` + // @Inject "cfg:schedule", ` + "`ScheduleConfig{EventName: \"Meeting\", StartDate: \"2024-12-25\", Duration: 3600000000000}`" + ` Config ScheduleConfig } @@ -86,7 +86,7 @@ func (s *TestService) GetInfo() string { return "info" } name: "backtick in Route annotation", serviceCode: `package testservice -// @RouterService name="test-service", prefix="/api" +// @EndpointService name="test-service", prefix="/api" type TestService struct {} // @Route ` + "`POST /users/{id}`" + ` diff --git a/core/annotation/test/codegen_cleanup_test.go b/core/annotation/test/codegen_cleanup_test.go index 3b75f140..7ad4e734 100644 --- a/core/annotation/test/codegen_cleanup_test.go +++ b/core/annotation/test/codegen_cleanup_test.go @@ -14,10 +14,10 @@ import ( func TestGenerateCodeForFolder_CleanupEmptyFile(t *testing.T) { tmpDir := t.TempDir() - // Step 1: Create a file WITH @RouterService annotation + // Step 1: Create a file WITH @EndpointService annotation contentWithAnnotation := `package application -// @RouterService name="user-service", prefix="/api/users" +// @EndpointService name="user-service", prefix="/api/users" type UserService struct { ID string } @@ -62,7 +62,7 @@ type UserService struct { t.Logf("✓ Step 1: Generated file created: %s", genPath) - // Step 2: Remove annotation from file (simulate user removing @RouterService) + // Step 2: Remove annotation from file (simulate user removing @EndpointService) contentWithoutAnnotation := `package application // UserService is a service without annotation @@ -128,7 +128,7 @@ func TestGenerateCodeForFolder_CleanupWhenSkipped(t *testing.T) { genPath := filepath.Join(tmpDir, internal.GeneratedFileName) emptyGenContent := `// AUTO-GENERATED CODE - DO NOT EDIT // Generated by lokstra-annotation from annotations in this folder -// Annotations: @RouterService, @Inject, @Route +// Annotations: @EndpointService, @Inject, @Route package application diff --git a/core/annotation/test/codegen_validation_test.go b/core/annotation/test/codegen_validation_test.go index 1d7b5cfe..8b6e8577 100644 --- a/core/annotation/test/codegen_validation_test.go +++ b/core/annotation/test/codegen_validation_test.go @@ -8,7 +8,7 @@ import ( "github.com/primadi/lokstra/core/annotation" ) -// TestRouterServiceValidation_MustBeOnStruct tests that @RouterService +// TestRouterServiceValidation_MustBeOnStruct tests that @EndpointService // annotation must be placed above a struct declaration, not a function func TestRouterServiceValidation_MustBeOnStruct(t *testing.T) { tests := []struct { @@ -21,7 +21,7 @@ func TestRouterServiceValidation_MustBeOnStruct(t *testing.T) { name: "valid - struct", content: `package application -// @RouterService name="user-service", prefix="/api/users" +// @EndpointService name="user-service", prefix="/api/users" type UserService struct { UserRepo UserRepository } @@ -40,7 +40,7 @@ type User struct { name: "invalid - function", content: `package application -// @RouterService name="register-service", prefix="/api/register" +// @EndpointService name="register-service", prefix="/api/register" func Register() { // This should fail validation } @@ -52,7 +52,7 @@ func Register() { name: "invalid - interface", content: `package application -// @RouterService name="user-service", prefix="/api/users" +// @EndpointService name="user-service", prefix="/api/users" type UserService interface { GetByID(id string) error } @@ -64,7 +64,7 @@ type UserService interface { name: "invalid - type alias", content: `package application -// @RouterService name="user-service", prefix="/api/users" +// @EndpointService name="user-service", prefix="/api/users" type UserService = string `, expectError: true, diff --git a/core/annotation/test/complex_processor_test.go b/core/annotation/test/complex_processor_test.go index a32bf9b3..16142c75 100644 --- a/core/annotation/test/complex_processor_test.go +++ b/core/annotation/test/complex_processor_test.go @@ -18,9 +18,9 @@ func TestProcessComplexAnnotations_NoDuplicateFolders(t *testing.T) { // Create nested folder structure with .go files testStructure := map[string]string{ - "module1/service.go": "package module1\n\n// @RouterService name=\"service1\"\ntype Service1 struct {}", - "module1/submodule/handler.go": "package submodule\n\n// @RouterService name=\"service2\"\ntype Service2 struct {}", - "module2/api.go": "package module2\n\n// @RouterService name=\"service3\"\ntype Service3 struct {}", + "module1/service.go": "package module1\n\n// @EndpointService name=\"service1\"\ntype Service1 struct {}", + "module1/submodule/handler.go": "package submodule\n\n// @EndpointService name=\"service2\"\ntype Service2 struct {}", + "module2/api.go": "package module2\n\n// @EndpointService name=\"service3\"\ntype Service3 struct {}", } for filePath, content := range testStructure { @@ -100,7 +100,7 @@ func TestProcessComplexAnnotations_DifferentPathFormats(t *testing.T) { // Create a test file testFile := filepath.Join(tmpDir, "test.go") - if err := os.WriteFile(testFile, []byte("package test\n\n// @RouterService name=\"test\"\ntype Test struct {}"), 0644); err != nil { + if err := os.WriteFile(testFile, []byte("package test\n\n// @EndpointService name=\"test\"\ntype Test struct {}"), 0644); err != nil { t.Fatalf("Failed to write test file: %v", err) } @@ -179,7 +179,7 @@ func TestProcessComplexAnnotations_ParallelProcessing(t *testing.T) { t.Fatalf("Failed to create directory: %v", err) } testFile := filepath.Join(folderPath, "service.go") - content := fmt.Sprintf("package module%d\n\n// @RouterService name=\"service%d\"\ntype Service%d struct {}", i, i, i) + content := fmt.Sprintf("package module%d\n\n// @EndpointService name=\"service%d\"\ntype Service%d struct {}", i, i, i) if err := os.WriteFile(testFile, []byte(content), 0644); err != nil { t.Fatalf("Failed to write file: %v", err) } @@ -229,9 +229,9 @@ func TestScenario1_OverlappingPaths(t *testing.T) { // Create structure: root with modules subfolder testStructure := map[string]string{ - "main.go": "package main\n\n// @RouterService name=\"main\"\ntype MainService struct {}", - "modules/user/user.go": "package user\n\n// @RouterService name=\"user\"\ntype UserService struct {}", - "modules/order/order.go": "package order\n\n// @RouterService name=\"order\"\ntype OrderService struct {}", + "main.go": "package main\n\n// @EndpointService name=\"main\"\ntype MainService struct {}", + "modules/user/user.go": "package user\n\n// @EndpointService name=\"user\"\ntype UserService struct {}", + "modules/order/order.go": "package order\n\n// @EndpointService name=\"order\"\ntype OrderService struct {}", } for filePath, content := range testStructure { @@ -292,7 +292,7 @@ func TestScenario2_SamePathDifferentForms(t *testing.T) { t.Fatalf("Failed to create directory: %v", err) } testFile := filepath.Join(modulesPath, "service.go") - if err := os.WriteFile(testFile, []byte("package modules\n\n// @RouterService name=\"modules\"\ntype Service struct {}"), 0644); err != nil { + if err := os.WriteFile(testFile, []byte("package modules\n\n// @EndpointService name=\"modules\"\ntype Service struct {}"), 0644); err != nil { t.Fatalf("Failed to write file: %v", err) } @@ -346,12 +346,12 @@ func TestScenario3_MultipleNestedPaths(t *testing.T) { // Create nested structure testStructure := map[string]string{ - "main.go": "package main\n\n// @RouterService name=\"main\"\ntype MainService struct {}", - "modules/user/service.go": "package user\n\n// @RouterService name=\"user\"\ntype UserService struct {}", + "main.go": "package main\n\n// @EndpointService name=\"main\"\ntype MainService struct {}", + "modules/user/service.go": "package user\n\n// @EndpointService name=\"user\"\ntype UserService struct {}", "modules/user/repository.go": "package user\n\n// No RouterService here", - "modules/order/service.go": "package order\n\n// @RouterService name=\"order\"\ntype OrderService struct {}", + "modules/order/service.go": "package order\n\n// @EndpointService name=\"order\"\ntype OrderService struct {}", "modules/order/repository.go": "package order\n\n// No RouterService here", - "modules/payment/service.go": "package payment\n\n// @RouterService name=\"payment\"\ntype PaymentService struct {}", + "modules/payment/service.go": "package payment\n\n// @EndpointService name=\"payment\"\ntype PaymentService struct {}", } for filePath, content := range testStructure { @@ -403,7 +403,7 @@ func TestScenario3_MultipleNestedPaths(t *testing.T) { } } -// TestFileContainsRouterService tests the quick check function for @RouterService annotation +// TestFileContainsRouterService tests the quick check function for @EndpointService annotation func TestFileContainsRouterService(t *testing.T) { tests := []struct { name string @@ -414,7 +414,7 @@ func TestFileContainsRouterService(t *testing.T) { name: "standard format", content: `package app -// @RouterService name="user-service" +// @EndpointService name="user-service" type UserService struct {} `, expected: true, @@ -423,7 +423,7 @@ type UserService struct {} name: "with_spaces_after_//", content: `package app -// @RouterService name="user-service" +// @EndpointService name="user-service" type UserService struct {} `, expected: false, // Changed: Multiple spaces (>1) are treated as indented (code example) @@ -432,7 +432,7 @@ type UserService struct {} name: "no space after //", content: `package app -//@RouterService name="user-service" +//@EndpointService name="user-service" type UserService struct {} `, expected: true, @@ -441,7 +441,7 @@ type UserService struct {} name: "in string (should not match - no comment)", content: `package app -const x = "@RouterService name=\"test\"" +const x = "@EndpointService name=\"test\"" `, expected: false, }, @@ -449,16 +449,16 @@ const x = "@RouterService name=\"test\"" name: "in descriptive comment (should NOT match - not at start)", content: `package app -// This is about @RouterService annotation +// This is about @EndpointService annotation type UserService struct {} `, - expected: false, // Should NOT match - @RouterService is not at the start of comment + expected: false, // Should NOT match - @EndpointService is not at the start of comment }, { name: "block comment (should not match)", content: `package app -/* @RouterService name="test" */ +/* @EndpointService name="test" */ type UserService struct {} `, expected: false, @@ -484,7 +484,7 @@ func GetUsers() {} name: "tab before comment", content: `package app - // @RouterService name="test" + // @EndpointService name="test" type Service struct {} `, expected: true, @@ -494,7 +494,7 @@ type Service struct {} content: `package app // @Route "GET /test" -// @RouterService name="test" +// @EndpointService name="test" type Service struct {} `, expected: true, diff --git a/core/annotation/test/duration_string_test.go b/core/annotation/test/duration_string_test.go index 57befdb0..6c3433bb 100644 --- a/core/annotation/test/duration_string_test.go +++ b/core/annotation/test/duration_string_test.go @@ -21,9 +21,9 @@ func TestDurationStringFormat(t *testing.T) { import "time" -// @RouterService name="test-service", prefix="/api" +// @EndpointService name="test-service", prefix="/api" type TestService struct { - // @InjectCfgValue key="timeout", default="15m" + // @Inject "cfg:timeout", "15m" Timeout time.Duration } @@ -38,9 +38,9 @@ func (s *TestService) GetInfo() string { return "info" } import "time" -// @RouterService name="test-service", prefix="/api" +// @EndpointService name="test-service", prefix="/api" type TestService struct { - // @InjectCfgValue key="timeout", default="2h" + // @Inject "cfg:timeout", "2h" Timeout time.Duration } @@ -55,9 +55,9 @@ func (s *TestService) GetInfo() string { return "info" } import "time" -// @RouterService name="test-service", prefix="/api" +// @EndpointService name="test-service", prefix="/api" type TestService struct { - // @InjectCfgValue key="timeout", default="30s" + // @Inject "cfg:timeout", "30s" Timeout time.Duration } diff --git a/core/annotation/test/error_cleanup_test.go b/core/annotation/test/error_cleanup_test.go index 3ca9db82..766ec655 100644 --- a/core/annotation/test/error_cleanup_test.go +++ b/core/annotation/test/error_cleanup_test.go @@ -17,7 +17,7 @@ func TestErrorCleanup_InvalidAnnotation(t *testing.T) { // Step 1: Create valid service with cache and generated file validContent := `package application -// @RouterService name="user-service", prefix="/api/users" +// @EndpointService name="user-service", prefix="/api/users" type UserService struct { ID string } @@ -47,10 +47,10 @@ type UserService struct { t.Logf("✓ Step 1: Cache and generated files created") // Step 2: Create invalid annotation that will cause parsing error - // Invalid: @RouterService on function instead of struct + // Invalid: @EndpointService on function instead of struct invalidContent := `package application -// @RouterService name="invalid-service", prefix="/api/invalid" +// @EndpointService name="invalid-service", prefix="/api/invalid" func InvalidService() { // This should cause validation error } @@ -88,7 +88,7 @@ func TestErrorCleanup_FileReadError(t *testing.T) { // Create a file with valid annotation first validContent := `package application -// @RouterService name="user-service", prefix="/api/users" +// @EndpointService name="user-service", prefix="/api/users" type UserService struct {} ` filePath := filepath.Join(tmpDir, "user_service.go") @@ -119,7 +119,7 @@ type UserService struct {} invalidGoFile := filepath.Join(tmpDir, "invalid.go") invalidContent := `package application -// @RouterService name="bad-service" +// @EndpointService name="bad-service" // This is not valid Go syntax type BadService struct { unclosed string "json:"name @@ -158,7 +158,7 @@ func TestErrorCleanup_ProcessingError(t *testing.T) { // Create valid file validContent := `package application -// @RouterService name="user-service", prefix="/api/users" +// @EndpointService name="user-service", prefix="/api/users" type UserService struct {} ` filePath := filepath.Join(tmpDir, "user_service.go") diff --git a/core/annotation/test/import_alias_merging_test.go b/core/annotation/test/import_alias_merging_test.go new file mode 100644 index 00000000..7d9e5301 --- /dev/null +++ b/core/annotation/test/import_alias_merging_test.go @@ -0,0 +1,335 @@ +package annotation_test + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/primadi/lokstra/core/annotation" + "github.com/primadi/lokstra/core/annotation/internal" +) + +// TestImportAlias_DifferentPathsSameAlias tests that when two different import paths +// use the same alias, the system automatically renames one of them to avoid conflict. +// Example: +// +// service_a.go: import models "pkga" +// service_b.go: import models "pkgb" +// +// Expected: One should be renamed to "models_1" or similar +func TestImportAlias_DifferentPathsSameAlias(t *testing.T) { + tmpDir := t.TempDir() + + // Create package A + pkgaDir := filepath.Join(tmpDir, "pkga") + if err := os.MkdirAll(pkgaDir, 0755); err != nil { + t.Fatalf("Failed to create pkga dir: %v", err) + } + + pkgaCode := `package pkga + +type User struct { + ID string + Name string +} +` + if err := os.WriteFile(filepath.Join(pkgaDir, "models.go"), []byte(pkgaCode), 0644); err != nil { + t.Fatalf("Failed to create pkga models: %v", err) + } + + // Create package B + pkgbDir := filepath.Join(tmpDir, "pkgb") + if err := os.MkdirAll(pkgbDir, 0755); err != nil { + t.Fatalf("Failed to create pkgb dir: %v", err) + } + + pkgbCode := `package pkgb + +type User struct { + UserID string + FullName string +} +` + if err := os.WriteFile(filepath.Join(pkgbDir, "models.go"), []byte(pkgbCode), 0644); err != nil { + t.Fatalf("Failed to create pkgb models: %v", err) + } + + // Create service A using pkga with alias "models" + serviceACode := `package main + +import ( + models "myapp/pkga" +) + +// @EndpointService name="service-a", prefix="/api/a" +type ServiceA struct {} + +// @Route "GET /users" +func (s *ServiceA) GetUsers() (*models.User, error) { + return nil, nil +} +` + if err := os.WriteFile(filepath.Join(tmpDir, "service_a.go"), []byte(serviceACode), 0644); err != nil { + t.Fatalf("Failed to create service_a.go: %v", err) + } + + // Create service B using pkgb with alias "models" (CONFLICT!) + serviceBCode := `package main + +import ( + models "myapp/pkgb" +) + +// @EndpointService name="service-b", prefix="/api/b" +type ServiceB struct {} + +// @Route "GET /users" +func (s *ServiceB) GetUsers() (*models.User, error) { + return nil, nil +} +` + if err := os.WriteFile(filepath.Join(tmpDir, "service_b.go"), []byte(serviceBCode), 0644); err != nil { + t.Fatalf("Failed to create service_b.go: %v", err) + } + + // Create go.mod + goModContent := `module myapp + +go 1.21 +` + if err := os.WriteFile(filepath.Join(tmpDir, "go.mod"), []byte(goModContent), 0644); err != nil { + t.Fatalf("Failed to create go.mod: %v", err) + } + + // Parse annotations for both services + annotationsA, err := annotation.ParseFileAnnotations(filepath.Join(tmpDir, "service_a.go")) + if err != nil { + t.Fatalf("Failed to parse service_a annotations: %v", err) + } + + annotationsB, err := annotation.ParseFileAnnotations(filepath.Join(tmpDir, "service_b.go")) + if err != nil { + t.Fatalf("Failed to parse service_b annotations: %v", err) + } + + // Create context + ctx := &annotation.RouterServiceContext{ + FolderPath: tmpDir, + UpdatedFiles: []*annotation.FileToProcess{ + { + Filename: "service_a.go", + FullPath: filepath.Join(tmpDir, "service_a.go"), + Annotations: annotationsA, + }, + { + Filename: "service_b.go", + FullPath: filepath.Join(tmpDir, "service_b.go"), + Annotations: annotationsB, + }, + }, + SkippedFiles: []*annotation.FileToProcess{}, + DeletedFiles: []string{}, + GeneratedCode: &annotation.GeneratedCode{ + Services: make(map[string]*annotation.ServiceGeneration), + PreservedSections: make(map[string]string), + }, + } + + // Generate code + if err := annotation.GenerateCodeForFolder(ctx); err != nil { + t.Fatalf("GenerateCodeForFolder failed: %v", err) + } + + // Read generated file + genFilePath := filepath.Join(tmpDir, internal.GeneratedFileName) + genContent, err := os.ReadFile(genFilePath) + if err != nil { + t.Fatalf("Failed to read generated file: %v", err) + } + + genCode := string(genContent) + + // Verify that both imports exist but with different aliases + hasPkgaImport := strings.Contains(genCode, `"myapp/pkga"`) + hasPkgbImport := strings.Contains(genCode, `"myapp/pkgb"`) + + if !hasPkgaImport { + t.Error("Expected pkga import in generated code") + } + if !hasPkgbImport { + t.Error("Expected pkgb import in generated code") + } + + // Count how many times "models" appears as an alias + // At least one should be renamed (e.g., models_1) + modelsCount := strings.Count(genCode, "models \"myapp/") + models1Count := strings.Count(genCode, "models_1 \"myapp/") + + t.Logf("Found 'models' alias: %d times", modelsCount) + t.Logf("Found 'models_1' alias: %d times", models1Count) + + // One should keep "models", another should be "models_1" + if modelsCount != 1 { + t.Errorf("Expected exactly 1 'models' alias, got %d", modelsCount) + } + if models1Count != 1 { + t.Errorf("Expected exactly 1 'models_1' alias, got %d", models1Count) + } + + t.Logf("Generated code:\n%s", genCode) +} + +// TestImportAlias_SamePathDifferentAliases tests that when the same import path +// is used with different aliases across services, they should be merged to use +// a single consistent alias (preferring the longer/more descriptive one). +// Example: +// +// service_c.go: import userentity "pkga" +// service_d.go: import pkgamodel "pkga" +// +// Expected: Should merge to one alias (e.g., "pkgamodel" or "userentity") +func TestImportAlias_SamePathDifferentAliases(t *testing.T) { + tmpDir := t.TempDir() + + // Create package A + pkgaDir := filepath.Join(tmpDir, "pkga") + if err := os.MkdirAll(pkgaDir, 0755); err != nil { + t.Fatalf("Failed to create pkga dir: %v", err) + } + + pkgaCode := `package pkga + +type User struct { + ID string + Name string +} +` + if err := os.WriteFile(filepath.Join(pkgaDir, "models.go"), []byte(pkgaCode), 0644); err != nil { + t.Fatalf("Failed to create pkga models: %v", err) + } + + // Create service C using pkga with alias "userentity" + serviceCCode := `package main + +import ( + userentity "myapp/pkga" +) + +// @EndpointService name="service-c", prefix="/api/c" +type ServiceC struct {} + +// @Route "GET /entity" +func (s *ServiceC) GetEntity() (*userentity.User, error) { + return nil, nil +} +` + if err := os.WriteFile(filepath.Join(tmpDir, "service_c.go"), []byte(serviceCCode), 0644); err != nil { + t.Fatalf("Failed to create service_c.go: %v", err) + } + + // Create service D using pkga with alias "pkgamodel" (same path, different alias) + serviceDCode := `package main + +import ( + pkgamodel "myapp/pkga" +) + +// @EndpointService name="service-d", prefix="/api/d" +type ServiceD struct {} + +// @Route "GET /data" +func (s *ServiceD) GetData() (*pkgamodel.User, error) { + return nil, nil +} +` + if err := os.WriteFile(filepath.Join(tmpDir, "service_d.go"), []byte(serviceDCode), 0644); err != nil { + t.Fatalf("Failed to create service_d.go: %v", err) + } + + // Create go.mod + goModContent := `module myapp + +go 1.21 +` + if err := os.WriteFile(filepath.Join(tmpDir, "go.mod"), []byte(goModContent), 0644); err != nil { + t.Fatalf("Failed to create go.mod: %v", err) + } + + // Parse annotations for both services + annotationsC, err := annotation.ParseFileAnnotations(filepath.Join(tmpDir, "service_c.go")) + if err != nil { + t.Fatalf("Failed to parse service_c annotations: %v", err) + } + + annotationsD, err := annotation.ParseFileAnnotations(filepath.Join(tmpDir, "service_d.go")) + if err != nil { + t.Fatalf("Failed to parse service_d annotations: %v", err) + } + + // Create context + ctx := &annotation.RouterServiceContext{ + FolderPath: tmpDir, + UpdatedFiles: []*annotation.FileToProcess{ + { + Filename: "service_c.go", + FullPath: filepath.Join(tmpDir, "service_c.go"), + Annotations: annotationsC, + }, + { + Filename: "service_d.go", + FullPath: filepath.Join(tmpDir, "service_d.go"), + Annotations: annotationsD, + }, + }, + SkippedFiles: []*annotation.FileToProcess{}, + DeletedFiles: []string{}, + GeneratedCode: &annotation.GeneratedCode{ + Services: make(map[string]*annotation.ServiceGeneration), + PreservedSections: make(map[string]string), + }, + } + + // Generate code + if err := annotation.GenerateCodeForFolder(ctx); err != nil { + t.Fatalf("GenerateCodeForFolder failed: %v", err) + } + + // Read generated file + genFilePath := filepath.Join(tmpDir, internal.GeneratedFileName) + genContent, err := os.ReadFile(genFilePath) + if err != nil { + t.Fatalf("Failed to read generated file: %v", err) + } + + genCode := string(genContent) + + // Verify that pkga is imported only once + pkgaImportCount := strings.Count(genCode, `"myapp/pkga"`) + + if pkgaImportCount != 1 { + t.Errorf("Expected pkga to be imported exactly once, got %d times", pkgaImportCount) + } + + // Check which alias was chosen (should prefer longer one) + hasUserEntity := strings.Contains(genCode, "userentity \"myapp/pkga\"") + hasPkgaModel := strings.Contains(genCode, "pkgamodel \"myapp/pkga\"") + + if !hasUserEntity && !hasPkgaModel { + t.Error("Expected either 'userentity' or 'pkgamodel' alias for pkga") + } + + // Should have exactly one alias + if hasUserEntity && hasPkgaModel { + t.Error("Expected only one alias for pkga, found both 'userentity' and 'pkgamodel'") + } + + // Log which alias was chosen + if hasUserEntity { + t.Log("Merged to alias: 'userentity'") + } else if hasPkgaModel { + t.Log("Merged to alias: 'pkgamodel'") + } + + t.Logf("Generated code:\n%s", genCode) +} diff --git a/core/annotation/test/inject_cfg_value_types_test.go b/core/annotation/test/inject_cfg_value_types_test.go index eec38311..b26a541b 100644 --- a/core/annotation/test/inject_cfg_value_types_test.go +++ b/core/annotation/test/inject_cfg_value_types_test.go @@ -47,13 +47,13 @@ func TestInjectCfgValue_AllTypes(t *testing.T) { import "time" -// @RouterService name="basic-service", prefix="/api/basic" +// @EndpointService name="basic-service", prefix="/api/basic" type BasicService struct { - // @InjectCfgValue "app.name" + // @Inject "cfg:app.name" AppName string - // @InjectCfgValue "app.port" + // @Inject "cfg:app.port" Port int - // @InjectCfgValue "app.timeout" + // @Inject "cfg:app.timeout" Timeout time.Duration } @@ -66,9 +66,9 @@ func (s *BasicService) GetInfo() string { return "info" } name: "ByteSlice", serviceCode: `package testservice -// @RouterService name="byte-service", prefix="/api/bytes" +// @EndpointService name="byte-service", prefix="/api/bytes" type ByteService struct { - // @InjectCfgValue "secret" + // @Inject "cfg:secret" Secret []byte } @@ -81,9 +81,9 @@ func (s *ByteService) GetInfo() string { return "info" } name: "StringSlice", serviceCode: `package testservice -// @RouterService name="string-slice-service", prefix="/api/strings" +// @EndpointService name="string-slice-service", prefix="/api/strings" type StringSliceService struct { - // @InjectCfgValue "hosts" + // @Inject "cfg:hosts" Hosts []string } @@ -96,13 +96,13 @@ func (s *StringSliceService) GetInfo() string { return "info" } name: "IntSlices", serviceCode: `package testservice -// @RouterService name="int-slice-service", prefix="/api/ints" +// @EndpointService name="int-slice-service", prefix="/api/ints" type IntSliceService struct { - // @InjectCfgValue "ports" + // @Inject "cfg:ports" Ports []int - // @InjectCfgValue "delays" + // @Inject "cfg:delays" Delays []int64 - // @InjectCfgValue "rates" + // @Inject "cfg:rates" Rates []float64 } @@ -120,9 +120,9 @@ type DatabaseConfig struct { Port int } -// @RouterService name="struct-service", prefix="/api/struct" +// @EndpointService name="struct-service", prefix="/api/struct" type StructService struct { - // @InjectCfgValue "database" + // @Inject "cfg:database" DBConfig DatabaseConfig } @@ -140,9 +140,9 @@ type ServerConfig struct { Port int } -// @RouterService name="server-service", prefix="/api/servers" +// @EndpointService name="server-service", prefix="/api/servers" type ServerService struct { - // @InjectCfgValue "servers" + // @Inject "cfg:servers" Servers []ServerConfig } @@ -161,19 +161,19 @@ type AuthConfig struct { Provider string } -// @RouterService name="mixed-service", prefix="/api/mixed" +// @EndpointService name="mixed-service", prefix="/api/mixed" type MixedService struct { - // @InjectCfgValue "name" + // @Inject "cfg:name" Name string - // @InjectCfgValue "timeout" + // @Inject "cfg:timeout" Timeout time.Duration - // @InjectCfgValue "secret" + // @Inject "cfg:secret" Secret []byte - // @InjectCfgValue "hosts" + // @Inject "cfg:hosts" Hosts []string - // @InjectCfgValue "ports" + // @Inject "cfg:ports" Ports []int - // @InjectCfgValue "auth" + // @Inject "cfg:auth" Auth AuthConfig } @@ -206,9 +206,9 @@ type ComplexConfig struct { Servers []NestedConfig } -// @RouterService name="complex-service", prefix="/api/complex" +// @EndpointService name="complex-service", prefix="/api/complex" type ComplexService struct { - // @InjectCfgValue "config" + // @Inject "cfg:config" Config ComplexConfig } @@ -230,9 +230,9 @@ type AppConfig struct { Port int } -// @RouterService name="default-service", prefix="/api/default" +// @EndpointService name="default-service", prefix="/api/default" type DefaultService struct { - // @InjectCfgValue key="appconfig", default="AppConfig{Name: \"myapp\", Port: 8080}" + // @Inject "cfg:appconfig", "AppConfig{Name: \"myapp\", Port: 8080}" Config AppConfig } @@ -277,9 +277,9 @@ type ScheduleConfig struct { Duration time.Duration } -// @RouterService name="schedule-service", prefix="/api/schedule" +// @EndpointService name="schedule-service", prefix="/api/schedule" type ScheduleService struct { - // @InjectCfgValue "schedule" + // @Inject "cfg:schedule" Config ScheduleConfig } @@ -323,9 +323,9 @@ type ScheduleConfig struct { Duration time.Duration } -// @RouterService name="schedule-service", prefix="/api/schedule" +// @EndpointService name="schedule-service", prefix="/api/schedule" type ScheduleService struct { - // @InjectCfgValue key="schedule", default="ScheduleConfig{EventName: \"DefaultEvent\", Duration: 3600000000000}" + // @Inject "cfg:schedule", "ScheduleConfig{EventName: \"DefaultEvent\", Duration: 3600000000000}" Config ScheduleConfig } @@ -348,9 +348,9 @@ type AppConfig struct { Port int } -// @RouterService name="default-service", prefix="/api/default" +// @EndpointService name="default-service", prefix="/api/default" type DefaultService struct { - // @InjectCfgValue key="appconfig", default=` + "`AppConfig{Name: \"myapp\", Port: 8080}`" + ` + // @Inject "cfg:appconfig", ` + "`AppConfig{Name: \"myapp\", Port: 8080}`" + ` Config AppConfig } diff --git a/core/annotation/test/struct_duration_test.go b/core/annotation/test/struct_duration_test.go index a6ba391d..5569d7dd 100644 --- a/core/annotation/test/struct_duration_test.go +++ b/core/annotation/test/struct_duration_test.go @@ -27,9 +27,9 @@ type ServerConfig struct { Timeout time.Duration } -// @RouterService name="test-service", prefix="/api" +// @EndpointService name="test-service", prefix="/api" type TestService struct { - // @InjectCfgValue "server" + // @Inject "cfg:server" Server ServerConfig } @@ -53,9 +53,11 @@ type ServerConfig struct { Timeout time.Duration } -// @RouterService name="test-service", prefix="/api" +// @EndpointService name="test-service", prefix="/api" type TestService struct { - // @InjectCfgValue key="server", default=` + "`ServerConfig{Host: \"localhost\", Port: 8080, Timeout: 15*time.Minute}`" + ` + // @Inject "cfg:server" + // NOTE: Default value not yet supported for struct config injection via @Inject + // Use config.yaml to provide default values instead Server ServerConfig } @@ -65,7 +67,6 @@ func (s *TestService) GetInfo() string { return "info" } expectedStrings: []string{ "Server", "ServerConfig", "cast.ToStruct", - `ServerConfig{Host: "localhost", Port: 8080, Timeout: 15*time.Minute}`, }, }, { @@ -80,9 +81,9 @@ type ServerConfig struct { Timeout time.Duration } -// @RouterService name="test-service", prefix="/api" +// @EndpointService name="test-service", prefix="/api" type TestService struct { - // @InjectCfgValue key="server", default="ServerConfig{Host: \"localhost\", Port: 8080, Timeout: 15*time.Minute}" + // @Inject "cfg:server" Server ServerConfig } @@ -92,7 +93,6 @@ func (s *TestService) GetInfo() string { return "info" } expectedStrings: []string{ "Server", "ServerConfig", "cast.ToStruct", - `ServerConfig{Host: \"localhost\", Port: 8080, Timeout: 15*time.Minute}`, // Escaped }, }, { @@ -107,9 +107,9 @@ type ServerConfig struct { Timeout time.Duration } -// @RouterService name="test-service", prefix="/api" +// @EndpointService name="test-service", prefix="/api" type TestService struct { - // @InjectCfgValue key="server", default=` + "`ServerConfig{Host: \"localhost\", Port: 8080, Timeout: 900000000000}`" + ` + // @Inject "cfg:server" Server ServerConfig } @@ -119,7 +119,6 @@ func (s *TestService) GetInfo() string { return "info" } expectedStrings: []string{ "Server", "ServerConfig", "cast.ToStruct", - `ServerConfig{Host: "localhost", Port: 8080, Timeout: 900000000000}`, }, }, } diff --git a/core/annotation/test/unused_imports_test.go b/core/annotation/test/unused_imports_test.go index acd9077e..1d9252af 100644 --- a/core/annotation/test/unused_imports_test.go +++ b/core/annotation/test/unused_imports_test.go @@ -24,7 +24,7 @@ import ( core_repository "github.com/primadi/lokstra-auth/infrastructure/repository" // USED in dependency ) -// @RouterService name="test-service", prefix="/api" +// @EndpointService name="test-service", prefix="/api" type TestService struct { // @Inject "user-repository" Repo core_repository.UserRepository @@ -111,13 +111,13 @@ type CreateUserParams struct { t.Error("Generated code should import lokstra_registry") } - // Should include deploy and proxy for @RouterService + // Should include deploy and proxy for @EndpointService if !strings.Contains(genCode, `"github.com/primadi/lokstra/core/deploy"`) { - t.Error("Generated code should import deploy for @RouterService") + t.Error("Generated code should import deploy for @EndpointService") } if !strings.Contains(genCode, `"github.com/primadi/lokstra/core/proxy"`) { - t.Error("Generated code should import proxy for @RouterService") + t.Error("Generated code should import proxy for @EndpointService") } t.Logf("✅ Generated code correctly filtered unused imports") @@ -176,7 +176,7 @@ import ( "testapp/helper" // NOT used in handler, only in helper method ) -// @RouterService name="test-service", prefix="/api" +// @EndpointService name="test-service", prefix="/api" type TestService struct { } diff --git a/core/annotation/testdata/cfg_injection_test.go b/core/annotation/testdata/cfg_injection_test.go index b067666a..793ad9fd 100644 --- a/core/annotation/testdata/cfg_injection_test.go +++ b/core/annotation/testdata/cfg_injection_test.go @@ -43,7 +43,7 @@ func (s *MySQLTenantStore) SaveTenant(tenant *Tenant) error { return nil } -// @RouterService name="tenant-service", prefix="/api/tenants", middlewares=["recovery"] +// @EndpointService name="tenant-service", prefix="/api/tenants", middlewares=["recovery"] type TenantService struct { // @Inject "cfg:store.tenant-store" Store TenantStore diff --git a/core/deploy/loader/builder.go b/core/deploy/loader/builder.go index f7291e48..caa6f211 100644 --- a/core/deploy/loader/builder.go +++ b/core/deploy/loader/builder.go @@ -758,7 +758,7 @@ func RegisterDefinitionsForRuntime(registry *deploy.GlobalRegistry, config *sche continue } - // Get service type metadata (has router config from @RouterService annotation) + // Get service type metadata (has router config from @EndpointService annotation) metadata := registry.GetServiceMetadata(serviceDef.Type) if metadata == nil { // Skip services without metadata diff --git a/core/deploy/registry.go b/core/deploy/registry.go index 84ec2dc5..1df1af1c 100644 --- a/core/deploy/registry.go +++ b/core/deploy/registry.go @@ -206,7 +206,7 @@ func ResetGlobalRegistryForTesting() { // // Both local and remote factories support all three signatures. // RegisterRouterServiceType registers a service type with HTTP routing configuration. -// Use this for services that expose HTTP endpoints (annotated with @RouterService). +// Use this for services that expose HTTP endpoints (annotated with @EndpointService). // For simple infrastructure services (DB, Redis, etc), use RegisterServiceType instead. // // Parameters: diff --git a/docs/00-introduction/examples/annotations/README.md b/docs/00-introduction/examples/annotations/README.md deleted file mode 100644 index 55bc10a1..00000000 --- a/docs/00-introduction/examples/annotations/README.md +++ /dev/null @@ -1,543 +0,0 @@ -# Lokstra Annotations Examples - -This folder contains complete examples of using Lokstra annotations for dependency injection and code generation. - -## Files - -### 1. `service_example.go` - @Service Examples - -Pure services without HTTP endpoints, demonstrating: - -**AuthService:** -- ✅ Required dependencies: `@Inject "user-repository"`, `@Inject "cache-service"` -- ✅ String config: `@InjectCfgValue "auth.jwt-secret"` -- ✅ Duration config: `@InjectCfgValue key="auth.token-expiry", default="24h"` -- ✅ Int config: `@InjectCfgValue key="auth.max-attempts", default=5` -- ✅ Bool config: `@InjectCfgValue key="auth.debug-mode", default=false` - -**NotificationService:** -- ✅ String config: `@InjectCfgValue "smtp.host"` -- ✅ Int config with default: `@InjectCfgValue key="smtp.port", default=587` -- ✅ String config with default: `@InjectCfgValue key="smtp.from-email", default="noreply@example.com"` -- ✅ Bool config with default: `@InjectCfgValue key="notification.enabled", default=true` - -### 2. `router_service_with_config_example.go` - @RouterService Example - -HTTP service with routes, demonstrating: - -**UserAPIService:** -- ✅ HTTP routes: `@Route "GET /{id}"`, `@Route "POST /"`, etc. -- ✅ Per-route middleware: `middlewares=["auth"]`, `middlewares=["auth", "admin"]` -- ✅ Required dependencies: `@Inject "user-repository"`, `@Inject "cache-service"` -- ✅ Multiple config injections for API settings, rate limiting, pagination -- ✅ Bool, int, duration, string configs - -### 3. `init_example.go` - Init() Method Pattern - -Service with post-initialization setup: - -**CacheManager:** -- ✅ Config injection: `@InjectCfgValue key="cache.max-size", default=1000` -- ✅ `Init() error` method called after dependency injection -- ✅ Internal state initialization (maps, slices) -- ✅ Configuration validation -- ✅ Pre-loading data - -### 4. `router_with_init_example.go` - RouterService with Init() - -HTTP service with initialization: - -**ProductAPIService:** -- ✅ Dependency injection: `@Inject "product-repository"` -- ✅ Config injection: `@InjectCfgValue key="api.products.max-items", default=100` -- ✅ `Init() error` method for cache setup -- ✅ HTTP routes with internal state - -### 5. `config.example.yaml` - Configuration - -Example configuration file showing: -- Config values for all injected configurations -- Service definitions (dependencies) -- Deployment configuration for development and production - -## How to Use - -### 1. Copy Examples to Your Project - -```bash -cp service_example.go your-project/application/ -cp router_service_with_config_example.go your-project/application/ -cp config.example.yaml your-project/config.yaml -``` - -### 2. Generate Code - -```bash -# Automatic (recommended) -go run . # lokstra.Bootstrap() auto-generates - -# Manual -lokstra autogen ./application - -# Force rebuild -go run . --generate-only -``` - -### 3. Generated Code - -After running code generation, you'll get `zz_generated.lokstra.go` with: - -**For @Service:** -```go -func RegisterAuthService() { - lokstra_registry.RegisterLazyService("auth-service", func(deps map[string]any, cfg map[string]any) any { - svc := &AuthService{ - Cache: deps["cache-service"].(CacheService), - UserRepo: deps["user-repository"].(UserRepository), - DebugMode: cfg["auth.debug-mode"].(bool), - JwtSecret: cfg["auth.jwt-secret"].(string), - MaxAttempts: cfg["auth.max-attempts"].(int), - TokenExpiry: cfg["auth.token-expiry"].(time.Duration), - } - - return svc - }, map[string]any{ - "depends-on": []string{ "cache-service", "user-repository", }, - "auth.debug-mode": lokstra_registry.GetConfig("auth.debug-mode", false), - "auth.jwt-secret": lokstra_registry.GetConfig("auth.jwt-secret", ""), - "auth.max-attempts": lokstra_registry.GetConfig("auth.max-attempts", 5), - "auth.token-expiry": lokstra_registry.GetConfig("auth.token-expiry", 24*time.Hour), - }) -} -``` - -**For @Service with Init():** -```go -func RegisterCacheManager() { - lokstra_registry.RegisterLazyService("cache-manager", func(deps map[string]any, cfg map[string]any) any { - svc := &CacheManager{ - MaxSize: cfg["cache.max-size"].(int), - TTLSeconds: cfg["cache.ttl-seconds"].(int), - } - - // Call Init() for post-initialization - if err := svc.Init(); err != nil { - panic("failed to initialize cache-manager: " + err.Error()) - } - - return svc - }, map[string]any{ - "cache.max-size": lokstra_registry.GetConfig("cache.max-size", 1000), - "cache.ttl-seconds": lokstra_registry.GetConfig("cache.ttl-seconds", 300), - }) -} -``` - -**For @RouterService:** -```go -func UserAPIServiceFactory(deps map[string]any, config map[string]any) any { - svc := &UserAPIService{ - Cache: deps["cache-service"].(CacheService), - UserRepo: deps["user-repository"].(UserRepository), - RateLimitEnabled: config["api.rate-limit.enabled"].(bool), - MaxRequests: config["api.rate-limit.max-requests"].(int), - // ... all other fields - } - - return svc -} - -func RegisterUserAPIService() { - lokstra_registry.RegisterRouterServiceType("user-api-service-factory", - UserAPIServiceFactory, - UserAPIServiceRemoteFactory, - &deploy.ServiceTypeConfig{ - PathPrefix: "/api/v1/users", - Middlewares: []string{"recovery", "request-logger"}, - RouteOverrides: map[string]deploy.RouteConfig{ - "GetByID": {Path: "GET /{id}"}, - "Create": {Path: "POST /", Middlewares: []string{"auth"}}, - // ... all routes - }, - }, - ) - - lokstra_registry.RegisterLazyService("user-api-service", - "user-api-service-factory", - map[string]any{ - "depends-on": []string{ "cache-service", "user-repository", }, - "api.rate-limit.enabled": lokstra_registry.GetConfig("api.rate-limit.enabled", true), - // ... all config values - }) -} -``` - -## Annotation Reference - -### @Service -```go -// @Service name="service-name" -type ServiceName struct { } -``` -- Used for pure services (no HTTP) -- Auto-generates `RegisterLazyService` -- All dependencies are **mandatory** (panic if not found) - -### @RouterService -```go -// @RouterService name="service-name", prefix="/api/path", middlewares=["mw1"] -type ServiceName struct { } -``` -- Used for HTTP services -- Auto-generates routes, factory, and registration -- All dependencies are **mandatory** (panic if not found) - -### @Inject -```go -// Inject service dependency -// @Inject "service-name" -Field ServiceType -``` -- Injects service dependencies -- Works with both `@Service` and `@RouterService` -- **All dependencies are mandatory** - framework panics if not found -- Generates: `deps["service-name"].(ServiceType)` - -### @InjectCfgValue -```go -// Required config -// @InjectCfgValue "config.key" -Field string - -// With default (unquoted for non-string types) -// @InjectCfgValue key="config.key", default=100 -Field int - -// @InjectCfgValue key="config.key", default=true -Field bool - -// @InjectCfgValue key="config.key", default="24h" -Field time.Duration -``` -- Injects configuration from `config.yaml` -- Works with both `@Service` and `@RouterService` -- Auto-detects type: `string`, `int`, `bool`, `float64`, `time.Duration` -- Default values are type-specific (no quotes for int/bool/float) - -### @Route -```go -// @Route "GET /path/{id}" -func (s *Service) Method(p *Params) (*Result, error) - -// With per-route middleware -// @Route "POST /path", middlewares=["auth", "admin"] -func (s *Service) Method(p *Params) (*Result, error) -``` -- Only for `@RouterService` -- Defines HTTP endpoints - -### Init() Method (Optional) - -```go -// @Service name="my-service" -type MyService struct { - // @InjectCfgValue key="max-size", default=100 - MaxSize int - - // Internal state (not injected) - cache map[string]any -} - -// Called automatically after dependency injection -func (s *MyService) Init() error { - // Initialize internal state - s.cache = make(map[string]any, s.MaxSize) - - // Validate configuration - if s.MaxSize <= 0 { - return fmt.Errorf("max size must be positive") - } - - // Pre-load data, setup connections, etc. - logger.LogInfo("Service initialized") - return nil -} -``` - -**Init() Requirements:** -- Method name must be exactly `Init` -- Signature: `func (s *Service) Init() error` -- No parameters -- Returns `error` -- Called after all dependencies and configs are injected -- If returns error, service creation panics - -**Use Init() for:** -- ✅ Initialize maps, slices, channels -- ✅ Validate injected configuration -- ✅ Pre-load data or cache -- ✅ Setup complex internal state -- ✅ Establish connections (after config available) - -## Testing - -1. **Start the application:** - ```bash - go run . - ``` - -2. **Test endpoints:** - ```bash - # Get user - curl http://localhost:8080/api/v1/users/123 - - # List users - curl http://localhost:8080/api/v1/users - - # Create user (requires auth) - curl -X POST http://localhost:8080/api/v1/users \ - -H "Content-Type: application/json" \ - -d '{"name":"John","email":"john@example.com"}' - ``` - -## Key Features Demonstrated - -✅ **Dependency Injection**: Mandatory service dependencies (panic if missing) -✅ **Configuration Injection**: Type-safe config from YAML -✅ **Auto Type Detection**: Config types inferred from field type -✅ **Default Values**: Sensible defaults when config missing -✅ **Init() Method**: Post-initialization setup and validation -✅ **HTTP Routes**: Automatic REST API generation -✅ **Per-Route Middleware**: Fine-grained access control -✅ **Service Separation**: `@Service` for logic, `@RouterService` for HTTP - -## Design Patterns - -### 1. Simple Service (Config Only) -```go -// @Service name="email-service" -type EmailService struct { - // @InjectCfgValue "smtp.host" - SMTPHost string -} -``` - -### 2. Service with Dependencies -```go -// @Service name="auth-service" -type AuthService struct { - // @Inject "user-repository" - UserRepo UserRepository - - // @InjectCfgValue "auth.jwt-secret" - JwtSecret string -} -``` - -### 3. Service with Init() -```go -// @Service name="cache-manager" -type CacheManager struct { - // @InjectCfgValue key="cache.max-size", default=1000 - MaxSize int - - cache map[string]any -} - -func (c *CacheManager) Init() error { - c.cache = make(map[string]any, c.MaxSize) - return nil -} -``` - -### 4. HTTP Service -```go -// @RouterService name="user-api", prefix="/api/users" -type UserAPIService struct { - // @Inject "user-repository" - UserRepo UserRepository -} - -// @Route "GET /{id}" -func (s *UserAPIService) Get(p *GetParams) (*User, error) { } -``` - -## Next Steps - -1. Read the [Full Documentation](https://primadi.github.io/lokstra/) -2. See [AI Agent Guide](../../AI-AGENT-GUIDE.md) for best practices -3. Check [Quick Reference](../../QUICK-REFERENCE.md) for common patterns -4. Explore [Full Framework Examples](../full-framework/) for larger projects - -Example configuration file showing: -- Config values for all injected configurations -- Service definitions (dependencies) -- Deployment configuration for development and production - -## How to Use - -### 1. Copy Examples to Your Project - -```bash -cp service_example.go your-project/application/ -cp router_service_with_config_example.go your-project/application/ -cp config.example.yaml your-project/config.yaml -``` - -### 2. Generate Code - -```bash -# Automatic (recommended) -go run . # lokstra.Bootstrap() auto-generates - -# Manual -lokstra autogen ./application - -# Force rebuild -go run . --generate-only -``` - -### 3. Generated Code - -After running code generation, you'll get `zz_generated.lokstra.go` with: - -**For @Service:** -```go -func RegisterAuthService() { - lokstra_registry.RegisterLazyService("auth-service", func(deps map[string]any, cfg map[string]any) any { - return &AuthService{ - UserRepo: lokstra_registry.GetService[UserRepository]("user-repository"), - Cache: // Optional - nil if not found - JwtSecret: lokstra_registry.GetConfig("auth.jwt-secret", ""), - TokenExpiry: lokstra_registry.GetConfigDuration("auth.token-expiry", 24*time.Hour), - MaxAttempts: lokstra_registry.GetConfigInt("auth.max-attempts", 5), - DebugMode: lokstra_registry.GetConfigBool("auth.debug-mode", false), - } - }, nil) -} -``` - -**For @RouterService:** -```go -func UserAPIServiceFactory(deps map[string]any, config map[string]any) any { - return &UserAPIService{ - UserRepo: deps["user-repository"].(domain.UserRepository), - Cache: // Optional - nil if not found - RateLimitEnabled: lokstra_registry.GetConfigBool("api.rate-limit.enabled", true), - MaxRequests: lokstra_registry.GetConfigInt("api.rate-limit.max-requests", 100), - // ... all other configs - } -} - -func RegisterUserAPIService() { - lokstra_registry.RegisterRouterServiceType("user-api-service-factory", - UserAPIServiceFactory, - UserAPIServiceRemoteFactory, - &deploy.ServiceTypeConfig{ - PathPrefix: "/api/v1/users", - Middlewares: []string{"recovery", "request-logger"}, - RouteOverrides: map[string]deploy.RouteConfig{ - "GetByID": {Path: "GET /{id}"}, - "Create": {Path: "POST /", Middlewares: []string{"auth"}}, - // ... all routes - }, - }, - ) -} -``` - -## Annotation Reference - -### @Service -```go -// @Service name="service-name" -type ServiceName struct { } -``` -- Used for pure services (no HTTP) -- Auto-generates `RegisterLazyService` - -### @RouterService -```go -// @RouterService name="service-name", prefix="/api/path", middlewares=["mw1"] -type ServiceName struct { } -``` -- Used for HTTP services -- Auto-generates routes, factory, and registration - -### @Inject -```go -// Required -// @Inject "service-name" -Field ServiceType - -// Optional -// @Inject service="service-name", optional=true -Field ServiceType // nil if not found -``` -- Injects service dependencies -- Works with both `@Service` and `@RouterService` - -### @InjectCfgValue -```go -// Required -// @InjectCfgValue "config.key" -Field string - -// With default -// @InjectCfgValue key="config.key", default="value" -Field string -``` -- Injects configuration from `config.yaml` -- Works with both `@Service` and `@RouterService` -- Auto-detects type: `string`, `int`, `bool`, `float64`, `time.Duration` - -### @Route -```go -// @Route "GET /path/{id}" -func (s *Service) Method(p *Params) (*Result, error) - -// With per-route middleware -// @Route "POST /path", middlewares=["auth", "admin"] -func (s *Service) Method(p *Params) (*Result, error) -``` -- Only for `@RouterService` -- Defines HTTP endpoints - -## Testing - -1. **Start the application:** - ```bash - go run . - ``` - -2. **Test endpoints:** - ```bash - # Get user - curl http://localhost:8080/api/v1/users/123 - - # List users - curl http://localhost:8080/api/v1/users - - # Create user (requires auth) - curl -X POST http://localhost:8080/api/v1/users \ - -H "Content-Type: application/json" \ - -d '{"name":"John","email":"john@example.com"}' - ``` - -## Key Features Demonstrated - -✅ **Dependency Injection**: Required and optional service dependencies -✅ **Configuration Injection**: Type-safe config from YAML -✅ **Optional Dependencies**: Graceful degradation (e.g., cache) -✅ **Auto Type Detection**: Config types inferred from field type -✅ **Default Values**: Sensible defaults when config missing -✅ **HTTP Routes**: Automatic REST API generation -✅ **Per-Route Middleware**: Fine-grained access control -✅ **Service Separation**: `@Service` for logic, `@RouterService` for HTTP - -## Next Steps - -1. Read the [Full Documentation](https://primadi.github.io/lokstra/) -2. See [AI Agent Guide](../../AI-AGENT-GUIDE.md) for best practices -3. Check [Quick Reference](../../QUICK-REFERENCE.md) for common patterns -4. Explore [Full Framework Examples](../full-framework/) for larger projects diff --git a/docs/00-introduction/examples/annotations/config.example.yaml b/docs/00-introduction/examples/annotations/config.example.yaml deleted file mode 100644 index e27ae583..00000000 --- a/docs/00-introduction/examples/annotations/config.example.yaml +++ /dev/null @@ -1,92 +0,0 @@ -# Example configuration for @Service and @RouterService with @InjectCfgValue - -# Configuration for @Service examples (service_example.go) -configs: - # Auth service configuration - auth: - jwt-secret: "your-super-secret-jwt-key-change-in-production" - token-expiry: "48h" # 48 hours - max-attempts: 3 - debug-mode: false - - # SMTP configuration for notification service - smtp: - host: "smtp.gmail.com" - port: 587 - from-email: "noreply@myapp.com" - - # Notification settings - notification: - enabled: true - - # API configuration for @RouterService (router_service_with_config_example.go) - api: - jwt-secret: "api-jwt-secret-key" - token-expiry: "24h" - - # Rate limiting - rate-limit: - enabled: true - max-requests: 1000 - window: "1m" - - # Pagination - pagination: - default-page-size: 20 - max-page-size: 100 - - # Response options - response: - include-metadata: true - -# Service definitions -service-definitions: - # Repository layer - user-repository: - type: user-repository-factory - - # Optional cache service - cache-service: - type: redis-cache-factory - depends-on: [] - - # @Service examples - auth-service: - type: auth-service # Auto-registered by @Service annotation - - notification-service: - type: notification-service # Auto-registered by @Service annotation - - # @RouterService example - user-api-service: - type: user-api-service-factory # Auto-registered by @RouterService annotation - depends-on: - - user-repository - -# Deployment configuration -deployments: - development: - servers: - api: - addr: ":8080" - published-services: - - user-api-service - middlewares: - recovery: {} - request-logger: - enabled: true - cors: - allowed-origins: ["*"] - - production: - servers: - api: - addr: ":8080" - published-services: - - user-api-service - middlewares: - recovery: {} - request-logger: - enabled: true - cors: - allowed-origins: ["https://myapp.com"] diff --git a/docs/00-introduction/examples/annotations/domain.go b/docs/00-introduction/examples/annotations/domain.go deleted file mode 100644 index 709946a4..00000000 --- a/docs/00-introduction/examples/annotations/domain.go +++ /dev/null @@ -1,26 +0,0 @@ -package application - -import "time" - -// Shared domain types for annotation examples - -type User struct { - ID string - Email string - Name string -} - -type UserRepository interface { - GetByID(id string) (*User, error) - GetByEmail(email string) (*User, error) - List() ([]*User, error) - Create(user *User) (*User, error) - Update(user *User) (*User, error) - Delete(id string) error -} - -type CacheService interface { - Get(key string) (any, error) - Set(key string, value any, ttl time.Duration) error - Delete(key string) error -} diff --git a/docs/00-introduction/examples/annotations/init_example.go b/docs/00-introduction/examples/annotations/init_example.go deleted file mode 100644 index 32d6f7ed..00000000 --- a/docs/00-introduction/examples/annotations/init_example.go +++ /dev/null @@ -1,54 +0,0 @@ -package application - -import ( - "fmt" - - "github.com/primadi/lokstra/common/logger" -) - -// Example with Init() method - -// @Service name="cache-manager" -type CacheManager struct { - // @InjectCfgValue key="cache.max-size", default=1000 - MaxSize int - - // @InjectCfgValue key="cache.ttl-seconds", default=300 - TTLSeconds int - - // Internal state (not injected) - cache map[string]any -} - -// Init is called after dependency injection -func (c *CacheManager) Init() error { - // Initialize internal state - c.cache = make(map[string]any, c.MaxSize) - - // Validation - if c.MaxSize <= 0 { - return fmt.Errorf("cache max size must be positive, got %d", c.MaxSize) - } - - if c.TTLSeconds <= 0 { - return fmt.Errorf("cache TTL must be positive, got %d", c.TTLSeconds) - } - - logger.LogInfo("✅ CacheManager initialized: max_size=%d, ttl=%ds", c.MaxSize, c.TTLSeconds) - return nil -} - -func (c *CacheManager) Set(key string, value any) { - if c.cache == nil { - c.cache = make(map[string]any) - } - c.cache[key] = value -} - -func (c *CacheManager) Get(key string) (any, bool) { - if c.cache == nil { - return nil, false - } - val, ok := c.cache[key] - return val, ok -} diff --git a/docs/00-introduction/examples/annotations/mixed_services_example.go b/docs/00-introduction/examples/annotations/mixed_services_example.go deleted file mode 100644 index c373d74b..00000000 --- a/docs/00-introduction/examples/annotations/mixed_services_example.go +++ /dev/null @@ -1,123 +0,0 @@ -package application - -import ( - "time" -) - -// Example file with mixed @Service and @RouterService annotations - -// ============================================================ -// Pure Service (No HTTP) -// ============================================================ - -// @Service name="email-service" -type EmailService struct { - // @InjectCfgValue key="smtp.host" - SMTPHost string - - // @InjectCfgValue key="smtp.port", default=587 - SMTPPort int - - // @InjectCfgValue key="smtp.username" - SMTPUsername string - - // @InjectCfgValue key="smtp.password" - SMTPPassword string - - // @InjectCfgValue key="email.from", default="noreply@example.com" - FromEmail string -} - -func (s *EmailService) SendEmail(to, subject, body string) error { - // Send email implementation - println("Sending email from", s.FromEmail, "to", to, "via", s.SMTPHost) - return nil -} - -// ============================================================ -// Another Pure Service -// ============================================================ - -// @Service name="background-job-service" -type BackgroundJobService struct { - // @Inject "email-service" - EmailService *EmailService - - // @InjectCfgValue key="jobs.max-workers", default=10 - MaxWorkers int - - // @InjectCfgValue key="jobs.retry-limit", default=3 - RetryLimit int - - // @InjectCfgValue key="jobs.retry-delay", default="5s" - RetryDelay time.Duration -} - -func (s *BackgroundJobService) ProcessJob(jobID string) error { - // Process job and send notification - return s.EmailService.SendEmail("admin@example.com", "Job Complete", "Job "+jobID+" completed") -} - -// ============================================================ -// HTTP Service (RouterService) -// ============================================================ - -// @RouterService name="admin-api-service", prefix="/api/admin", middlewares=["recovery", "auth", "admin"] -type AdminAPIService struct { - // @Inject "background-job-service" - JobService *BackgroundJobService - - // @Inject service="email-service" - EmailService *EmailService - - // @InjectCfgValue key="admin.allow-job-restart", default=true - AllowJobRestart bool - - // @InjectCfgValue key="admin.max-jobs-per-page", default=50 - MaxJobsPerPage int -} - -// @Route "POST /jobs/{id}/restart" -func (s *AdminAPIService) RestartJob(req *RestartJobRequest) (*JobResponse, error) { - if !s.AllowJobRestart { - return nil, nil - } - - err := s.JobService.ProcessJob(req.JobID) - if err != nil { - return nil, err - } - - return &JobResponse{ - JobID: req.JobID, - Status: "restarted", - }, nil -} - -// @Route "GET /jobs" -func (s *AdminAPIService) ListJobs(req *ListJobsRequest) (*JobListResponse, error) { - // List jobs with pagination - return &JobListResponse{ - Jobs: []*JobResponse{}, - PageSize: s.MaxJobsPerPage, - }, nil -} - -// Request/Response DTOs for AdminAPIService -type RestartJobRequest struct { - JobID string `path:"id"` -} - -type ListJobsRequest struct { - Page int `query:"page"` -} - -type JobResponse struct { - JobID string `json:"job_id"` - Status string `json:"status"` -} - -type JobListResponse struct { - Jobs []*JobResponse `json:"jobs"` - PageSize int `json:"page_size"` -} diff --git a/docs/00-introduction/examples/annotations/router_service_with_config_example.go b/docs/00-introduction/examples/annotations/router_service_with_config_example.go deleted file mode 100644 index 950cb3d5..00000000 --- a/docs/00-introduction/examples/annotations/router_service_with_config_example.go +++ /dev/null @@ -1,225 +0,0 @@ -package application - -import ( - "fmt" - "time" -) - -// Request DTOs -type GetUserRequest struct { - ID string `path:"id"` -} - -type ListUsersRequest struct { - Page int `query:"page"` - PageSize int `query:"page_size"` -} - -type CreateUserRequest struct { - Name string `json:"name" validate:"required"` - Email string `json:"email" validate:"required,email"` -} - -type UpdateUserRequest struct { - ID string `path:"id"` - Name string `json:"name" validate:"required"` - Email string `json:"email" validate:"required,email"` -} - -type DeleteUserRequest struct { - ID string `path:"id"` -} - -// Response DTOs -type UserResponse struct { - User *User `json:"user"` - Metadata map[string]any `json:"metadata,omitempty"` -} - -type UserListResponse struct { - Users []*User `json:"users"` - PageSize int `json:"page_size"` - Metadata map[string]any `json:"metadata,omitempty"` -} - -// Example: @RouterService with @Inject (optional), @InjectCfgValue, and @Route - -// @RouterService name="user-api-service", prefix="/api/v1/users", middlewares=["recovery", "request-logger"] -type UserAPIService struct { - // Required dependency - // @Inject "user-repository" - UserRepo UserRepository - - // Cache dependency for performance - // @Inject "cache-service" - Cache CacheService - - // Configuration: API rate limiting - // @InjectCfgValue key="api.rate-limit.enabled", default=true - RateLimitEnabled bool - - // @InjectCfgValue key="api.rate-limit.max-requests", default=100 - MaxRequests int - - // @InjectCfgValue "api.rate-limit.window", "1m" - RateLimitWindow time.Duration - - // Configuration: Pagination - // @InjectCfgValue key="api.pagination.default-page-size", default="20" - DefaultPageSize int - - // @InjectCfgValue key="api.pagination.max-page-size", default="100" - MaxPageSize int - - // Configuration: Response - // @InjectCfgValue key="api.response.include-metadata", default="true" - IncludeMetadata bool - - // Configuration: Authentication - // @InjectCfgValue "api.jwt-secret" - JwtSecret string - - // @InjectCfgValue key="api.token-expiry", default="24h" - TokenExpiry time.Duration -} - -// @Route "GET /{id}" -func (s *UserAPIService) GetByID(p *GetUserRequest) (*UserResponse, error) { - // Check cache if available - if s.Cache != nil { - cacheKey := "user:" + p.ID - if cached, err := s.Cache.Get(cacheKey); err == nil { - return cached.(*UserResponse), nil - } - } - - user, err := s.UserRepo.GetByID(p.ID) - if err != nil { - return nil, err - } - - response := &UserResponse{ - User: user, - Metadata: s.buildMetadata(), - } - - // Cache if available - if s.Cache != nil { - _ = s.Cache.Set("user:"+p.ID, response, 5*time.Minute) - } - - return response, nil -} - -// @Route "GET /" -func (s *UserAPIService) List(p *ListUsersRequest) (*UserListResponse, error) { - // Apply default page size - if p.PageSize == 0 { - p.PageSize = s.DefaultPageSize - } - - // Enforce max page size - if p.PageSize > s.MaxPageSize { - p.PageSize = s.MaxPageSize - } - - users, err := s.UserRepo.List() - if err != nil { - return nil, err - } - - response := &UserListResponse{ - Users: users, - PageSize: p.PageSize, - } - - if s.IncludeMetadata { - response.Metadata = s.buildMetadata() - } - - return response, nil -} - -// @Route "POST /", ["auth"] -func (s *UserAPIService) Create(p *CreateUserRequest) (*UserResponse, error) { - // Check rate limit - if s.RateLimitEnabled { - // Rate limiting logic here - _ = s.MaxRequests - _ = s.RateLimitWindow - } - - user := &User{ - Name: p.Name, - Email: p.Email, - } - - created, err := s.UserRepo.Create(user) - if err != nil { - return nil, err - } - - response := &UserResponse{ - User: created, - Metadata: s.buildMetadata(), - } - - return response, nil -} - -// @Route "PUT /{id}", ["auth"] -func (s *UserAPIService) Update(p *UpdateUserRequest) (*UserResponse, error) { - user := &User{ - ID: p.ID, - Name: p.Name, - Email: p.Email, - } - - updated, err := s.UserRepo.Update(user) - if err != nil { - return nil, err - } - - // Invalidate cache if available - if s.Cache != nil { - _ = s.Cache.Delete("user:" + p.ID) - } - - response := &UserResponse{ - User: updated, - Metadata: s.buildMetadata(), - } - - return response, nil -} - -// @Route "DELETE /{id}", ["auth", "admin"] -func (s *UserAPIService) Delete(p *DeleteUserRequest) error { - err := s.UserRepo.Delete(p.ID) - if err != nil { - return err - } - - // Invalidate cache if available - if s.Cache != nil { - _ = s.Cache.Delete("user:" + p.ID) - } - - return nil -} - -func (s *UserAPIService) buildMetadata() map[string]any { - if !s.IncludeMetadata { - return nil - } - - return map[string]any{ - "timestamp": time.Now().Unix(), - "rate_limit_window": s.RateLimitWindow.String(), - "max_page_size": fmt.Sprintf("%d", s.MaxPageSize), - } -} - -func Register() { - // Package auto-loaded by code generation -} diff --git a/docs/00-introduction/examples/annotations/router_with_init_example.go b/docs/00-introduction/examples/annotations/router_with_init_example.go deleted file mode 100644 index 3ba8a393..00000000 --- a/docs/00-introduction/examples/annotations/router_with_init_example.go +++ /dev/null @@ -1,71 +0,0 @@ -package application - -import ( - "fmt" - - "github.com/primadi/lokstra/common/logger" -) - -// Example RouterService with Init() method - -// @RouterService name="product-api", prefix="/api/products" -type ProductAPIService struct { - // @Inject "product-repository" - ProductRepo ProductRepository - - // @InjectCfgValue key="api.products.max-items", default=100 - MaxItems int - - // Internal cache (initialized in Init()) - categoryCache map[string][]string -} - -// Init is called after dependency injection -func (s *ProductAPIService) Init() error { - // Initialize internal cache - s.categoryCache = make(map[string][]string) - - // Validate configuration - if s.MaxItems <= 0 { - return fmt.Errorf("max items must be positive, got %d", s.MaxItems) - } - - // Pre-load data if needed - logger.LogInfo("✅ ProductAPIService initialized: max_items=%d", s.MaxItems) - return nil -} - -// @Route "GET /" -func (s *ProductAPIService) ListProducts(req *ListProductsRequest) (*ProductListResponse, error) { - // Use cache - _ = s.categoryCache - return &ProductListResponse{}, nil -} - -// @Route "GET /{id}" -func (s *ProductAPIService) GetProduct(req *GetProductRequest) (*ProductResponse, error) { - return &ProductResponse{}, nil -} - -// DTOs -type ListProductsRequest struct { - Category string `query:"category"` -} - -type GetProductRequest struct { - ID string `path:"id"` -} - -type ProductResponse struct { - ID string `json:"id"` - Name string `json:"name"` -} - -type ProductListResponse struct { - Products []*ProductResponse `json:"products"` -} - -// Domain interface -type ProductRepository interface { - FindAll() ([]*ProductResponse, error) -} diff --git a/docs/00-introduction/examples/annotations/service_example.go b/docs/00-introduction/examples/annotations/service_example.go deleted file mode 100644 index ea3d91c4..00000000 --- a/docs/00-introduction/examples/annotations/service_example.go +++ /dev/null @@ -1,89 +0,0 @@ -package application - -import ( - "time" -) - -// Example: @Service annotation with @Inject and @InjectCfgValue - -// @Service name="auth-service" -type AuthService struct { - // @Inject "user-repository" - UserRepo UserRepository - - // @Inject "cache-service" - Cache CacheService - - // @InjectCfgValue "auth.jwt-secret" - JwtSecret string - - // @InjectCfgValue key="auth.token-expiry", default="24h" - TokenExpiry time.Duration - - // @InjectCfgValue key="auth.max-attempts", default=5 - MaxAttempts int - - // @InjectCfgValue key="auth.debug-mode", default="false" - DebugMode bool -} - -// Login authenticates user and returns JWT token -func (s *AuthService) Login(email, password string) (string, error) { - // Check cache first if available - if s.Cache != nil { - if cachedUser, err := s.Cache.Get("user:" + email); err == nil { - // Use cached user - _ = cachedUser - } - } - - user, err := s.UserRepo.GetByEmail(email) - if err != nil { - return "", err - } - - // Verify password (simplified) - if user.Email != email { - return "", nil - } - - // Generate JWT token with configured expiry - token := "jwt_token_for_" + user.ID + "_expires_in_" + s.TokenExpiry.String() - - // Cache user if cache is available - if s.Cache != nil { - _ = s.Cache.Set("user:"+email, user, s.TokenExpiry) - } - - if s.DebugMode { - println("Debug: Login successful for", email) - } - - return token, nil -} - -// @Service name="notification-service" -type NotificationService struct { - // @InjectCfgValue "smtp.host" - SMTPHost string - - // @InjectCfgValue key="smtp.port", default="587" - SMTPPort int - - // @InjectCfgValue key="smtp.from-email", default="noreply@example.com" - FromEmail string - - // @InjectCfgValue key="notification.enabled", default="true" - Enabled bool -} - -// SendEmail sends notification email -func (s *NotificationService) SendEmail(to, subject, body string) error { - if !s.Enabled { - return nil // Skip if disabled - } - - // Send email via SMTP - println("Sending email from", s.FromEmail, "to", to, "via", s.SMTPHost+":"+string(rune(s.SMTPPort))) - return nil -} diff --git a/docs/00-introduction/examples/annotations/zz_cache.lokstra.json b/docs/00-introduction/examples/annotations/zz_cache.lokstra.json deleted file mode 100644 index 608dbd71..00000000 --- a/docs/00-introduction/examples/annotations/zz_cache.lokstra.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "version": 1, - "files": { - "init_example.go": { - "filename": "init_example.go", - "checksum": "696a7b4b95565d9f3716cd685310671eb138cf19f86772a58f06e40fe794b61e", - "annotations": 3, - "last_scan": "2025-12-07T03:46:41.7400124+07:00", - "generated": [ - "zz_generated.lokstra.go" - ], - "generated_mod_time": "2025-12-07T03:46:41.7400124+07:00" - }, - "mixed_services_example.go": { - "filename": "mixed_services_example.go", - "checksum": "7e27d4c1dd03132ad25c2d48a3d9aee10848963897d8e436b950ebcc184f902a", - "annotations": 18, - "last_scan": "2025-12-07T03:46:41.7400124+07:00", - "generated": [ - "zz_generated.lokstra.go" - ], - "generated_mod_time": "2025-12-07T03:46:41.7400124+07:00" - }, - "router_service_with_config_example.go": { - "filename": "router_service_with_config_example.go", - "checksum": "8615e7b99db57225d9c442e2bcfcdddaceae1d38be692dec2daaf7be3cdfcbe0", - "annotations": 16, - "last_scan": "2025-12-07T03:46:41.7400124+07:00", - "generated": [ - "zz_generated.lokstra.go" - ], - "generated_mod_time": "2025-12-07T03:46:41.7400124+07:00" - }, - "router_with_init_example.go": { - "filename": "router_with_init_example.go", - "checksum": "0618ae863b04e0d7f1759085749465572e9e41fff1f8c489afaa6de43e69564f", - "annotations": 5, - "last_scan": "2025-12-07T03:46:41.7400124+07:00", - "generated": [ - "zz_generated.lokstra.go" - ], - "generated_mod_time": "2025-12-07T03:46:41.7400124+07:00" - }, - "service_example.go": { - "filename": "service_example.go", - "checksum": "e1830383b1cab4f69bca409897d06039121f08185bbcbbdb1c869f7e20943e68", - "annotations": 12, - "last_scan": "2025-12-07T03:46:41.7400124+07:00", - "generated": [ - "zz_generated.lokstra.go" - ], - "generated_mod_time": "2025-12-07T03:46:41.7400124+07:00" - } - }, - "updated_at": "2025-12-07T03:46:41.7400124+07:00", - "generated_checksum": "f900aa878c75bf0f7595877fe58eec0a989cd7a7450fd66ebfcd2cec3a15f507" -} \ No newline at end of file diff --git a/docs/00-introduction/examples/annotations/zz_generated.lokstra.go b/docs/00-introduction/examples/annotations/zz_generated.lokstra.go deleted file mode 100644 index 269ae504..00000000 --- a/docs/00-introduction/examples/annotations/zz_generated.lokstra.go +++ /dev/null @@ -1,462 +0,0 @@ -// AUTO-GENERATED CODE - DO NOT EDIT -// Generated by lokstra-annotation from annotations in this folder -// Annotations: @RouterService, @Service, @Inject, @InjectCfgValue, @Route - -package application - -import ( - time "time" - - "github.com/primadi/lokstra/core/deploy" - "github.com/primadi/lokstra/core/proxy" - "github.com/primadi/lokstra/lokstra_registry" -) - -// Auto-register on package import -func init() { - RegisterAdminAPIService() - RegisterAuthService() - RegisterBackgroundJobService() - RegisterCacheManager() - RegisterEmailService() - RegisterNotificationService() - RegisterProductAPIService() - RegisterUserAPIService() -} - -// ============================================================ -// FILE: mixed_services_example.go -// ============================================================ - -// AdminAPIServiceRemote implements AdminAPIServiceInterface with HTTP proxy -// Auto-generated from AdminAPIService interface methods -type AdminAPIServiceRemote struct { - proxyService *proxy.Service -} - -// NewAdminAPIServiceRemote creates a new remote admin-api-service proxy -func NewAdminAPIServiceRemote(proxyService *proxy.Service) *AdminAPIServiceRemote { - return &AdminAPIServiceRemote{ - proxyService: proxyService, - } -} - -// ListJobs via HTTP -// Generated from: @Route "GET /jobs" -func (s *AdminAPIServiceRemote) ListJobs(p *ListJobsRequest) (*JobListResponse, error) { - return proxy.CallWithData[*JobListResponse](s.proxyService, "ListJobs", p) -} - -// RestartJob via HTTP -// Generated from: @Route "POST /jobs/{id}/restart" -func (s *AdminAPIServiceRemote) RestartJob(p *RestartJobRequest) (*JobResponse, error) { - return proxy.CallWithData[*JobResponse](s.proxyService, "RestartJob", p) -} - -func AdminAPIServiceFactory(deps map[string]any, config map[string]any) any { - svc := &AdminAPIService{ - JobService: deps["background-job-service"].(*BackgroundJobService), - EmailService: deps["email-service"].(*EmailService), - AllowJobRestart: config["admin.allow-job-restart"].(bool), - MaxJobsPerPage: config["admin.max-jobs-per-page"].(int), - } - - return svc -} - -// AdminAPIServiceRemoteFactory creates a remote HTTP client for AdminAPIServiceInterface -// Auto-generated from @RouterService annotation -func AdminAPIServiceRemoteFactory(deps, config map[string]any) any { - proxyService, ok := config["remote"].(*proxy.Service) - if !ok { - panic("remote factory requires 'remote' (proxy.Service) in config") - } - return NewAdminAPIServiceRemote(proxyService) -} - -// RegisterAdminAPIService registers the admin-api-service with the registry -// Auto-generated from annotations: -// - @RouterService name="admin-api-service", prefix="/api/admin" -// - @Inject annotations -// - @InjectCfgValue annotations -// - @Route annotations on methods -func RegisterAdminAPIService() { - // Register service type with router configuration - lokstra_registry.RegisterRouterServiceType("admin-api-service-factory", - AdminAPIServiceFactory, - AdminAPIServiceRemoteFactory, - &deploy.ServiceTypeConfig{ - PathPrefix: "/api/admin", - Middlewares: []string{"recovery", "auth", "admin"}, - RouteOverrides: map[string]deploy.RouteConfig{ - "ListJobs": { - Path: "GET /jobs", - }, - "RestartJob": { - Path: "POST /jobs/{id}/restart", - }, - }, - }, - ) - - // Register lazy service with auto-detected dependencies - lokstra_registry.RegisterLazyService("admin-api-service", - "admin-api-service-factory", - map[string]any{ - "depends-on": []string{"background-job-service", "email-service"}, - "admin.allow-job-restart": lokstra_registry.GetConfig("admin.allow-job-restart", true), - "admin.max-jobs-per-page": lokstra_registry.GetConfig("admin.max-jobs-per-page", 50), - }) -} - -// ============================================================ -// FILE: service_example.go -// ============================================================ - -// RegisterAuthService registers the auth-service with the registry -// Auto-generated from annotations: -// - @Service name="auth-service" -// - @Inject annotations -// - @InjectCfgValue annotations -func RegisterAuthService() { - lokstra_registry.RegisterLazyService("auth-service", func(deps map[string]any, cfg map[string]any) any { - svc := &AuthService{ - Cache: deps["cache-service"].(CacheService), - UserRepo: deps["user-repository"].(UserRepository), - DebugMode: cfg["auth.debug-mode"].(bool), - JwtSecret: cfg["auth.jwt-secret"].(string), - MaxAttempts: cfg["auth.max-attempts"].(int), - TokenExpiry: cfg["auth.token-expiry"].(time.Duration), - } - - return svc - }, map[string]any{ - "depends-on": []string{"cache-service", "user-repository"}, - "auth.debug-mode": lokstra_registry.GetConfig("auth.debug-mode", false), - "auth.jwt-secret": lokstra_registry.GetConfig("auth.jwt-secret", ""), - "auth.max-attempts": lokstra_registry.GetConfig("auth.max-attempts", 5), - "auth.token-expiry": lokstra_registry.GetConfig("auth.token-expiry", 24*time.Hour), - }) -} - -// ============================================================ -// FILE: mixed_services_example.go -// ============================================================ - -// RegisterBackgroundJobService registers the background-job-service with the registry -// Auto-generated from annotations: -// - @Service name="background-job-service" -// - @Inject annotations -// - @InjectCfgValue annotations -func RegisterBackgroundJobService() { - lokstra_registry.RegisterLazyService("background-job-service", func(deps map[string]any, cfg map[string]any) any { - svc := &BackgroundJobService{ - EmailService: deps["email-service"].(*EmailService), - MaxWorkers: cfg["jobs.max-workers"].(int), - RetryDelay: cfg["jobs.retry-delay"].(time.Duration), - RetryLimit: cfg["jobs.retry-limit"].(int), - } - - return svc - }, map[string]any{ - "depends-on": []string{"email-service"}, - "jobs.max-workers": lokstra_registry.GetConfig("jobs.max-workers", 10), - "jobs.retry-delay": lokstra_registry.GetConfig("jobs.retry-delay", 5*time.Second), - "jobs.retry-limit": lokstra_registry.GetConfig("jobs.retry-limit", 3), - }) -} - -// ============================================================ -// FILE: init_example.go -// ============================================================ - -// RegisterCacheManager registers the cache-manager with the registry -// Auto-generated from annotations: -// - @Service name="cache-manager" -// - @InjectCfgValue annotations -func RegisterCacheManager() { - lokstra_registry.RegisterLazyService("cache-manager", func(deps map[string]any, cfg map[string]any) any { - svc := &CacheManager{ - MaxSize: cfg["cache.max-size"].(int), - TTLSeconds: cfg["cache.ttl-seconds"].(int), - } - - // Call Init() for post-initialization - if err := svc.Init(); err != nil { - panic("failed to initialize cache-manager: " + err.Error()) - } - - return svc - }, map[string]any{ - "cache.max-size": lokstra_registry.GetConfig("cache.max-size", 1000), - "cache.ttl-seconds": lokstra_registry.GetConfig("cache.ttl-seconds", 300), - }) -} - -// ============================================================ -// FILE: mixed_services_example.go -// ============================================================ - -// RegisterEmailService registers the email-service with the registry -// Auto-generated from annotations: -// - @Service name="email-service" -// - @InjectCfgValue annotations -func RegisterEmailService() { - lokstra_registry.RegisterLazyService("email-service", func(deps map[string]any, cfg map[string]any) any { - svc := &EmailService{ - FromEmail: cfg["email.from"].(string), - SMTPHost: cfg["smtp.host"].(string), - SMTPPassword: cfg["smtp.password"].(string), - SMTPPort: cfg["smtp.port"].(int), - SMTPUsername: cfg["smtp.username"].(string), - } - - return svc - }, map[string]any{ - "email.from": lokstra_registry.GetConfig("email.from", "noreply@example.com"), - "smtp.host": lokstra_registry.GetConfig("smtp.host", ""), - "smtp.password": lokstra_registry.GetConfig("smtp.password", ""), - "smtp.port": lokstra_registry.GetConfig("smtp.port", 587), - "smtp.username": lokstra_registry.GetConfig("smtp.username", ""), - }) -} - -// ============================================================ -// FILE: service_example.go -// ============================================================ - -// RegisterNotificationService registers the notification-service with the registry -// Auto-generated from annotations: -// - @Service name="notification-service" -// - @InjectCfgValue annotations -func RegisterNotificationService() { - lokstra_registry.RegisterLazyService("notification-service", func(deps map[string]any, cfg map[string]any) any { - svc := &NotificationService{ - Enabled: cfg["notification.enabled"].(bool), - FromEmail: cfg["smtp.from-email"].(string), - SMTPHost: cfg["smtp.host"].(string), - SMTPPort: cfg["smtp.port"].(int), - } - - return svc - }, map[string]any{ - "notification.enabled": lokstra_registry.GetConfig("notification.enabled", true), - "smtp.from-email": lokstra_registry.GetConfig("smtp.from-email", "noreply@example.com"), - "smtp.host": lokstra_registry.GetConfig("smtp.host", ""), - "smtp.port": lokstra_registry.GetConfig("smtp.port", 587), - }) -} - -// ============================================================ -// FILE: router_with_init_example.go -// ============================================================ - -// ProductAPIServiceRemote implements ProductAPIServiceInterface with HTTP proxy -// Auto-generated from ProductAPIService interface methods -type ProductAPIServiceRemote struct { - proxyService *proxy.Service -} - -// NewProductAPIServiceRemote creates a new remote product-api proxy -func NewProductAPIServiceRemote(proxyService *proxy.Service) *ProductAPIServiceRemote { - return &ProductAPIServiceRemote{ - proxyService: proxyService, - } -} - -// GetProduct via HTTP -// Generated from: @Route "GET /{id}" -func (s *ProductAPIServiceRemote) GetProduct(p *GetProductRequest) (*ProductResponse, error) { - return proxy.CallWithData[*ProductResponse](s.proxyService, "GetProduct", p) -} - -// ListProducts via HTTP -// Generated from: @Route "GET /" -func (s *ProductAPIServiceRemote) ListProducts(p *ListProductsRequest) (*ProductListResponse, error) { - return proxy.CallWithData[*ProductListResponse](s.proxyService, "ListProducts", p) -} - -func ProductAPIServiceFactory(deps map[string]any, config map[string]any) any { - svc := &ProductAPIService{ - ProductRepo: deps["product-repository"].(ProductRepository), - MaxItems: config["api.products.max-items"].(int), - } - - // Call Init() for post-initialization - if err := svc.Init(); err != nil { - panic("failed to initialize product-api: " + err.Error()) - } - - return svc -} - -// ProductAPIServiceRemoteFactory creates a remote HTTP client for ProductAPIServiceInterface -// Auto-generated from @RouterService annotation -func ProductAPIServiceRemoteFactory(deps, config map[string]any) any { - proxyService, ok := config["remote"].(*proxy.Service) - if !ok { - panic("remote factory requires 'remote' (proxy.Service) in config") - } - return NewProductAPIServiceRemote(proxyService) -} - -// RegisterProductAPIService registers the product-api with the registry -// Auto-generated from annotations: -// - @RouterService name="product-api", prefix="/api/products" -// - @Inject annotations -// - @InjectCfgValue annotations -// - @Route annotations on methods -func RegisterProductAPIService() { - // Register service type with router configuration - lokstra_registry.RegisterRouterServiceType("product-api-factory", - ProductAPIServiceFactory, - ProductAPIServiceRemoteFactory, - &deploy.ServiceTypeConfig{ - PathPrefix: "/api/products", - Middlewares: []string{}, - RouteOverrides: map[string]deploy.RouteConfig{ - "GetProduct": { - Path: "GET /{id}", - }, - "ListProducts": { - Path: "GET /", - }, - }, - }, - ) - - // Register lazy service with auto-detected dependencies - lokstra_registry.RegisterLazyService("product-api", - "product-api-factory", - map[string]any{ - "depends-on": []string{"product-repository"}, - "api.products.max-items": lokstra_registry.GetConfig("api.products.max-items", 100), - }) -} - -// ============================================================ -// FILE: router_service_with_config_example.go -// ============================================================ - -// UserAPIServiceRemote implements UserAPIServiceInterface with HTTP proxy -// Auto-generated from UserAPIService interface methods -type UserAPIServiceRemote struct { - proxyService *proxy.Service -} - -// NewUserAPIServiceRemote creates a new remote user-api-service proxy -func NewUserAPIServiceRemote(proxyService *proxy.Service) *UserAPIServiceRemote { - return &UserAPIServiceRemote{ - proxyService: proxyService, - } -} - -// Create via HTTP -// Generated from: @Route "POST /" -func (s *UserAPIServiceRemote) Create(p *CreateUserRequest) (*UserResponse, error) { - return proxy.CallWithData[*UserResponse](s.proxyService, "Create", p) -} - -// Delete via HTTP -// Generated from: @Route "DELETE /{id}" -func (s *UserAPIServiceRemote) Delete(p *DeleteUserRequest) error { - return proxy.Call(s.proxyService, "Delete", p) -} - -// GetByID via HTTP -// Generated from: @Route "GET /{id}" -func (s *UserAPIServiceRemote) GetByID(p *GetUserRequest) (*UserResponse, error) { - return proxy.CallWithData[*UserResponse](s.proxyService, "GetByID", p) -} - -// List via HTTP -// Generated from: @Route "GET /" -func (s *UserAPIServiceRemote) List(p *ListUsersRequest) (*UserListResponse, error) { - return proxy.CallWithData[*UserListResponse](s.proxyService, "List", p) -} - -// Update via HTTP -// Generated from: @Route "PUT /{id}" -func (s *UserAPIServiceRemote) Update(p *UpdateUserRequest) (*UserResponse, error) { - return proxy.CallWithData[*UserResponse](s.proxyService, "Update", p) -} - -func UserAPIServiceFactory(deps map[string]any, config map[string]any) any { - svc := &UserAPIService{ - Cache: deps["cache-service"].(CacheService), - UserRepo: deps["user-repository"].(UserRepository), - JwtSecret: config["api.jwt-secret"].(string), - DefaultPageSize: config["api.pagination.default-page-size"].(int), - MaxPageSize: config["api.pagination.max-page-size"].(int), - RateLimitEnabled: config["api.rate-limit.enabled"].(bool), - MaxRequests: config["api.rate-limit.max-requests"].(int), - RateLimitWindow: config["api.rate-limit.window"].(time.Duration), - IncludeMetadata: config["api.response.include-metadata"].(bool), - TokenExpiry: config["api.token-expiry"].(time.Duration), - } - - return svc -} - -// UserAPIServiceRemoteFactory creates a remote HTTP client for UserAPIServiceInterface -// Auto-generated from @RouterService annotation -func UserAPIServiceRemoteFactory(deps, config map[string]any) any { - proxyService, ok := config["remote"].(*proxy.Service) - if !ok { - panic("remote factory requires 'remote' (proxy.Service) in config") - } - return NewUserAPIServiceRemote(proxyService) -} - -// RegisterUserAPIService registers the user-api-service with the registry -// Auto-generated from annotations: -// - @RouterService name="user-api-service", prefix="/api/v1/users" -// - @Inject annotations -// - @InjectCfgValue annotations -// - @Route annotations on methods -func RegisterUserAPIService() { - // Register service type with router configuration - lokstra_registry.RegisterRouterServiceType("user-api-service-factory", - UserAPIServiceFactory, - UserAPIServiceRemoteFactory, - &deploy.ServiceTypeConfig{ - PathPrefix: "/api/v1/users", - Middlewares: []string{"recovery", "request-logger"}, - RouteOverrides: map[string]deploy.RouteConfig{ - "Create": { - Path: "POST /", - Middlewares: []string{"auth"}, - }, - "Delete": { - Path: "DELETE /{id}", - Middlewares: []string{"auth", "admin"}, - }, - "GetByID": { - Path: "GET /{id}", - }, - "List": { - Path: "GET /", - }, - "Update": { - Path: "PUT /{id}", - Middlewares: []string{"auth"}, - }, - }, - }, - ) - - // Register lazy service with auto-detected dependencies - lokstra_registry.RegisterLazyService("user-api-service", - "user-api-service-factory", - map[string]any{ - "depends-on": []string{"cache-service", "user-repository"}, - "api.jwt-secret": lokstra_registry.GetConfig("api.jwt-secret", ""), - "api.pagination.default-page-size": lokstra_registry.GetConfig("api.pagination.default-page-size", 20), - "api.pagination.max-page-size": lokstra_registry.GetConfig("api.pagination.max-page-size", 100), - "api.rate-limit.enabled": lokstra_registry.GetConfig("api.rate-limit.enabled", true), - "api.rate-limit.max-requests": lokstra_registry.GetConfig("api.rate-limit.max-requests", 100), - "api.rate-limit.window": lokstra_registry.GetConfig("api.rate-limit.window", 1*time.Minute), - "api.response.include-metadata": lokstra_registry.GetConfig("api.response.include-metadata", true), - "api.token-expiry": lokstra_registry.GetConfig("api.token-expiry", 24*time.Hour), - }) -} diff --git a/docs/00-introduction/examples/full-framework/01_enterprise_router_service/README.md b/docs/00-introduction/examples/full-framework/01_enterprise_router_service/README.md index 3f7aeb8d..2612fc5b 100644 --- a/docs/00-introduction/examples/full-framework/01_enterprise_router_service/README.md +++ b/docs/00-introduction/examples/full-framework/01_enterprise_router_service/README.md @@ -34,7 +34,7 @@ This template demonstrates how to use **Lokstra Annotations** to automatically g **Write this:** ```go -// @RouterService name="user-service", prefix="/api" +// @EndpointService name="user-service", prefix="/api" type UserServiceImpl struct { // @Inject "user-repository" UserRepo *service.Cached[domain.UserRepository] @@ -111,7 +111,7 @@ This template uses **three core annotations**: | Annotation | Purpose | Example | |------------|---------|---------| -| `@RouterService` | Marks a service to be published as HTTP router | `@RouterService name="user-service"` | +| `@EndpointService` | Marks a service to be published as HTTP router | `@EndpointService name="user-service"` | | `@Inject` | Auto-wires dependencies | `@Inject "user-repository"` | | `@Route` | Maps methods to HTTP endpoints | `@Route "GET /users/{id}"` | @@ -121,7 +121,7 @@ This template uses **three core annotations**: modules/{module-name}/ ├── domain/ # Business entities and interfaces ├── application/ # Service implementation with annotations -│ ├── user_service.go # @RouterService, @Inject, @Route +│ ├── user_service.go # @EndpointService, @Inject, @Route │ └── zz_generated.lokstra.go # Auto-generated └── infrastructure/ # Data access implementations ``` @@ -131,7 +131,7 @@ Each layer has specific responsibilities: | Layer | Responsibility | Annotations | |------------------|---------------------------------------|------------------| | **Domain** | Business entities and rules | None | -| **Application** | Service with annotations | `@RouterService`, `@Inject`, `@Route` | +| **Application** | Service with annotations | `@EndpointService`, `@Inject`, `@Route` | | **Infrastructure**| Repository implementations | None | --- @@ -179,7 +179,7 @@ func main() { **user_service.go** - Annotated service: ```go -// @RouterService name="user-service", prefix="/api" +// @EndpointService name="user-service", prefix="/api" type UserServiceImpl struct { // @Inject "user-repository" UserRepo *service.Cached[domain.UserRepository] @@ -231,13 +231,13 @@ func registerServiceTypes() { ## 📝 Lokstra Annotations Reference -### @RouterService +### @EndpointService **Marks a struct as a router service** - generates factory, remote proxy, and router registration. **Syntax:** ```go -// @RouterService name="service-name", prefix="/api", middlewares=["recovery", "logger"] +// @EndpointService name="service-name", prefix="/api", middlewares=["recovery", "logger"] type MyService struct { ... } ``` @@ -248,7 +248,7 @@ type MyService struct { ... } **Example:** ```go -// @RouterService name="user-service", prefix="/api/v1", middlewares=["auth", "logging"] +// @EndpointService name="user-service", prefix="/api/v1", middlewares=["auth", "logging"] type UserServiceImpl struct { ... } ``` @@ -392,7 +392,7 @@ func main() { The annotation processor scans your code for: ```go -// @RouterService name="user-service", prefix="/api" +// @EndpointService name="user-service", prefix="/api" type UserServiceImpl struct { // @Inject "user-repository" UserRepo *service.Cached[domain.UserRepository] @@ -675,7 +675,7 @@ import ( "github.com/primadi/lokstra/.../modules/product/domain" ) -// @RouterService name="product-service", prefix="/api", middlewares=["recovery", "request-logger"] +// @EndpointService name="product-service", prefix="/api", middlewares=["recovery", "request-logger"] type ProductServiceImpl struct { // @Inject "product-repository" ProductRepo *service.Cached[domain.ProductRepository] @@ -839,7 +839,7 @@ GET /api/products/{id} ### Generated Files -Every folder with `@RouterService` annotations gets: +Every folder with `@EndpointService` annotations gets: **zz_generated.lokstra.go** - Generated Go code ```go @@ -1061,7 +1061,7 @@ func init() { **12 lines of business logic:** ```go -// @RouterService name="user-service", prefix="/api" +// @EndpointService name="user-service", prefix="/api" type UserServiceImpl struct { // @Inject "user-repository" UserRepo *service.Cached[domain.UserRepository] @@ -1119,7 +1119,7 @@ func Register() {} // Trigger package load ```go // DECLARATIVE: What you want -// @RouterService name="user-service", prefix="/api" +// @EndpointService name="user-service", prefix="/api" // @Route "GET /users/{id}" // vs. @@ -1168,28 +1168,28 @@ func (s *UserService) SpecialAction(...) { ... } ```go // ✅ Good - clear and concise -// @RouterService name="user-service", prefix="/api" +// @EndpointService name="user-service", prefix="/api" type UserServiceImpl struct { ... } // ❌ Bad - too complex -// @RouterService name="user-service", prefix="/api/v1/internal/services", middlewares=["auth", "rbac", "logging", "metrics", "tracing"] +// @EndpointService name="user-service", prefix="/api/v1/internal/services", middlewares=["auth", "rbac", "logging", "metrics", "tracing"] ``` ### 2. Use Descriptive Service Names ```go // ✅ Good - follows naming convention -// @RouterService name="user-service" +// @EndpointService name="user-service" // ❌ Bad - inconsistent naming -// @RouterService name="usrSvc" +// @EndpointService name="usrSvc" ``` ### 3. Group Related Routes ```go // ✅ Good - consistent prefix -// @RouterService name="user-service", prefix="/api/v1/users" +// @EndpointService name="user-service", prefix="/api/v1/users" // @Route "GET /{id}" func GetByID(...) { ... } @@ -1603,7 +1603,7 @@ modules/order → modules/shared **Problem:** Annotation not being processed **Checklist:** -- ✅ Correct syntax: `// @RouterService` (with space after `//`) +- ✅ Correct syntax: `// @EndpointService` (with space after `//`) - ✅ Correct parameter format: `name="value"` - ✅ File saved before running - ✅ Not in test file (`_test.go`) diff --git a/docs/00-introduction/examples/full-framework/01_enterprise_router_service/modules/order/application/order_service.go b/docs/00-introduction/examples/full-framework/01_enterprise_router_service/modules/order/application/order_service.go index 1750c6d8..8192a646 100644 --- a/docs/00-introduction/examples/full-framework/01_enterprise_router_service/modules/order/application/order_service.go +++ b/docs/00-introduction/examples/full-framework/01_enterprise_router_service/modules/order/application/order_service.go @@ -7,7 +7,7 @@ import ( userDomain "github.com/primadi/lokstra/docs/00-introduction/examples/full-framework/01_enterprise_router_service/modules/user/domain" ) -// @RouterService name="order-service", prefix="/api", middlewares=["recovery", "request-logger"] +// @EndpointService name="order-service", prefix="/api", middlewares=["recovery", "request-logger"] type OrderServiceImpl struct { // @Inject "order-repository" OrderRepo domain.OrderRepository diff --git a/docs/00-introduction/examples/full-framework/01_enterprise_router_service/modules/order/application/zz_generated.lokstra.go b/docs/00-introduction/examples/full-framework/01_enterprise_router_service/modules/order/application/zz_generated.lokstra.go index 6ef5fa84..55094f29 100644 --- a/docs/00-introduction/examples/full-framework/01_enterprise_router_service/modules/order/application/zz_generated.lokstra.go +++ b/docs/00-introduction/examples/full-framework/01_enterprise_router_service/modules/order/application/zz_generated.lokstra.go @@ -1,15 +1,15 @@ // AUTO-GENERATED CODE - DO NOT EDIT // Generated by lokstra-annotation from annotations in this folder -// Annotations: @RouterService, @Inject, @Route +// Annotations: @EndpointService, @Inject, @Route package application import ( "github.com/primadi/lokstra/core/deploy" "github.com/primadi/lokstra/core/proxy" - "github.com/primadi/lokstra/lokstra_registry" domain "github.com/primadi/lokstra/docs/00-introduction/examples/full-framework/01_enterprise_router_service/modules/order/domain" userDomain "github.com/primadi/lokstra/docs/00-introduction/examples/full-framework/01_enterprise_router_service/modules/user/domain" + "github.com/primadi/lokstra/lokstra_registry" ) // Auto-register on package import @@ -70,16 +70,15 @@ func (s *OrderServiceImplRemote) UpdateStatus(p *domain.UpdateOrderStatusRequest return proxy.CallWithData[*domain.Order](s.proxyService, "UpdateStatus", p) } - func OrderServiceImplFactory(deps map[string]any, config map[string]any) any { return &OrderServiceImpl{ - OrderRepo: deps["order-repository"].(domain.OrderRepository), + OrderRepo: deps["order-repository"].(domain.OrderRepository), UserService: deps["user-service"].(userDomain.UserService), } } // OrderServiceImplRemoteFactory creates a remote HTTP client for OrderServiceImplInterface -// Auto-generated from @RouterService annotation +// Auto-generated from @EndpointService annotation func OrderServiceImplRemoteFactory(deps, config map[string]any) any { proxyService, ok := config["remote"].(*proxy.Service) if !ok { @@ -90,7 +89,7 @@ func OrderServiceImplRemoteFactory(deps, config map[string]any) any { // RegisterOrderServiceImpl registers the order-service with the registry // Auto-generated from annotations: -// - @RouterService name="order-service", prefix="/api" +// - @EndpointService name="order-service", prefix="/api" // - @Inject annotations // - @Route annotations on methods func RegisterOrderServiceImpl() { @@ -100,7 +99,7 @@ func RegisterOrderServiceImpl() { OrderServiceImplRemoteFactory, &deploy.ServiceTypeConfig{ PathPrefix: "/api", - Middlewares: []string{ "recovery", "request-logger" }, + Middlewares: []string{"recovery", "request-logger"}, RouteOverrides: map[string]deploy.RouteConfig{ "Cancel": { Path: "POST /orders/{id}/cancel", @@ -128,7 +127,6 @@ func RegisterOrderServiceImpl() { lokstra_registry.RegisterLazyService("order-service", "order-service-factory", map[string]any{ - "depends-on": []string{ "order-repository", "user-service", }, + "depends-on": []string{"order-repository", "user-service"}, }) } - diff --git a/docs/00-introduction/examples/full-framework/01_enterprise_router_service/modules/user/application/user_service.go b/docs/00-introduction/examples/full-framework/01_enterprise_router_service/modules/user/application/user_service.go index 6624986b..4ef33238 100644 --- a/docs/00-introduction/examples/full-framework/01_enterprise_router_service/modules/user/application/user_service.go +++ b/docs/00-introduction/examples/full-framework/01_enterprise_router_service/modules/user/application/user_service.go @@ -4,7 +4,7 @@ import ( "github.com/primadi/lokstra/docs/00-introduction/examples/full-framework/01_enterprise_router_service/modules/user/domain" ) -// @RouterService name="user-service", prefix="/api", middlewares=["recovery", "request-logger"] +// @EndpointService name="user-service", prefix="/api", middlewares=["recovery", "request-logger"] type UserServiceImpl struct { // @Inject "user-repository" UserRepo domain.UserRepository diff --git a/docs/00-introduction/examples/full-framework/01_enterprise_router_service/modules/user/application/zz_generated.lokstra.go b/docs/00-introduction/examples/full-framework/01_enterprise_router_service/modules/user/application/zz_generated.lokstra.go index d0597c71..9168dac9 100644 --- a/docs/00-introduction/examples/full-framework/01_enterprise_router_service/modules/user/application/zz_generated.lokstra.go +++ b/docs/00-introduction/examples/full-framework/01_enterprise_router_service/modules/user/application/zz_generated.lokstra.go @@ -1,14 +1,14 @@ // AUTO-GENERATED CODE - DO NOT EDIT // Generated by lokstra-annotation from annotations in this folder -// Annotations: @RouterService, @Inject, @Route +// Annotations: @EndpointService, @Inject, @Route package application import ( "github.com/primadi/lokstra/core/deploy" "github.com/primadi/lokstra/core/proxy" - "github.com/primadi/lokstra/lokstra_registry" domain "github.com/primadi/lokstra/docs/00-introduction/examples/full-framework/01_enterprise_router_service/modules/user/domain" + "github.com/primadi/lokstra/lokstra_registry" ) // Auto-register on package import @@ -75,7 +75,6 @@ func (s *UserServiceImplRemote) Update(p *domain.UpdateUserRequest) (*domain.Use return proxy.CallWithData[*domain.User](s.proxyService, "Update", p) } - func UserServiceImplFactory(deps map[string]any, config map[string]any) any { return &UserServiceImpl{ UserRepo: deps["user-repository"].(domain.UserRepository), @@ -83,7 +82,7 @@ func UserServiceImplFactory(deps map[string]any, config map[string]any) any { } // UserServiceImplRemoteFactory creates a remote HTTP client for UserServiceImplInterface -// Auto-generated from @RouterService annotation +// Auto-generated from @EndpointService annotation func UserServiceImplRemoteFactory(deps, config map[string]any) any { proxyService, ok := config["remote"].(*proxy.Service) if !ok { @@ -94,7 +93,7 @@ func UserServiceImplRemoteFactory(deps, config map[string]any) any { // RegisterUserServiceImpl registers the user-service with the registry // Auto-generated from annotations: -// - @RouterService name="user-service", prefix="/api" +// - @EndpointService name="user-service", prefix="/api" // - @Inject annotations // - @Route annotations on methods func RegisterUserServiceImpl() { @@ -104,7 +103,7 @@ func RegisterUserServiceImpl() { UserServiceImplRemoteFactory, &deploy.ServiceTypeConfig{ PathPrefix: "/api", - Middlewares: []string{ "recovery", "request-logger" }, + Middlewares: []string{"recovery", "request-logger"}, RouteOverrides: map[string]deploy.RouteConfig{ "Activate": { Path: "POST /users/{id}/activate", @@ -135,7 +134,6 @@ func RegisterUserServiceImpl() { lokstra_registry.RegisterLazyService("user-service", "user-service-factory", map[string]any{ - "depends-on": []string{ "user-repository", }, + "depends-on": []string{"user-repository"}, }) } - diff --git a/docs/00-introduction/examples/full-framework/index.md b/docs/00-introduction/examples/full-framework/index.md index e1e16016..28f8387a 100644 --- a/docs/00-introduction/examples/full-framework/index.md +++ b/docs/00-introduction/examples/full-framework/index.md @@ -14,7 +14,7 @@ title: Full Framework Examples This track covers the **complete Lokstra framework** with dependency injection, auto-generated routers, and deployment patterns: -- ✅ Annotation-based service development (`@RouterService`, `@Inject`, `@Route`) +- ✅ Annotation-based service development (`@EndpointService`, `@Inject`, `@Route`) - ✅ Auto-generated REST routers from service methods - ✅ Type-safe dependency injection (eager loading) - ✅ Configuration-driven deployment (YAML or Code) @@ -34,7 +34,7 @@ This track covers the **complete Lokstra framework** with dependency injection, ```go // Write this: -// @RouterService name="user-service", prefix="/api" +// @EndpointService name="user-service", prefix="/api" type UserServiceImpl struct { // @Inject "user-repository" UserRepo domain.UserRepository @@ -53,7 +53,7 @@ func (s *UserServiceImpl) GetByID(p *GetUserRequest) (*User, error) { ``` **What you'll learn:** -- **Lokstra Annotations** - `@RouterService`, `@Inject`, `@Route` +- **Lokstra Annotations** - `@EndpointService`, `@Inject`, `@Route` - Auto-code generation with `lokstra.Bootstrap()` - Zero boilerplate router services - Hot reload in dev mode (auto-regenerates on changes) @@ -108,7 +108,7 @@ go run . -server=microservices.user-server **What you'll learn:** - YAML-based configuration -- Annotation-driven development with @RouterService +- Annotation-driven development with @EndpointService - Service interfaces (local vs remote) - Proxy pattern for remote calls - Monolith vs microservices topology @@ -263,7 +263,7 @@ Example 05: Quick Integration → proxy.Router, simple HTTP calls Example 07: Annotation-Driven (RECOMMENDED) - → @RouterService, @Inject, @Route, auto-generation, zero boilerplate + → @EndpointService, @Inject, @Route, auto-generation, zero boilerplate ``` --- @@ -313,7 +313,7 @@ Example 07: Annotation-Driven (RECOMMENDED) **Lokstra advantages:** - ✅ Type-safe generics (no `any`) -- ✅ Annotation-driven routes with @RouterService +- ✅ Annotation-driven routes with @EndpointService - ✅ Zero-code deployment topology changes - ✅ Code or YAML configuration (your choice) - ✅ **Annotation-driven development (Example 01)** - Like NestJS decorators, but with Go code generation @@ -331,7 +331,7 @@ Example 07: Annotation-Driven (RECOMMENDED) **STRONGLY RECOMMENDED for all developers** - annotation-driven, minimal boilerplate ```go -// @RouterService, @Inject, @Route - that's it! +// @EndpointService, @Inject, @Route - that's it! // Everything auto-generated with lokstra.Bootstrap() ``` @@ -347,4 +347,4 @@ External APIs, payment gateways, third-party services **Ready to start?** → [01 - Enterprise Router Service (Annotations)](./01_enterprise_router_service/) ⭐⭐⭐ **START HERE** -**Coming from Router Track?** Full Framework eliminates manual routing with `@RouterService` annotations! +**Coming from Router Track?** Full Framework eliminates manual routing with `@EndpointService` annotations! diff --git a/docs/00-introduction/examples/index.md b/docs/00-introduction/examples/index.md index 2c686d95..9de41c88 100644 --- a/docs/00-introduction/examples/index.md +++ b/docs/00-introduction/examples/index.md @@ -43,7 +43,7 @@ Learn services, dependency injection, auto-routers, annotations, and deployment **What you'll learn:** - ✅ Service layer and dependency injection -- ✅ **Annotation-driven development** (`@RouterService`, `@Inject`, `@Route`) +- ✅ **Annotation-driven development** (`@EndpointService`, `@Inject`, `@Route`) - ✅ Auto-generated REST routers from service methods - ✅ Configuration-driven deployment (YAML or Code) - ✅ Monolith → Microservices migrations @@ -67,7 +67,7 @@ Learn services, dependency injection, auto-routers, annotations, and deployment | **Middleware** | ✅ Global, per-route | ✅ Plus registry-based | | **Services** | ❌ Not covered | ✅ Core pattern | | **Dependency Injection** | ❌ Not needed | ✅ Lazy, type-safe | -| **Annotations** | ❌ Not covered | ✅ `@RouterService`, `@Inject`, `@Route` | +| **Annotations** | ❌ Not covered | ✅ `@EndpointService`, `@Inject`, `@Route` | | **Auto-Router** | ❌ Manual only | ✅ From services | | **Configuration** | ❌ Code only | ✅ YAML or Code | | **Microservices** | ❌ Not covered | ✅ Multi-deployment | diff --git a/docs/00-introduction/index.md b/docs/00-introduction/index.md index ecf1d6c7..0fa6b917 100644 --- a/docs/00-introduction/index.md +++ b/docs/00-introduction/index.md @@ -48,7 +48,7 @@ Use Lokstra as a complete framework for: **With Lokstra Annotations (Recommended) - Zero boilerplate!** ```go -// @RouterService name="user-service", prefix="/api" +// @EndpointService name="user-service", prefix="/api" type UserServiceImpl struct { // @Inject "user-repository" UserRepo domain.UserRepository diff --git a/docs/00-introduction/key-features.md b/docs/00-introduction/key-features.md index d978fa3f..66db239a 100644 --- a/docs/00-introduction/key-features.md +++ b/docs/00-introduction/key-features.md @@ -647,7 +647,7 @@ lokstra_registry.RegisterRouter("user-router", setupUserRouter()) **Annotations replace 70+ lines with 12 lines** - like NestJS decorators: ```go -// @RouterService name="user-service", prefix="/api" +// @EndpointService name="user-service", prefix="/api" type UserServiceImpl struct { // @Inject "database" DB *service.Cached[*Database] @@ -670,7 +670,7 @@ func (s *UserServiceImpl) GetByID(p *GetByIDRequest) (*User, error) { **Step 1: Add Annotations** ```go -// @RouterService name="user-service", prefix="/api", mount="/api" +// @EndpointService name="user-service", prefix="/api", mount="/api" type UserServiceImpl struct { // @Inject "database" DB *service.Cached[*Database] @@ -711,9 +711,9 @@ type UserServiceRemote struct { ... } ### Three Powerful Annotations -#### 1. @RouterService - Define Service +#### 1. @EndpointService - Define Service ```go -// @RouterService name="user-service", prefix="/api", mount="/api" +// @EndpointService name="user-service", prefix="/api", mount="/api" type UserServiceImpl struct {} ``` diff --git a/docs/00-introduction/why-lokstra.md b/docs/00-introduction/why-lokstra.md index cb65a806..c2d61c48 100644 --- a/docs/00-introduction/why-lokstra.md +++ b/docs/00-introduction/why-lokstra.md @@ -303,7 +303,7 @@ lokstra_registry.RegisterRouter("user-router", setupUserRouter()) #### The Lokstra Annotation Way (12 Lines!) ```go -// @RouterService name="user-service", prefix="/api" +// @EndpointService name="user-service", prefix="/api" type UserServiceImpl struct { // @Inject "database" DB *Database @@ -361,9 +361,9 @@ type UserServiceRemote struct { ... } **Three Powerful Annotations**: -1. **@RouterService** - Define service + router +1. **@EndpointService** - Define service + router ```go - // @RouterService name="user-service", prefix="/api", mount="/api" + // @EndpointService name="user-service", prefix="/api", mount="/api" type UserServiceImpl struct {} ``` diff --git a/docs/02-framework-guide/02-service/index.md b/docs/02-framework-guide/02-service/index.md index fd458d02..6383f065 100644 --- a/docs/02-framework-guide/02-service/index.md +++ b/docs/02-framework-guide/02-service/index.md @@ -35,7 +35,7 @@ Service-to-service communication: ### 3. Annotation-Driven Development Build services with annotations: -- `@RouterService` for service definition +- `@EndpointService` for service definition - `@Route` for explicit route mapping - `@Inject` for dependency injection - Variable support in routes and prefixes diff --git a/docs/02-framework-guide/04-config/examples/01-basic-config/README.md b/docs/02-framework-guide/04-config/examples/01-basic-config/README.md index ab8b15f5..e8dfeb83 100644 --- a/docs/02-framework-guide/04-config/examples/01-basic-config/README.md +++ b/docs/02-framework-guide/04-config/examples/01-basic-config/README.md @@ -5,7 +5,7 @@ Simple single-file YAML configuration demonstrating core concepts. ## What's Demonstrated - ✅ Single-file YAML configuration -- ✅ Service registration with @RouterService annotation +- ✅ Service registration with @EndpointService annotation - ✅ Dependency injection (service → repository) - ✅ Router auto-generation from published services - ✅ Middleware registration and usage @@ -17,7 +17,7 @@ Simple single-file YAML configuration demonstrating core concepts. ``` 01-basic-config/ ├── main.go # Application entry point + registration -├── user_service.go # Service with @RouterService annotation +├── user_service.go # Service with @EndpointService annotation ├── user_repository.go # Repository implementation ├── middleware.go # Custom middleware ├── config.yaml # YAML configuration @@ -29,11 +29,11 @@ Simple single-file YAML configuration demonstrating core concepts. ## Code Walkthrough -### 1. Service with @RouterService Annotation +### 1. Service with @EndpointService Annotation **user_service.go:** ```go -// @RouterService name="user-service", prefix="/api/users", middlewares=["recovery", "request-logger"] +// @EndpointService name="user-service", prefix="/api/users", middlewares=["recovery", "request-logger"] type UserService struct { // @Inject "user-repository" UserRepo UserRepository @@ -56,7 +56,7 @@ func (s *UserService) Create(p *CreateUserRequest) (*User, error) { ``` **Annotations explained:** -- `@RouterService` - Marks this as a service with auto-generated router +- `@EndpointService` - Marks this as a service with auto-generated router - `name` - Service name for registration - `prefix` - URL prefix for all routes - `middlewares` - Middleware applied to all routes @@ -116,7 +116,7 @@ deployments: ``` **Minimal config! Everything else comes from:** -- `@RouterService` annotation (service metadata) +- `@EndpointService` annotation (service metadata) - Service type registration in code - Convention over configuration @@ -124,7 +124,7 @@ deployments: ### Bootstrap Flow -1. **`lokstra.Bootstrap()`** scans for `@RouterService` annotations +1. **`lokstra.Bootstrap()`** scans for `@EndpointService` annotations 2. Generates `zz_generated.lokstra.go` with service metadata 3. Auto-registers service type: `user-service` → `UserServiceFactory` @@ -199,7 +199,7 @@ All routes have these middleware applied: ### 1. Convention Over Configuration Minimal YAML config because: - Routes defined by `@Route` annotations -- Service metadata from `@RouterService` +- Service metadata from `@EndpointService` - Dependencies from `@Inject` ### 2. Auto-Generation diff --git a/docs/02-framework-guide/04-config/examples/01-basic-config/main.go b/docs/02-framework-guide/04-config/examples/01-basic-config/main.go index fb6013fa..d93f84ce 100644 --- a/docs/02-framework-guide/04-config/examples/01-basic-config/main.go +++ b/docs/02-framework-guide/04-config/examples/01-basic-config/main.go @@ -10,7 +10,7 @@ import ( ) func main() { - // Auto-generate code from @RouterService annotations + // Auto-generate code from @EndpointService annotations lokstra_init.Bootstrap() // STEP 1: Load Config diff --git a/docs/02-framework-guide/04-config/examples/01-basic-config/user_service.go b/docs/02-framework-guide/04-config/examples/01-basic-config/user_service.go index 036ad8f5..b01e170a 100644 --- a/docs/02-framework-guide/04-config/examples/01-basic-config/user_service.go +++ b/docs/02-framework-guide/04-config/examples/01-basic-config/user_service.go @@ -12,7 +12,7 @@ type CreateUserRequest struct { Email string `json:"email" validate:"required,email"` } -// @RouterService name="user-service", prefix="/api/users", middlewares=["recovery", "request-logger"] +// @EndpointService name="user-service", prefix="/api/users", middlewares=["recovery", "request-logger"] type UserService struct { // @Inject "user-repository" UserRepo UserRepository diff --git a/docs/02-framework-guide/04-config/examples/06-handlers/user_service.go b/docs/02-framework-guide/04-config/examples/06-handlers/user_service.go index 49ed53e3..e6a9411b 100644 --- a/docs/02-framework-guide/04-config/examples/06-handlers/user_service.go +++ b/docs/02-framework-guide/04-config/examples/06-handlers/user_service.go @@ -6,7 +6,7 @@ type GetUserRequest struct { type ListUsersRequest struct{} -// @RouterService name="user-service", prefix="/api/users" +// @EndpointService name="user-service", prefix="/api/users" type UserService struct { // @Inject "user-repository" UserRepo UserRepository diff --git a/docs/02-framework-guide/04-config/examples/README.md b/docs/02-framework-guide/04-config/examples/README.md index efda3815..0fd3ddcb 100644 --- a/docs/02-framework-guide/04-config/examples/README.md +++ b/docs/02-framework-guide/04-config/examples/README.md @@ -90,7 +90,7 @@ deployments # Deployment topologies ### Auto-Generation Flow ``` -1. @RouterService annotation → Service metadata +1. @EndpointService annotation → Service metadata 2. published-services → Auto-generate router 3. service.router → Router customization 4. router-definitions → Override defaults @@ -112,7 +112,7 @@ service-definitions: user-service: type: user-service-factory depends-on: [user-repository] - router: {} # Uses conventions from @RouterService + router: {} # Uses conventions from @EndpointService deployments: dev: diff --git a/docs/02-framework-guide/04-config/index.md b/docs/02-framework-guide/04-config/index.md index 993bb7a1..dd08f001 100644 --- a/docs/02-framework-guide/04-config/index.md +++ b/docs/02-framework-guide/04-config/index.md @@ -163,7 +163,7 @@ deployments: ## 🔧 Key Features ### 1. Service Auto-Discovery -Services with `@RouterService` annotation are automatically discovered and registered. +Services with `@EndpointService` annotation are automatically discovered and registered. ### 2. Router Auto-Generation Routers are auto-generated from published services based on metadata. diff --git a/docs/02-framework-guide/06-service-annotation.md b/docs/02-framework-guide/06-service-annotation.md index 330f40cc..6782ff8f 100644 --- a/docs/02-framework-guide/06-service-annotation.md +++ b/docs/02-framework-guide/06-service-annotation.md @@ -18,7 +18,7 @@ The `@Service` annotation is used to register **pure service classes** (non-HTTP - Any service that doesn't expose HTTP endpoints **Key Differences:** -- `@RouterService` → HTTP handlers (controllers) with routes +- `@EndpointService` → HTTP handlers (controllers) with routes - `@Service` → Pure services without HTTP endpoints ## Basic Syntax @@ -91,16 +91,16 @@ func RegisterAuthService() { } ``` -### 3. Configuration Injection with @InjectCfgValue +### 3. Configuration Injection with @Inject "cfg:..." **Basic config:** ```go // @Service name="auth-service" type AuthService struct { - // @InjectCfgValue "auth.jwt-secret" + // @Inject "cfg:auth.jwt-secret" JwtSecret string - // @InjectCfgValue key="auth.token-expiry" + // @Inject "cfg:auth.token-expiry" TokenExpiry time.Duration } ``` @@ -109,13 +109,13 @@ type AuthService struct { ```go // @Service name="email-service" type EmailService struct { - // @InjectCfgValue key="smtp.host", default="localhost" + // @Inject "cfg:smtp.host", "localhost" SMTPHost string - // @InjectCfgValue key="smtp.port", default="587" + // @Inject "cfg:smtp.port", "587" SMTPPort int - // @InjectCfgValue key="smtp.enabled", default="true" + // @Inject "cfg:smtp.enabled", "true" Enabled bool } ``` @@ -163,17 +163,17 @@ type AuthService struct { Cache domain.CacheService // Required config (no default) - // @InjectCfgValue "auth.jwt-secret" + // @Inject "cfg:auth.jwt-secret" JwtSecret string // Config with defaults - // @InjectCfgValue key="auth.token-expiry", default="24h" + // @Inject "cfg:auth.token-expiry", "24h" TokenExpiry time.Duration - // @InjectCfgValue key="auth.max-attempts", default="5" + // @Inject "cfg:auth.max-attempts", "5" MaxAttempts int - // @InjectCfgValue key="auth.debug-mode", default="false" + // @Inject "cfg:auth.debug-mode", "false" DebugMode bool } @@ -248,7 +248,6 @@ func init() { // Auto-generated from annotations: // - @Service name="auth-service" // - @Inject annotations -// - @InjectCfgValue annotations func RegisterAuthService() { lokstra_registry.RegisterLazyService("auth-service", func(deps map[string]any, cfg map[string]any) any { return &AuthService{ @@ -311,7 +310,7 @@ type PaymentProcessor struct { ❌ **Bad:** ```go // Don't use @Service for HTTP controllers -// Use @RouterService instead +// Use @EndpointService instead ``` ### 2. Separate Configuration Concerns @@ -320,10 +319,10 @@ type PaymentProcessor struct { ```go // @Service name="sms-service" type SMSService struct { - // @InjectCfgValue key="sms.api-key" + // @Inject "cfg:sms.api-key" APIKey string - // @InjectCfgValue key="sms.endpoint", default="https://api.sms.com" + // @Inject "cfg:sms.endpoint", "https://api.sms.com" Endpoint string } ``` @@ -372,24 +371,24 @@ func (s *UserService) GetUser(id string) (*User, error) { ```go // @Service name="config-service" type ConfigService struct { - // @InjectCfgValue key="server.port", default="8080" + // @Inject "cfg:server.port", "8080" Port int // Auto-uses GetConfigInt - // @InjectCfgValue key="cache.ttl", default="5m" + // @Inject "cfg:cache.ttl", "5m" CacheTTL time.Duration // Auto-uses GetConfigDuration - // @InjectCfgValue key="debug", default="false" + // @Inject "cfg:debug", "false" Debug bool // Auto-uses GetConfigBool } ``` -## Comparison: @Service vs @RouterService +## Comparison: @Service vs @EndpointService -| Feature | @Service | @RouterService | +| Feature | @Service | @EndpointService | |---------|----------|----------------| | HTTP Routes | ❌ No | ✅ Yes (@Route) | | Dependency Injection | ✅ @Inject | ✅ @Inject | -| Config Injection | ✅ @InjectCfgValue | ✅ @InjectCfgValue | +| Config Injection | ✅ @Inject "cfg:..." | ✅ @Inject "cfg:..." | | Optional Dependencies | ✅ Yes | ✅ Yes | | Use Case | Business logic, utilities | HTTP controllers | | Generated Code | `RegisterLazyService` | `RegisterRouterServiceType` | @@ -419,7 +418,7 @@ go run . --generate-only ## See Also -- [@RouterService](05-router-service-annotation.md) - For HTTP endpoints +- [@EndpointService](05-router-service-annotation.md) - For HTTP endpoints - [@Inject](07-inject-annotation.md) - Dependency injection details -- [@InjectCfgValue](08-inject-cfg-annotation.md) - Configuration injection +- [@Inject "cfg:..."](08-inject-cfg-annotation.md) - Configuration injection - [Service Registry](09-service-registry.md) - Manual service registration diff --git a/docs/02-framework-guide/07-inject-annotation.md b/docs/02-framework-guide/07-inject-annotation.md index a82d474d..8efbbb73 100644 --- a/docs/02-framework-guide/07-inject-annotation.md +++ b/docs/02-framework-guide/07-inject-annotation.md @@ -9,7 +9,7 @@ nav_order: 7 ## Overview -The `@Inject` annotation marks struct fields for automatic dependency injection. It works with both `@Service` and `@RouterService` annotations. +The `@Inject` annotation marks struct fields for automatic dependency injection. It works with both `@Service` and `@EndpointService` annotations. ## Basic Syntax @@ -250,10 +250,10 @@ func (s *PaymentService) ProcessPayment(amount float64, userID string) error { } ``` -## With @RouterService +## With @EndpointService ```go -// @RouterService name="user-service", prefix="/api/users" +// @EndpointService name="user-service", prefix="/api/users" type UserServiceImpl struct { // @Inject "user-repository" UserRepo domain.UserRepository @@ -405,6 +405,6 @@ type B struct { ## See Also - [@Service](06-service-annotation.md) - Service registration -- [@RouterService](05-router-service-annotation.md) - HTTP services -- [@InjectCfgValue](08-inject-cfg-annotation.md) - Configuration injection +- [@EndpointService](05-router-service-annotation.md) - HTTP services +- [@Inject "cfg:..."](08-inject-cfg-annotation.md) - Configuration injection - [Service Registry](09-service-registry.md) - Manual service registration diff --git a/docs/02-framework-guide/08-database-pools.md b/docs/02-framework-guide/08-database-pools.md deleted file mode 100644 index c4c75dfd..00000000 --- a/docs/02-framework-guide/08-database-pools.md +++ /dev/null @@ -1,654 +0,0 @@ ---- -title: Database Pools -layout: default -parent: Framework Guide -nav_order: 8 ---- - -# Database Pools - -Lokstra provides built-in support for database connection pooling with automatic configuration from YAML files. Understanding the hierarchy of database abstractions helps you use them effectively. - -## Database Components Hierarchy - -Lokstra's database system has four main components: - -``` -DbPoolManager (service) - ↓ manages -DbPool (connection pool, injectable service) - ↓ provides -DbConn (individual connection) - ↓ can create -DbTx (transaction) -``` - -### 1. DbPoolManager - -**Service** that manages multiple named database pools. - -- Manages pool configurations (DSN, schema, etc.) -- Creates and caches pool instances -- Provides access to pools by name -- Supports local map or distributed sync storage - -### 2. DbPool - -**Interface** representing a connection pool. Can be injected into services. - -```go -type DbPool interface { - Acquire(ctx context.Context) (DbConn, error) - DbConn // Can also execute queries directly -} -``` - -- Provides connections from the pool -- Can be injected as `@Inject "pool-name"` -- Shared across services using the same pool name - -### 3. DbConn - -**Interface** representing an individual database connection. - -```go -type DbConn interface { - Begin(ctx context.Context) (DbTx, error) - Transaction(ctx context.Context, fn func(tx DbExecutor) error) error - Release() error - DbExecutor // Can execute queries -} -``` - -- Represents a single connection from the pool -- Must be released when done -- Can create transactions - -### 4. DbTx (Transaction) - -**Interface** representing a database transaction. - -```go -type DbTx interface { - Commit(ctx context.Context) error - Rollback(ctx context.Context) error - DbExecutor // Can execute queries -} -``` - -- Represents an ongoing transaction -- Created from DbConn or via context (recommended) -- Must be committed or rolled back - -## Transaction via Context (Recommended) - -The recommended way to handle transactions is using `ctx.BeginTransaction(poolName)` from `request.Context`. Transactions are automatically finalized (commit/rollback) when the response is written. - -## Setup Database Pools - -### 1. Define DB Pools in Config - -Lokstra has a special `dbpool-definitions:` section in YAML config for defining named database pools: - -**config.yaml:** -```yaml -# Named database pools configuration -dbpool-definitions: - main-db: - dsn: "postgres://user:pass@localhost:5432/mydb?sslmode=disable" - min_conns: 2 - max_conns: 10 - max_idle_time: "30m" - max_lifetime: "1h" - schema: "public" - - analytics-db: - host: localhost - port: 5432 - database: analytics - username: analytics_user - password: secret - sslmode: disable - min_conns: 2 - max_conns: 20 - schema: "analytics" -``` - -This section is automatically loaded and pools are registered as services that can be injected. - -### 2. Recommended: Use lokstra_init - -**The recommended way** is to use `lokstra_init.BootstrapAndRun()` which handles everything in the correct order: - -```go -package main - -import "github.com/primadi/lokstra/lokstra_init" - -func main() { - // Handles all initialization including dbpool-definitions setup - if err := lokstra_init.BootstrapAndRun(); err != nil { - log.Fatal(err) - } -} -``` - -With options for sync mode: -```go -err := lokstra_init.BootstrapAndRun( - lokstra_init.WithDbPoolManager(true, true), // enable, useSync - lokstra_init.WithPgSyncMap(true, "db_main"), -) -``` - -**See [Lokstra Initialization](./09-lokstra-init.md) for details.** - -### 3. Manual Setup (Advanced) - -If you need more control, you can set up manually (not recommended unless you understand the initialization order): - -```go -func main() { - lokstra.Bootstrap() - - // 1. Load config - if err := lokstra_registry.LoadConfig("config.yaml"); err != nil { - log.Fatal(err) - } - - // 2. Setup sync-config first (if using sync mode) - sync_config_pg.Register("db_main", 5*time.Minute, 5*time.Second) - - // 3. Setup definitions - lokstra_init.UsePgxDbPoolManager(true) // true = sync mode - - // 4. Load pools from config - if err := loader.LoadDbPoolManagerFromConfig(); err != nil { - log.Fatal(err) - } - - // 5. Run server - lokstra_registry.InitAndRunServer() -} -``` - -## Inject DB Pool into Service - -### Using @Inject Annotation - -```go -// @Service "user-repository" -type UserRepository struct { - // @Inject "main-db" - DB serviceapi.DbPool -} - -func (r *UserRepository) GetUser(id string) (*User, error) { - var user User - err := r.DB.QueryRow(context.Background(), - "SELECT id, name, email FROM users WHERE id = $1", id, - ).Scan(&user.ID, &user.Name, &user.Email) - return &user, err -} -``` - -### Using Manual Injection - -```go -func UserRepositoryFactory(deps map[string]any, config map[string]any) any { - return &UserRepository{ - DB: deps["main-db"].(serviceapi.DbPool), - } -} - -// In register.go -lokstra_registry.RegisterServiceType("user-repository-factory", - UserRepositoryFactory, nil) -``` - -**config.yaml:** -```yaml -service-definitions: - user-repository: - type: user-repository-factory - depends-on: - - DB:main-db # Inject DB pool named "main-db" -``` - -## DSN Configuration - -### Option 1: Direct DSN - -```yaml -dbpool-definitions: - mydb: - dsn: "postgres://user:pass@localhost:5432/mydb?sslmode=disable" -``` - -### Option 2: Component-Based (Recommended) - -```yaml -dbpool-definitions: - mydb: - host: ${DB_HOST:localhost} - port: ${DB_PORT:5432} - database: ${DB_NAME:mydb} - username: ${DB_USER:user} - password: ${DB_PASS:secret} - sslmode: ${DB_SSLMODE:disable} -``` - -## Pool Parameters - -| Parameter | Default | Description | -|-----------|---------|-------------| -| `min_conns` | 2 | Minimum connections in pool | -| `max_conns` | 10 | Maximum connections in pool | -| `max_idle_time` | 30m | Max time a connection can be idle | -| `max_lifetime` | 1h | Max lifetime of a connection | -| `schema` | public | Default PostgreSQL schema | - -## Best Practices - -### 1. Separate Config from Code - -✅ **Good:** -```go -// Load config first, setup DB later -lokstra_registry.LoadConfig("config.yaml") -lokstra.SetupDbPoolManager() -``` - -❌ **Bad:** -```go -// Auto-setup couples config loading with infrastructure -lokstra_registry.RunServerFromConfig("config.yaml") -``` - -### 2. Use Named Pools for Different Purposes - -```yaml -dbpool-definitions: - transactional-db: # For OLTP workloads - max_conns: 10 - - analytics-db: # For OLAP workloads - max_conns: 50 - - cache-db: # For caching - max_conns: 5 -``` - -### 3. Environment-Specific Configuration - -```yaml -dbpool-definitions: - main-db: - host: ${DB_HOST:localhost} - port: ${DB_PORT:5432} - database: ${DB_NAME} # Required in production - username: ${DB_USER} # Required in production - password: ${DB_PASS} # Required in production - sslmode: ${DB_SSLMODE:require} -``` - -**Development:** -```bash -export DB_NAME=myapp_dev -export DB_USER=dev_user -export DB_PASS=dev_pass -export DB_SSLMODE=disable -``` - -**Production:** -```bash -export DB_NAME=myapp_prod -export DB_USER=prod_user -export DB_PASS=secure_password -export DB_SSLMODE=require -``` - -## Testing Without DB - -```go -func TestUserService(t *testing.T) { - // Load config without setting up DB pools - lokstra_registry.LoadConfig("config.yaml") - - // Mock DB pool - mockDB := &MockDbPool{} - lokstra_registry.RegisterService("main-db", mockDB) - - // Test service - service := lokstra_registry.GetService[*UserService]("user-service") - // ... -} -``` - -## Multiple Databases - -```go -// @Service "reporting-service" -type ReportingService struct { - // @Inject "transactional-db" - TransactionalDB serviceapi.DbPool - - // @Inject "analytics-db" - AnalyticsDB serviceapi.DbPool -} - -func (s *ReportingService) GenerateReport() (*Report, error) { - // Read from transactional DB - users, _ := s.TransactionalDB.Query(...) - - // Read from analytics DB - metrics, _ := s.AnalyticsDB.Query(...) - - return &Report{Users: users, Metrics: metrics}, nil -} -``` - -## DbPool Manager Modes - -Lokstra supports two types of `dbpool-manager` implementations: - -### 1. Local Map Mode (Default) - -Uses in-memory map to store pool configurations. Suitable for single-instance applications. - -```yaml -# No special config needed - this is the default -dbpool-definitions: - main-db: - dsn: "postgres://localhost/mydb" -``` - -**Characteristics:** -- ✅ Fast access (in-memory map) -- ✅ Simple configuration -- ❌ Pool configs not shared across instances -- ❌ Changes lost on restart - -### 2. Distributed Sync Mode - -Uses PostgreSQL-based SyncMap to share pool configurations across multiple instances. Requires `sync_config_pg` service. - -```yaml -# Configure in configs section -configs: - dbpool-definitions: - use_sync: true - -# Then define pools as usual -dbpool-definitions: - main-db: - dsn: "postgres://localhost/mydb" - schema: "public" -``` - -**Characteristics:** -- ✅ Pool configs shared across all instances -- ✅ Changes persist and sync in real-time -- ✅ Suitable for multi-instance deployments -- ⚠️ Requires `sync_config_pg` service to be registered - -**When to use:** -- Multiple application instances running -- Need dynamic pool management across instances -- Want pool configurations to persist and sync - -**Setup for Sync Mode:** - -**Recommended:** Use `lokstra_init`: - -```go -import "github.com/primadi/lokstra/lokstra_init" - -func main() { - err := lokstra_init.BootstrapAndRun( - lokstra_init.WithDbPoolManager(true, true), // enable, useSync=true - lokstra_init.WithPgSyncMap(true, "db_main"), - ) - if err != nil { - log.Fatal(err) - } -} -``` - -**Manual setup** (advanced, not recommended): -```go -// 1. Register sync-config first (required for sync mode) -sync_config_pg.Register("db_main", 5*time.Minute, 5*time.Second) - -// 2. Load config (contains use_sync: true) -lokstra_registry.LoadConfig("config.yaml") - -// 3. Setup dbpool-manager with sync mode -lokstra_init.UsePgxDbPoolManager(true) - -// 4. Load pools from config -loader.LoadDbPoolManagerFromConfig() - -// 5. Run server -lokstra_registry.InitAndRunServer() -``` - -**See [Lokstra Initialization](./09-lokstra-init.md) for recommended approach.** - -## Transaction Management - -**Recommended approach:** Use `ctx.BeginTransaction(poolName)` from `request.Context`. This creates a transaction for the specified pool name lazily (only when first database operation occurs) and automatically commits/rolls back based on error state. - -### Basic Transaction Usage - -```go -func (s *UserService) CreateUser(ctx *request.Context, user *User) error { - // Begin transaction for pool named "main-db" - // Transaction will be created automatically on first DB operation - // Will auto-commit on success or rollback on error in FinalizeResponse - ctx.BeginTransaction("main-db") - - // All database operations using this ctx will join the same transaction - if err := s.userRepo.Create(ctx, user); err != nil { - return err // Auto rollback on error - } - - if err := s.auditRepo.Log(ctx, "user_created", user.ID); err != nil { - return err // Auto rollback on error - } - - return nil // Auto commit on success -} -``` - -**Example from real code:** - -```go -// @Route "POST /" -func (s *TenantService) CreateTenant(ctx *request.Context, - req *domain.CreateTenantRequest) (*domain.Tenant, error) { - - // Begin transaction for pool "db_auth" - // All subsequent DB operations using ctx will join this transaction - ctx.BeginTransaction("db_auth") - - // These operations will automatically join the transaction - existing, err := s.TenantStore.GetByName(ctx, req.Name) - if err == nil && existing != nil { - return nil, fmt.Errorf("tenant already exists") - } - - // Create tenant (also joins transaction) - tenant, err := s.TenantStore.Create(ctx, ...) - - return tenant, err // Auto commit if nil, rollback if error -} -``` - -### How It Works - -1. **Marking**: `BeginTransaction("pool-name")` marks the context with transaction intent -2. **Lazy Creation**: Transaction is created only when first DB operation occurs (not immediately) -3. **Pool Name Based**: Transaction is tracked by pool name, not pool instance -4. **Auto-Join**: All DB operations using the same context automatically join the transaction -5. **Auto-Finalization**: Happens automatically in `FinalizeResponse()`: - - Returns `nil` + status < 400 → **Commit** - - Returns error OR status >= 400 → **Rollback** - -**Key Points:** -- No need to manually inject DbPool - just use the pool name -- Transaction is created lazily (zero overhead if no DB operations) -- All operations in the same context automatically share the transaction -- Rollback happens on **any** error status (400+), even if handler returns nil error - -**Example - Status-based rollback:** -```go -func (s *Service) Create(ctx *request.Context, req *Request) error { - ctx.BeginTransaction("db") - - s.repo.Create(ctx, data) - - // Even though error is nil, transaction will rollback because status = 400 - return ctx.Api.BadRequest("Validation failed") // ← Triggers rollback! -} -``` - -### Manual Transaction Control - -For advanced scenarios (dry-run, testing, conditional commit): - -```go -// Dry-run: Execute operations but don't persist -func (s *Service) DryRun(ctx *request.Context, req *Request) error { - ctx.BeginTransaction("main-db") - - // Execute all operations - result, err := s.repo.Create(ctx, req) - if err != nil { - return err - } - - // Manual rollback - changes discarded - ctx.RollbackTransaction("main-db") - - // Return 200 OK with results - return ctx.Api.Ok(map[string]any{ - "message": "Dry run successful", - "result": result, - }) -} - -// Conditional commit -func (s *Service) BatchProcess(ctx *request.Context, items []Item) error { - ctx.BeginTransaction("main-db") - - successCount := s.processItems(ctx, items) - - if successCount < len(items) * 0.8 { - ctx.RollbackTransaction("main-db") // Below threshold - return ctx.Api.Ok(map[string]any{"status": "rolled_back"}) - } - - ctx.CommitTransaction("main-db") // Above threshold - return ctx.Api.Ok(map[string]any{"status": "committed"}) -} -``` - -**Available Methods:** -- `ctx.RollbackTransaction(poolName)` - Force rollback -- `ctx.CommitTransaction(poolName)` - Force commit - -**See:** [Manual Transaction Control Examples](../examples/manual-transaction-control.md) - -### Using Without Request Context (Service Layer) - -If you need transactions outside of HTTP handlers, use `serviceapi.BeginTransaction()`: - -```go -import "github.com/primadi/lokstra/serviceapi" - -func (s *UserService) DoWork(ctx context.Context) (err error) { - // Begin transaction (same API, but for standard context.Context) - ctx, finish := serviceapi.BeginTransaction(ctx, "main-db") - defer finish(&err) - - // Database operations join the transaction - s.repo1.Create(ctx, ...) - s.repo2.Update(ctx, ...) - - return nil // Auto commit -} -``` - -**Note:** In HTTP handlers, prefer `ctx.BeginTransaction()` from `request.Context` as shown above. - -### Transaction with Multiple Pools - -Each pool name has its own transaction context: - -```go -func (s *Service) Transfer(ctx *request.Context, amount float64) error { - // Transaction for main database - ctx.BeginTransaction("main-db") - - // Transaction for analytics database (separate) - ctx.BeginTransaction("analytics-db") - - // Operations on main-db join main-db transaction - s.mainRepo.Deduct(ctx, amount) - - // Operations on analytics-db join analytics-db transaction - s.analyticsRepo.Log(ctx, "transfer", amount) - - return nil -} -``` - -### Ignoring Parent Transaction - -Sometimes you need to execute operations outside of a transaction (e.g., audit logs that must commit immediately): - -```go -import "github.com/primadi/lokstra/serviceapi" - -func (s *Service) CreateWithAudit(ctx *request.Context, data *Data) error { - ctx.BeginTransaction("main-db") - - // This joins the transaction - if err := s.repo.Create(ctx, data); err != nil { - return err - } - - // This uses a separate connection (no transaction) - isolatedCtx := serviceapi.WithoutTransaction(ctx) - s.auditRepo.Log(isolatedCtx, "data_created") // Commits immediately - - return nil -} -``` - -## Summary - -### Recommended Setup Flow - -1. **Use lokstra_init** for initialization (handles everything correctly) -2. **Define pools** in YAML `dbpool-definitions:` section -3. **Inject DbPool** into services using `@Inject "pool-name"` -4. **Use transactions** via `ctx.BeginTransaction("pool-name")` in handlers - -### Component Usage - -| Component | Purpose | How to Get | -|-----------|---------|------------| -| **DbPoolManager** | Manages pools | Service: `"dbpool-manager"` | -| **DbPool** | Connection pool | Inject: `@Inject "pool-name"` or `lokstra_registry.GetService[DbPool]("pool-name")` | -| **DbConn** | Individual connection | `pool.Acquire(ctx)` | -| **Transaction** | Database transaction | `ctx.BeginTransaction("pool-name")` (recommended) | - -## See Also - -- [Lokstra Initialization](./09-lokstra-init.md) - **Recommended initialization approach** -- [Service Registration](./02-service/index.md) - Service setup -- [Dependency Injection](./07-inject-annotation.md) - Injection patterns -- [Configuration Management](./04-config/index.md) - YAML configuration -- [DbPool Manager API Reference](../03-api-reference/06-services/dbpool-manager.md) - API details diff --git a/docs/02-framework-guide/08-inject-cfg-annotation.md b/docs/02-framework-guide/08-inject-cfg-annotation.md index e468afc5..23344e71 100644 --- a/docs/02-framework-guide/08-inject-cfg-annotation.md +++ b/docs/02-framework-guide/08-inject-cfg-annotation.md @@ -1,46 +1,47 @@ --- layout: default -title: "@InjectCfgValue Annotation" +title: "Config Injection with @Inject" parent: Framework Guide nav_order: 8 --- -# @InjectCfgValue Annotation +# Config Injection with @Inject ## Overview -The `@InjectCfgValue` annotation injects configuration values from `config.yaml` into service fields. It provides type-safe configuration injection with automatic type detection and optional default values. +The `@Inject` annotation with `cfg:` prefix injects configuration values from `config.yaml` into service fields. It provides type-safe configuration injection with automatic type detection and optional default values. ## Basic Syntax ```go -// @InjectCfgValue "config.key" +// @Inject "cfg:config.key" FieldName FieldType // or with default value -// @InjectCfgValue key="config.key", default="value" +// @Inject "cfg:config.key", "default-value" FieldName FieldType ``` ## Supported Formats -### 1. Positional Arguments +### 1. Positional Arguments (Recommended) ```go -// @InjectCfgValue "smtp.host" +// @Inject "cfg:smtp.host" SMTPHost string -// @InjectCfgValue "smtp.host", "localhost" +// @Inject "cfg:smtp.host", "localhost" SMTPHost string ``` -### 2. Named Arguments +### 2. Named Arguments (Legacy compatibility) ```go -// @InjectCfgValue key="smtp.host" +// @Inject service="cfg:smtp.host" SMTPHost string -// @InjectCfgValue key="smtp.host", default="localhost" +// Note: default value in second position +// @Inject "cfg:smtp.host", "localhost" SMTPHost string ``` @@ -65,11 +66,11 @@ The framework automatically detects the field type and uses the appropriate `Get // @Service name="email-service" type EmailService struct { // No default - required in config - // @InjectCfgValue "smtp.host" + // @Inject "cfg:smtp.host" SMTPHost string // With default - // @InjectCfgValue key="smtp.from", default="noreply@example.com" + // @Inject "cfg:smtp.from", "noreply@example.com" FromEmail string } ``` @@ -93,10 +94,10 @@ configs: ```go // @Service name="rate-limiter" type RateLimiter struct { - // @InjectCfgValue key="rate.max-requests", default="100" + // @Inject "cfg:rate.max-requests", "100" MaxRequests int - // @InjectCfgValue key="rate.window-seconds", default="60" + // @Inject "cfg:rate.window-seconds", "60" WindowSeconds int64 } ``` @@ -112,10 +113,10 @@ WindowSeconds: lokstra_registry.GetConfigInt("rate.window-seconds", 60), ```go // @Service name="feature-flags" type FeatureFlags struct { - // @InjectCfgValue key="features.new-ui", default="false" + // @Inject "cfg:features.new-ui", "false" EnableNewUI bool - // @InjectCfgValue key="features.debug-mode", default="false" + // @Inject "cfg:features.debug-mode", "false" DebugMode bool } ``` @@ -131,10 +132,10 @@ DebugMode: lokstra_registry.GetConfigBool("features.debug-mode", false), ```go // @Service name="cache-service" type CacheService struct { - // @InjectCfgValue key="cache.ttl", default="5m" + // @Inject "cfg:cache.ttl", "5m" TTL time.Duration - // @InjectCfgValue key="cache.cleanup-interval", default="1h" + // @Inject "cfg:cache.cleanup-interval", "1h" CleanupInterval time.Duration } ``` @@ -150,10 +151,10 @@ CleanupInterval: lokstra_registry.GetConfigDuration("cache.cleanup-interval", 1* ```go // @Service name="payment-service" type PaymentService struct { - // @InjectCfgValue key="payment.fee-percentage", default="2.5" + // @Inject "cfg:payment.fee-percentage", "2.5" FeePercentage float64 - // @InjectCfgValue key="payment.min-amount", default="10.0" + // @Inject "cfg:payment.min-amount", "10.0" MinAmount float32 } ``` @@ -176,35 +177,35 @@ import "time" // @Service name="app-config" type AppConfig struct { // String configs - // @InjectCfgValue key="app.name", default="MyApp" + // @Inject "cfg:app.name", "MyApp" AppName string - // @InjectCfgValue "app.version" + // @Inject "cfg:app.version" Version string // Required, no default // Integer configs - // @InjectCfgValue key="server.port", default="8080" + // @Inject "cfg:server.port", "8080" ServerPort int - // @InjectCfgValue key="server.max-connections", default="1000" + // @Inject "cfg:server.max-connections", "1000" MaxConnections int64 // Boolean configs - // @InjectCfgValue key="server.enable-gzip", default="true" + // @Inject "cfg:server.enable-gzip", "true" EnableGzip bool - // @InjectCfgValue key="server.debug", default="false" + // @Inject "cfg:server.debug", "false" Debug bool // Duration configs - // @InjectCfgValue key="server.read-timeout", default="30s" + // @Inject "cfg:server.read-timeout", "30s" ReadTimeout time.Duration - // @InjectCfgValue key="server.write-timeout", default="30s" + // @Inject "cfg:server.write-timeout", "30s" WriteTimeout time.Duration // Float configs - // @InjectCfgValue key="cache.eviction-ratio", default="0.1" + // @Inject "cfg:cache.eviction-ratio", "0.1" CacheEvictionRatio float64 } @@ -262,7 +263,7 @@ func RegisterAppConfig() { If config key is missing in `config.yaml`, uses the default: ```go -// @InjectCfgValue key="smtp.host", default="localhost" +// @Inject "cfg:smtp.host", "localhost" SMTPHost string // "localhost" if not in config ``` @@ -271,13 +272,13 @@ SMTPHost string // "localhost" if not in config If config key is missing, uses type's zero value: ```go -// @InjectCfgValue "smtp.host" +// @Inject "cfg:smtp.host" SMTPHost string // "" if not in config -// @InjectCfgValue "server.port" +// @Inject "cfg:server.port" Port int // 0 if not in config -// @InjectCfgValue "debug" +// @Inject "cfg:debug" Debug bool // false if not in config ``` @@ -287,19 +288,19 @@ Debug bool // false if not in config ✅ **Good:** ```go -// @InjectCfgValue key="database.connection-timeout", default="30s" +// @Inject "cfg:database.connection-timeout", "30s" DBTimeout time.Duration -// @InjectCfgValue key="auth.jwt-secret" +// @Inject "cfg:auth.jwt-secret" JWTSecret string ``` ❌ **Bad:** ```go -// @InjectCfgValue "timeout" // Too vague +// @Inject "cfg:timeout" // Too vague Timeout time.Duration -// @InjectCfgValue "secret" // Not descriptive +// @Inject "cfg:secret" // Not descriptive Secret string ``` @@ -307,10 +308,10 @@ Secret string ✅ **Good:** ```go -// @InjectCfgValue key="server.port", default="8080" +// @Inject "cfg:server.port", "8080" Port int -// @InjectCfgValue key="cache.ttl", default="5m" +// @Inject "cfg:cache.ttl", "5m" CacheTTL time.Duration ``` @@ -339,13 +340,13 @@ configs: ```go // @Service name="db-service" type DBService struct { - // @InjectCfgValue key="database.host", default="localhost" + // @Inject "cfg:database.host", "localhost" Host string - // @InjectCfgValue key="database.port", default="5432" + // @Inject "cfg:database.port", "5432" Port int - // @InjectCfgValue key="database.timeout", default="30s" + // @Inject "cfg:database.timeout", "30s" Timeout time.Duration } ``` @@ -356,16 +357,16 @@ type DBService struct { // @Service name="payment-service" type PaymentService struct { // REQUIRED - no default - // @InjectCfgValue "payment.api-key" + // @Inject "cfg:payment.api-key" APIKey string // Optional - has default - // @InjectCfgValue key="payment.timeout", default="60s" + // @Inject "cfg:payment.timeout", "60s" Timeout time.Duration } ``` -## Combining @Inject and @InjectCfgValue +## Combining @Inject (Service and Config) ```go // @Service name="notification-service" @@ -378,13 +379,13 @@ type NotificationService struct { EmailSvc EmailService // Configuration - // @InjectCfgValue key="notifications.enabled", default="true" + // @Inject "cfg:notifications.enabled", "true" Enabled bool - // @InjectCfgValue key="notifications.batch-size", default="100" + // @Inject "cfg:notifications.batch-size", "100" BatchSize int - // @InjectCfgValue key="notifications.retry-attempts", default="3" + // @Inject "cfg:notifications.retry-attempts", "3" RetryAttempts int } diff --git a/docs/02-framework-guide/09-lokstra-init.md b/docs/02-framework-guide/09-lokstra-init.md index 58d19aad..81a34f48 100644 --- a/docs/02-framework-guide/09-lokstra-init.md +++ b/docs/02-framework-guide/09-lokstra-init.md @@ -175,7 +175,7 @@ if cfg.EnableAnnotation { lokstra.Bootstrap(cfg.AnnotationScanPaths...) } ``` -- Scans for `@RouterService`, `@Service`, `@Route` annotations +- Scans for `@EndpointService`, `@Service`, `@Route` annotations - Generates router code if needed - Auto-registers services diff --git a/docs/02-framework-guide/index.md b/docs/02-framework-guide/index.md index 0027229f..3ae749c1 100644 --- a/docs/02-framework-guide/index.md +++ b/docs/02-framework-guide/index.md @@ -174,7 +174,7 @@ deployments: Build REST APIs with explicit annotations. **What you'll learn:** -- ✅ `@RouterService` for service definition +- ✅ `@EndpointService` for service definition - ✅ `@Route` for explicit route mapping - ✅ `@Inject` for dependency injection - ✅ Variable support in routes (`${config-vars}`) @@ -183,7 +183,7 @@ Build REST APIs with explicit annotations. **Key concepts:** ```go // Explicit route definitions with annotations -// @RouterService name="user-service", prefix="/api/users" +// @EndpointService name="user-service", prefix="/api/users" type UserService struct { // @Inject "user-repository" UserRepo UserRepository diff --git a/docs/02-framework-guide/lokstra-vs-nestjs.md b/docs/02-framework-guide/lokstra-vs-nestjs.md index 60f3b3c7..d66a8ec4 100644 --- a/docs/02-framework-guide/lokstra-vs-nestjs.md +++ b/docs/02-framework-guide/lokstra-vs-nestjs.md @@ -169,7 +169,7 @@ export class AppModule {} ```go // Lokstra - Annotation-driven routes (explicit) -// @RouterService name="user-service", prefix="/api/users" +// @EndpointService name="user-service", prefix="/api/users" type UserService struct { // @Inject "user-repository" UserRepo UserRepository diff --git a/docs/02-framework-guide/lokstra-vs-spring-boot.md b/docs/02-framework-guide/lokstra-vs-spring-boot.md index b3fbbaa8..9caaedcc 100644 --- a/docs/02-framework-guide/lokstra-vs-spring-boot.md +++ b/docs/02-framework-guide/lokstra-vs-spring-boot.md @@ -218,7 +218,7 @@ public class AppConfig { // Service method signatures determine routes func (s *UserService) GetAll(p *GetAllParams) ([]User, error) // GET /users // Lokstra - Annotation-driven routes (explicit) -// @RouterService name="user-service", prefix="/api/users" +// @EndpointService name="user-service", prefix="/api/users" type UserService struct { // @Inject "user-repository" UserRepo UserRepository diff --git a/docs/AI-AGENT-GUIDE.md b/docs/AI-AGENT-GUIDE.md index 213cd5e6..abbb226a 100644 --- a/docs/AI-AGENT-GUIDE.md +++ b/docs/AI-AGENT-GUIDE.md @@ -196,13 +196,13 @@ import ( "github.com/primadi/lokstra/core/deploy" "github.com/primadi/lokstra/lokstra_registry" - // Import packages with @RouterService annotations + // Import packages with @EndpointService annotations _ "myapp/modules/user/application" _ "myapp/modules/order/application" ) func main() { - // Auto-generates code when @RouterService changes detected + // Auto-generates code when @EndpointService changes detected lokstra.Bootstrap() deploy.SetLogLevelFromEnv() // LOKSTRA_LOG_LEVEL=debug @@ -221,7 +221,7 @@ import ( "myapp/modules/user/infrastructure" ) -// @RouterService name="user-service", prefix="/api/users" +// @EndpointService name="user-service", prefix="/api/users" type UserServiceImpl struct { // @Inject "user-repository" UserRepo domain.UserRepository @@ -288,7 +288,7 @@ service-definitions: dsn: "memory://users" user-service: - # Type auto-registered via @RouterService annotation + # Type auto-registered via @EndpointService annotation depends-on: - user-repository @@ -797,7 +797,7 @@ LOKSTRA_DEPLOYMENT=production go run . ## Annotation System -### @RouterService Annotation (HTTP Controllers) +### @EndpointService Annotation (HTTP Controllers) Generate REST routers automatically from service methods. Use for services that expose HTTP endpoints. @@ -809,7 +809,7 @@ import ( "myapp/domain" ) -// @RouterService name="user-service", prefix="/api", middlewares=["recovery", "request-logger"] +// @EndpointService name="user-service", prefix="/api", middlewares=["recovery", "request-logger"] type UserServiceImpl struct { // @Inject "user-repository" UserRepo domain.UserRepository @@ -880,16 +880,16 @@ type AuthService struct { Cache domain.CacheService // Configuration injection (type-safe) - // @InjectCfgValue "auth.jwt-secret" + // @Inject "cfg:auth.jwt-secret" JwtSecret string - // @InjectCfgValue key="auth.token-expiry", default="24h" + // @Inject "cfg:auth.token-expiry", "24h" TokenExpiry time.Duration - // @InjectCfgValue key="auth.max-attempts", default="5" + // @Inject "cfg:auth.max-attempts", "5" MaxAttempts int - // @InjectCfgValue key="auth.debug-mode", default="false" + // @Inject "cfg:auth.debug-mode", "false" DebugMode bool } @@ -929,7 +929,7 @@ configs: **Recommended: Automatic with Bootstrap** ```go func main() { - lokstra.Bootstrap() // Auto-generates when @Service/@RouterService changes detected + lokstra.Bootstrap() // Auto-generates when @Service/@EndpointService changes detected // App code... } ``` @@ -952,13 +952,12 @@ go run . --generate-only | Annotation | Purpose | Example | |------------|---------|---------| -| `@RouterService` | HTTP service with routes | `@RouterService name="user-service", prefix="/api"` | +| `@EndpointService` | HTTP service with routes | `@EndpointService name="user-service", prefix="/api"` | | `@Service` | Pure service (no HTTP) | `@Service name="auth-service"` | -| `@Inject` | Dependency injection | `@Inject "user-repository"` or `@Inject service="cache", optional=true` | -| `@InjectCfgValue` | Config injection | `@InjectCfgValue "jwt-secret"` or `@InjectCfgValue key="timeout", default="30s"` | +| `@Inject` | Dependency/config injection | `@Inject "user-repository"` or `@Inject "cfg:timeout", "30s"` | | `@Route` | HTTP route mapping | `@Route "GET /users/{id}"` | -**@RouterService Parameters:** +**@EndpointService Parameters:** - `name`: Service name (required) - `prefix`: URL prefix (optional, default: "/") - **Supports variables**: `prefix="${api-prefix}"` resolves from config @@ -968,15 +967,13 @@ go run . --generate-only - `name`: Service name (required, positional or named) **@Inject Parameters:** -- `service`: Service name (required, positional or named) +- `service`: Service name (required, positional or named) OR +- `cfg:key`: Config key for config injection (e.g., `@Inject "cfg:auth.jwt-secret"`) - `optional`: Boolean, default `false` - set to `true` for optional dependencies +- For config injection: Second positional arg or `default` param for default value +- Config types auto-detected: `string`, `int`, `bool`, `float64`, `time.Duration` -**@InjectCfgValue Parameters:** -- `key`: Config key (required, positional or named) -- `default`: Default value (optional) -- Type auto-detected: `string`, `int`, `bool`, `float64`, `time.Duration` - -**@Route Parameters:** +**@Route Parameters:**** - HTTP method + path pattern - Supports path parameters: `{id}`, `{userId}`, etc. - **Supports variables in path**: `"GET ${api-version}/users/{id}"` resolves from config @@ -996,7 +993,7 @@ configs: ```go // Usage in annotations -// @RouterService name="user-service", prefix="${api-prefix}" +// @EndpointService name="user-service", prefix="${api-prefix}" // Resolves to: prefix="/api/v1" // @Route "GET ${api-version}/users/{id}" @@ -1271,7 +1268,7 @@ myapp/ │ ├── repository.go │ └── service.go ├── application/ - │ ├── user_service.go # Contains @RouterService, @Route + │ ├── user_service.go # Contains @EndpointService, @Route │ └── zz_generated.lokstra.go # Auto-generated └── infrastructure/ └── user_repository.go @@ -1456,7 +1453,7 @@ panic: service 'user-service' not found in registry ``` **Solution:** -- **For business services**: Use `@RouterService` annotation + `lokstra autogen .` +- **For business services**: Use `@EndpointService` annotation + `lokstra autogen .` - **For infrastructure services**: Check service registered: `lokstra_registry.RegisterServiceType("user-service-factory", ...)` - Check config.yaml: Service name must match factory type - Check annotation-generated file: `zz_generated.lokstra.go` exists @@ -1510,7 +1507,7 @@ zz_generated.lokstra.go not created lokstra autogen ./path/to/service # Ensure annotations are correct -# @RouterService name="service-name", prefix="/api" +# @EndpointService name="service-name", prefix="/api" # @Route "GET /users/{id}" ``` @@ -1617,12 +1614,12 @@ LOKSTRA_CONFIG=./config.yaml - Include error handling - Include validation tags - Include config.yaml if using framework mode - - **Use `@RouterService` annotations for business services** + - **Use `@EndpointService` annotations for business services** 4. **Follow project structure:** - `domain/` for interfaces and models - `infrastructure/` for data access (repositories) - - `application/` for business logic (services with `@RouterService`) + - `application/` for business logic (services with `@EndpointService`) - `main.go` for bootstrap (import annotation packages) 5. **Use type-safe patterns:** @@ -1631,7 +1628,7 @@ LOKSTRA_CONFIG=./config.yaml - **Prefer annotations over manual registration for business services** 6. **Recommend annotation workflow:** - - Define service with `@RouterService` annotation + - Define service with `@EndpointService` annotation - Add routes with `@Route` annotation - Run `lokstra autogen .` to generate code - Manual registration only for infrastructure/custom factories diff --git a/docs/AI-DOCUMENTATION-SUMMARY.md b/docs/AI-DOCUMENTATION-SUMMARY.md index 1ada969f..24975634 100644 --- a/docs/AI-DOCUMENTATION-SUMMARY.md +++ b/docs/AI-DOCUMENTATION-SUMMARY.md @@ -16,7 +16,7 @@ Dokumentasi lengkap dan terstruktur yang mencakup: - Router patterns (29+ handler signatures) - Service patterns (Factory, DI, Lazy loading) - Configuration YAML (schema lengkap) -- Annotation system (@RouterService, @Route, @Inject) +- Annotation system (@EndpointService, @Route, @Inject) - Middleware usage (built-in dan custom) - Dependency injection (LazyLoad, Cached) - Project structure templates (3 levels) @@ -145,7 +145,7 @@ File konteks minimal untuk AI assistants: - Multi-environment setup ### 4. **Annotations** -- @RouterService +- @EndpointService - @Route (HTTP methods + paths) - @Inject - Code generation (`lokstra autogen`) diff --git a/docs/QUICK-REFERENCE.md b/docs/QUICK-REFERENCE.md index 0bf2150a..61290c67 100644 --- a/docs/QUICK-REFERENCE.md +++ b/docs/QUICK-REFERENCE.md @@ -238,7 +238,7 @@ func (s *Service) DoWork(ctx context.Context) (err error) { ### Annotation-Based Service (Recommended) ```go -// @RouterService name="user-service", prefix="/api/users" +// @EndpointService name="user-service", prefix="/api/users" type UserService struct { // @Inject "user-repository" UserRepo UserRepository @@ -266,7 +266,7 @@ func (s *UserService) Delete(p *DeleteUserParams) error { **Annotation with Variables** (resolves from config.yaml): ```go -// @RouterService name="user-service", prefix="${api-prefix}" +// @EndpointService name="user-service", prefix="${api-prefix}" // @Route "GET ${api-version}/users/{id}" ``` @@ -295,13 +295,13 @@ type AuthService struct { Cache CacheService // Configuration injection - // @InjectCfgValue "auth.jwt-secret" + // @Inject "cfg:auth.jwt-secret" JwtSecret string - // @InjectCfgValue key="auth.token-expiry", default="24h" + // @Inject "cfg:auth.token-expiry", "24h" TokenExpiry time.Duration - // @InjectCfgValue key="auth.max-attempts", default="5" + // @Inject "cfg:auth.max-attempts", "5" MaxAttempts int } @@ -332,8 +332,8 @@ configs: **@Service supports:** - `@Inject` - Service dependencies (required or optional) -- `@InjectCfgValue` - Configuration injection (auto-typed) -- No HTTP routes (use `@RouterService` for that) +- `@Inject "cfg:..."` - Configuration injection (auto-typed) +- No HTTP routes (use `@EndpointService` for that) **Generated code:** ```go @@ -354,20 +354,16 @@ func RegisterAuthService() { | Annotation | Purpose | Where | |------------|---------|-------| -| `@RouterService` | HTTP service with routes | Above struct | +| `@EndpointService` | HTTP service with routes | Above struct | | `@Service` | Pure service (no HTTP) | Above struct | | `@Route` | HTTP endpoint | Above method (RouterService only) | -| `@Inject` | Dependency injection | Above field | -| `@InjectCfgValue` | Config injection | Above field | +| `@Inject` | Dependency/config injection | Above field | -**@Inject parameters:** -- `service` (positional or named) - service name +**@Inject parameters:**** +- `service` (positional or named) - service name (or use `cfg:` prefix for config) - `optional` - `true`/`false` (default: `false`) - -**@InjectCfgValue parameters:** -- `key` (positional or named) - config key -- `default` - default value (optional) -- Type auto-detected: `string`, `int`, `bool`, `float64`, `time.Duration` +- For config: `@Inject "cfg:config.key"` or `@Inject "cfg:config.key", "default"` +- Type auto-detected for config: `string`, `int`, `bool`, `float64`, `time.Duration` ### Manual Service Factory (Advanced) @@ -553,7 +549,7 @@ deployments: ## Annotations ```go -// @RouterService name="user-service", prefix="/api", middlewares=["recovery"] +// @EndpointService name="user-service", prefix="/api", middlewares=["recovery"] type UserService struct { // @Inject "user-repository" UserRepo UserRepository diff --git a/docs/examples/STRUCT-DURATION-BACKTICK.md b/docs/examples/STRUCT-DURATION-BACKTICK.md index 94e5d902..f902d76e 100644 --- a/docs/examples/STRUCT-DURATION-BACKTICK.md +++ b/docs/examples/STRUCT-DURATION-BACKTICK.md @@ -21,12 +21,12 @@ type RetryConfig struct { MaxDelay time.Duration `json:"max_delay"` } -// @RouterService name="app-service", prefix="/api" +// @EndpointService name="app-service", prefix="/api" type AppService struct { - // @InjectCfgValue "server" + // @Inject "cfg:server" Server ServerConfig - // @InjectCfgValue "retry" + // @Inject "cfg:retry" Retry RetryConfig } ``` @@ -75,19 +75,19 @@ Server: func() ServerConfig { ### Option A: Using Backtick (Recommended - No Escaping!) ```go -// @InjectCfgValue key="server", default=`ServerConfig{Host: "localhost", Port: 8080, ReadTimeout: 30*time.Second}` +// @Inject "cfg:server", `ServerConfig{Host: "localhost", Port: 8080, ReadTimeout: 30*time.Second}` Server ServerConfig ``` ### Option B: Using Double Quotes (Need Escaping) ```go -// @InjectCfgValue key="server", default="ServerConfig{Host: \"localhost\", Port: 8080, ReadTimeout: 30*time.Second}" +// @Inject "cfg:server", "ServerConfig{Host: \"localhost\", Port: 8080, ReadTimeout: 30*time.Second}" Server ServerConfig ``` ### Option C: Duration String Format ```go -// @InjectCfgValue key="timeout", default="15m" +// @Inject "cfg:timeout", "15m" Timeout time.Duration ``` @@ -107,9 +107,9 @@ type DatabaseConfig struct { IdleTimeout time.Duration `json:"idle_timeout"` } -// @RouterService name="user-service", prefix="/api/users" +// @EndpointService name="user-service", prefix="/api/users" type UserService struct { - // @InjectCfgValue key="database", default=`DatabaseConfig{Host: "localhost", Port: 5432, MaxConnections: 10, ConnectTimeout: 5*time.Second, QueryTimeout: 30*time.Second, IdleTimeout: 10*time.Minute}` + // @Inject "cfg:database", `DatabaseConfig{Host: "localhost", Port: 5432, MaxConnections: 10, ConnectTimeout: 5*time.Second, QueryTimeout: 30*time.Second, IdleTimeout: 10*time.Minute}` DB DatabaseConfig } diff --git a/docs/fixes/unused-imports-fix.md b/docs/fixes/unused-imports-fix.md index be6ab82f..8b6afc6d 100644 --- a/docs/fixes/unused-imports-fix.md +++ b/docs/fixes/unused-imports-fix.md @@ -12,7 +12,7 @@ import ( core_repository "github.com/primadi/lokstra-auth/infrastructure/repository" // USED in @Inject ) -// @RouterService name="credential-service", prefix="/api" +// @EndpointService name="credential-service", prefix="/api" type CredentialService struct { // @Inject "credential-repository" Repo core_repository.CredentialRepository diff --git a/lokstra_init/initialize.go b/lokstra_init/initialize.go index 6ff6ea56..8454e2a2 100644 --- a/lokstra_init/initialize.go +++ b/lokstra_init/initialize.go @@ -75,7 +75,7 @@ func BootstrapAndRun(opts ...InitializeOption) error { PanicOnConfigError: true, LogLevel: logger.LogLevelInfo, EnableLoadConfig: true, - EnableAnnotation: true, // Auto-detect @RouterService + EnableAnnotation: true, // Auto-detect @EndpointService EnablePgxSyncMap: false, SkipMigrationOnProd: true, PgxSyncMapDbPoolName: "db_main", diff --git a/lokstra_init/option.go b/lokstra_init/option.go index a1a81c46..abf1a621 100644 --- a/lokstra_init/option.go +++ b/lokstra_init/option.go @@ -25,7 +25,7 @@ func WithLogLevel(level logger.LogLevel) InitializeOption { // enable annotations with optional scan paths // if no paths provided, use default paths -// example annotations : @RouterService, @Service, @Route +// example annotations : @EndpointService, @Service, @Route // default enable is true, path is empty (current folder) func WithAnnotations(enable bool, paths ...string) InitializeOption { return func(c *InitializeConfig) { diff --git a/lokstra_registry/registry.go b/lokstra_registry/registry.go index 5b377093..5d73a6f1 100644 --- a/lokstra_registry/registry.go +++ b/lokstra_registry/registry.go @@ -224,7 +224,7 @@ func GetAllRouters() map[string]router.Router { // ) // // RegisterRouterServiceType registers a service type with HTTP routing configuration. -// Use this for services that expose HTTP endpoints (annotated with @RouterService). +// Use this for services that expose HTTP endpoints (annotated with @EndpointService). // For simple infrastructure services (DB, Redis, etc), use RegisterServiceType instead. // // Parameters: diff --git a/project_templates/02_app_framework/01_enterprise_router_service/README.md b/project_templates/02_app_framework/01_enterprise_router_service/README.md index 85681a0d..162cff53 100644 --- a/project_templates/02_app_framework/01_enterprise_router_service/README.md +++ b/project_templates/02_app_framework/01_enterprise_router_service/README.md @@ -224,9 +224,9 @@ The `simple-auth` middleware provides basic Bearer token authentication: - Extracts user ID from token (e.g., `demo-user123` → user ID: `user123`) - Stores user info in request context -**Usage in @RouterService:** +**Usage in @EndpointService:** ```go -// @RouterService name="user-service", prefix="/api", middlewares=["recovery", "request-logger", "simple-auth"] +// @EndpointService name="user-service", prefix="/api", middlewares=["recovery", "request-logger", "simple-auth"] type UserServiceImpl struct { UserRepo domain.UserRepository } @@ -296,9 +296,9 @@ func registerMiddlewareTypes() { } ``` -**Step 2:** Use in `@RouterService` annotation: +**Step 2:** Use in `@EndpointService` annotation: ```go -// @RouterService name="my-service", middlewares=["recovery", "my-middleware"] +// @EndpointService name="my-service", middlewares=["recovery", "my-middleware"] type MyService struct {} ``` diff --git a/project_templates/02_app_framework/01_enterprise_router_service/health_router.go b/project_templates/02_app_framework/01_enterprise_router_service/health_router.go index 16fa9f78..4228c7a8 100644 --- a/project_templates/02_app_framework/01_enterprise_router_service/health_router.go +++ b/project_templates/02_app_framework/01_enterprise_router_service/health_router.go @@ -6,7 +6,7 @@ import ( ) // NewHealthRouter creates a manual health check router -// This demonstrates how to create routers manually without @RouterService annotation +// This demonstrates how to create routers manually without @EndpointService annotation func NewHealthRouter() router.Router { r := router.New("health-router") diff --git a/project_templates/02_app_framework/01_enterprise_router_service/modules/order/application/order_service.go b/project_templates/02_app_framework/01_enterprise_router_service/modules/order/application/order_service.go index f680f8c3..5e4ad2fe 100644 --- a/project_templates/02_app_framework/01_enterprise_router_service/modules/order/application/order_service.go +++ b/project_templates/02_app_framework/01_enterprise_router_service/modules/order/application/order_service.go @@ -7,7 +7,7 @@ import ( userDomain "github.com/primadi/lokstra/project_templates/02_app_framework/01_enterprise_router_service/modules/user/domain" ) -// @RouterService name="order-service", prefix="/api", middlewares=["recovery", "request-logger"] +// @EndpointService name="order-service", prefix="/api", middlewares=["recovery", "request-logger"] type OrderServiceImpl struct { // @Inject "@store.order-repository" OrderRepo domain.OrderRepository diff --git a/project_templates/02_app_framework/01_enterprise_router_service/modules/order/application/zz_cache.lokstra.json b/project_templates/02_app_framework/01_enterprise_router_service/modules/order/application/zz_cache.lokstra.json index 30c71ef9..f8add5ac 100644 --- a/project_templates/02_app_framework/01_enterprise_router_service/modules/order/application/zz_cache.lokstra.json +++ b/project_templates/02_app_framework/01_enterprise_router_service/modules/order/application/zz_cache.lokstra.json @@ -3,15 +3,15 @@ "files": { "order_service.go": { "filename": "order_service.go", - "checksum": "d3bd9a1249a57191cfeea8a4072695a9c39f07c50a19c1b4e87d50efdd2e8466", + "checksum": "fbb2d0889c267c9619580882a1ed9a9d0fc72f8ce47ad01546425f3bbbf6837d", "annotations": 9, - "last_scan": "2025-12-16T17:43:21.8176723+07:00", + "last_scan": "2026-01-11T03:01:08.6140823+07:00", "generated": [ "zz_generated.lokstra.go" ], - "generated_mod_time": "2025-12-16T17:43:21.8176723+07:00" + "generated_mod_time": "2026-01-11T03:01:08.6140823+07:00" } }, - "updated_at": "2025-12-16T17:43:21.8176723+07:00", - "generated_checksum": "a865053805281d0e8b085d81574cf15b64946051af9c4f7d7a2e4361cc0f2e43" + "updated_at": "2026-01-11T03:01:08.6140823+07:00", + "generated_checksum": "ab7cc6a75239f47179949d2a29decfdffbbcd3b389507cd00170bef04cc3fb62" } \ No newline at end of file diff --git a/project_templates/02_app_framework/01_enterprise_router_service/modules/order/application/zz_generated.lokstra.go b/project_templates/02_app_framework/01_enterprise_router_service/modules/order/application/zz_generated.lokstra.go index 0643ebaa..b976cc9a 100644 --- a/project_templates/02_app_framework/01_enterprise_router_service/modules/order/application/zz_generated.lokstra.go +++ b/project_templates/02_app_framework/01_enterprise_router_service/modules/order/application/zz_generated.lokstra.go @@ -1,6 +1,12 @@ // AUTO-GENERATED CODE - DO NOT EDIT // Generated by lokstra-annotation from annotations in this folder -// Annotations: @RouterService, @Service, @Inject, @InjectCfgValue, @Route +// Annotations: @EndpointService, @Service, @Inject, @Route +// +// @Inject supports: +// - "service-name" : Direct service injection +// - "@config.key" : Service name from config value +// - "cfg:config.key" : Config value injection +// - "cfg:@config.key" : Config value via indirection package application @@ -8,8 +14,8 @@ import ( "github.com/primadi/lokstra/core/deploy" "github.com/primadi/lokstra/core/proxy" "github.com/primadi/lokstra/lokstra_registry" - domain "github.com/primadi/lokstra/project_templates/02_app_framework/01_enterprise_router_service/modules/order/domain" userDomain "github.com/primadi/lokstra/project_templates/02_app_framework/01_enterprise_router_service/modules/user/domain" + domain "github.com/primadi/lokstra/project_templates/02_app_framework/01_enterprise_router_service/modules/order/domain" ) // Auto-register on package import @@ -81,7 +87,7 @@ func OrderServiceImplFactory(deps map[string]any, config map[string]any) any { } // OrderServiceImplRemoteFactory creates a remote HTTP client for OrderServiceImplInterface -// Auto-generated from @RouterService annotation +// Auto-generated from @EndpointService annotation func OrderServiceImplRemoteFactory(deps, config map[string]any) any { proxyService, ok := config["remote"].(*proxy.Service) if !ok { @@ -92,7 +98,7 @@ func OrderServiceImplRemoteFactory(deps, config map[string]any) any { // RegisterOrderServiceImpl registers the order-service with the registry // Auto-generated from annotations: -// - @RouterService name="order-service", prefix="/api" +// - @EndpointService name="order-service", prefix="/api" // - @Inject annotations // - @Route annotations on methods func RegisterOrderServiceImpl() { diff --git a/project_templates/02_app_framework/01_enterprise_router_service/modules/order/infrastructure/repository/zz_cache.lokstra.json b/project_templates/02_app_framework/01_enterprise_router_service/modules/order/infrastructure/repository/zz_cache.lokstra.json index e65aaa22..20f20fb1 100644 --- a/project_templates/02_app_framework/01_enterprise_router_service/modules/order/infrastructure/repository/zz_cache.lokstra.json +++ b/project_templates/02_app_framework/01_enterprise_router_service/modules/order/infrastructure/repository/zz_cache.lokstra.json @@ -5,13 +5,13 @@ "filename": "order_repository.go", "checksum": "ffc140484afce3b4848145419ca9f619a3b8e65dba0613e2c329b913c78d3680", "annotations": 1, - "last_scan": "2025-12-16T17:43:21.8160622+07:00", + "last_scan": "2026-01-11T03:01:08.6116839+07:00", "generated": [ "zz_generated.lokstra.go" ], - "generated_mod_time": "2025-12-16T17:43:21.8160622+07:00" + "generated_mod_time": "2026-01-11T03:01:08.6116839+07:00" } }, - "updated_at": "2025-12-16T17:43:21.8160622+07:00", - "generated_checksum": "16bf8b62121710a85ab8925d12f8fc61adf16b6bceb54ced37056b178b9482ef" + "updated_at": "2026-01-11T03:01:08.6116839+07:00", + "generated_checksum": "02e212171c7caef98a7aeb8cf70a66c961943cba341e708f3e6da093bea69233" } \ No newline at end of file diff --git a/project_templates/02_app_framework/01_enterprise_router_service/modules/order/infrastructure/repository/zz_generated.lokstra.go b/project_templates/02_app_framework/01_enterprise_router_service/modules/order/infrastructure/repository/zz_generated.lokstra.go index 5ed35515..2da23f2c 100644 --- a/project_templates/02_app_framework/01_enterprise_router_service/modules/order/infrastructure/repository/zz_generated.lokstra.go +++ b/project_templates/02_app_framework/01_enterprise_router_service/modules/order/infrastructure/repository/zz_generated.lokstra.go @@ -1,6 +1,12 @@ // AUTO-GENERATED CODE - DO NOT EDIT // Generated by lokstra-annotation from annotations in this folder -// Annotations: @RouterService, @Service, @Inject, @InjectCfgValue, @Route +// Annotations: @EndpointService, @Service, @Inject, @Route +// +// @Inject supports: +// - "service-name" : Direct service injection +// - "@config.key" : Service name from config value +// - "cfg:config.key" : Config value injection +// - "cfg:@config.key" : Config value via indirection package repository diff --git a/project_templates/02_app_framework/01_enterprise_router_service/modules/user/application/user_service.go b/project_templates/02_app_framework/01_enterprise_router_service/modules/user/application/user_service.go index eed89c6f..3cc4246b 100644 --- a/project_templates/02_app_framework/01_enterprise_router_service/modules/user/application/user_service.go +++ b/project_templates/02_app_framework/01_enterprise_router_service/modules/user/application/user_service.go @@ -4,7 +4,7 @@ import ( "github.com/primadi/lokstra/project_templates/02_app_framework/01_enterprise_router_service/modules/user/domain" ) -// @RouterService name="user-service", prefix="/api", middlewares=["recovery", "request-logger", "simple-auth"] +// @EndpointService name="user-service", prefix="/api", middlewares=["recovery", "request-logger", "simple-auth"] type UserServiceImpl struct { // @Inject "user-repository" UserRepo domain.UserRepository diff --git a/project_templates/02_app_framework/01_enterprise_router_service/modules/user/application/zz_cache.lokstra.json b/project_templates/02_app_framework/01_enterprise_router_service/modules/user/application/zz_cache.lokstra.json index 78bc9f13..622e713a 100644 --- a/project_templates/02_app_framework/01_enterprise_router_service/modules/user/application/zz_cache.lokstra.json +++ b/project_templates/02_app_framework/01_enterprise_router_service/modules/user/application/zz_cache.lokstra.json @@ -3,15 +3,15 @@ "files": { "user_service.go": { "filename": "user_service.go", - "checksum": "27df1fa6c9c87d51734fc041633a354016a31ce59fff68fa3df574068629d2a9", + "checksum": "df5b3ffc03f706f6ef30261b152baf0a1754c848481f60d8d2c9e51370c74c21", "annotations": 9, - "last_scan": "2025-12-16T17:43:21.8182007+07:00", + "last_scan": "2026-01-11T03:01:08.6135279+07:00", "generated": [ "zz_generated.lokstra.go" ], - "generated_mod_time": "2025-12-16T17:43:21.8182007+07:00" + "generated_mod_time": "2026-01-11T03:01:08.6135279+07:00" } }, - "updated_at": "2025-12-16T17:43:21.8182007+07:00", - "generated_checksum": "5277fb8fb492a881e91fddfe495731ad8548aee64af1f7c4158171673965dc54" + "updated_at": "2026-01-11T03:01:08.6135279+07:00", + "generated_checksum": "feac625ae8d96928abcf5febce937226d58824d36b4c991fb8eed48d5b6ffdd3" } \ No newline at end of file diff --git a/project_templates/02_app_framework/01_enterprise_router_service/modules/user/application/zz_generated.lokstra.go b/project_templates/02_app_framework/01_enterprise_router_service/modules/user/application/zz_generated.lokstra.go index eace9b58..4e08b038 100644 --- a/project_templates/02_app_framework/01_enterprise_router_service/modules/user/application/zz_generated.lokstra.go +++ b/project_templates/02_app_framework/01_enterprise_router_service/modules/user/application/zz_generated.lokstra.go @@ -1,6 +1,12 @@ // AUTO-GENERATED CODE - DO NOT EDIT // Generated by lokstra-annotation from annotations in this folder -// Annotations: @RouterService, @Service, @Inject, @InjectCfgValue, @Route +// Annotations: @EndpointService, @Service, @Inject, @Route +// +// @Inject supports: +// - "service-name" : Direct service injection +// - "@config.key" : Service name from config value +// - "cfg:config.key" : Config value injection +// - "cfg:@config.key" : Config value via indirection package application @@ -85,7 +91,7 @@ func UserServiceImplFactory(deps map[string]any, config map[string]any) any { } // UserServiceImplRemoteFactory creates a remote HTTP client for UserServiceImplInterface -// Auto-generated from @RouterService annotation +// Auto-generated from @EndpointService annotation func UserServiceImplRemoteFactory(deps, config map[string]any) any { proxyService, ok := config["remote"].(*proxy.Service) if !ok { @@ -96,7 +102,7 @@ func UserServiceImplRemoteFactory(deps, config map[string]any) any { // RegisterUserServiceImpl registers the user-service with the registry // Auto-generated from annotations: -// - @RouterService name="user-service", prefix="/api" +// - @EndpointService name="user-service", prefix="/api" // - @Inject annotations // - @Route annotations on methods func RegisterUserServiceImpl() { diff --git a/project_templates/02_app_framework/01_enterprise_router_service/modules/user/infrastructure/repository/zz_cache.lokstra.json b/project_templates/02_app_framework/01_enterprise_router_service/modules/user/infrastructure/repository/zz_cache.lokstra.json index cf7828bc..8861ce7b 100644 --- a/project_templates/02_app_framework/01_enterprise_router_service/modules/user/infrastructure/repository/zz_cache.lokstra.json +++ b/project_templates/02_app_framework/01_enterprise_router_service/modules/user/infrastructure/repository/zz_cache.lokstra.json @@ -5,13 +5,13 @@ "filename": "user_repository.go", "checksum": "b8d5736b4b8821d68c9ec0dc142c92962f7474e1e5a6e0496d3bd9652d93026d", "annotations": 1, - "last_scan": "2025-12-16T17:43:21.8160622+07:00", + "last_scan": "2026-01-11T03:01:08.6116839+07:00", "generated": [ "zz_generated.lokstra.go" ], - "generated_mod_time": "2025-12-16T17:43:21.8160622+07:00" + "generated_mod_time": "2026-01-11T03:01:08.6116839+07:00" } }, - "updated_at": "2025-12-16T17:43:21.8160622+07:00", - "generated_checksum": "84e08cd520f9a6f6ccefb1c77c8d82b2b5cf5481eef82a869c454fbf23cf5208" + "updated_at": "2026-01-11T03:01:08.6116839+07:00", + "generated_checksum": "b66fc9647b5ed728017ab3423902544615ade49c07c5ac130f93744464de4712" } \ No newline at end of file diff --git a/project_templates/02_app_framework/01_enterprise_router_service/modules/user/infrastructure/repository/zz_generated.lokstra.go b/project_templates/02_app_framework/01_enterprise_router_service/modules/user/infrastructure/repository/zz_generated.lokstra.go index f71e931e..b21d923f 100644 --- a/project_templates/02_app_framework/01_enterprise_router_service/modules/user/infrastructure/repository/zz_generated.lokstra.go +++ b/project_templates/02_app_framework/01_enterprise_router_service/modules/user/infrastructure/repository/zz_generated.lokstra.go @@ -1,6 +1,12 @@ // AUTO-GENERATED CODE - DO NOT EDIT // Generated by lokstra-annotation from annotations in this folder -// Annotations: @RouterService, @Service, @Inject, @InjectCfgValue, @Route +// Annotations: @EndpointService, @Service, @Inject, @Route +// +// @Inject supports: +// - "service-name" : Direct service injection +// - "@config.key" : Service name from config value +// - "cfg:config.key" : Config value injection +// - "cfg:@config.key" : Config value via indirection package repository diff --git a/project_templates/02_app_framework/01_enterprise_router_service/register.go b/project_templates/02_app_framework/01_enterprise_router_service/register.go index aa1822b8..15428be8 100644 --- a/project_templates/02_app_framework/01_enterprise_router_service/register.go +++ b/project_templates/02_app_framework/01_enterprise_router_service/register.go @@ -9,7 +9,7 @@ import ( ) func registerRouters() { - // Register manual routers (not generated from @RouterService) + // Register manual routers (not generated from @EndpointService) healthRouter := NewHealthRouter() lokstra_registry.RegisterRouter("health-router", healthRouter) logger.LogInfo("✅ Registered manual router: health-router") diff --git a/project_templates/02_app_framework/01_enterprise_router_service/zz_lokstra_imports.go b/project_templates/02_app_framework/01_enterprise_router_service/zz_lokstra_imports.go index 9e111e56..429c50e6 100644 --- a/project_templates/02_app_framework/01_enterprise_router_service/zz_lokstra_imports.go +++ b/project_templates/02_app_framework/01_enterprise_router_service/zz_lokstra_imports.go @@ -1,6 +1,6 @@ // AUTO-GENERATED CODE - DO NOT EDIT // Generated by lokstra-annotation to auto-register services via init() -// This file imports all packages containing @Service or @RouterService annotations +// This file imports all packages containing @Service or @EndpointService annotations package main diff --git a/project_templates/02_app_framework/03_tenant_management/README.md b/project_templates/02_app_framework/03_tenant_management/README.md index 69a07a5c..fb6610be 100644 --- a/project_templates/02_app_framework/03_tenant_management/README.md +++ b/project_templates/02_app_framework/03_tenant_management/README.md @@ -8,7 +8,7 @@ using Lokstra's **initialization helpers**, **YAML config**, and - `lokstra_init.BootstrapAndRun()` - `config/config.yaml` with `configs`, `dbpool-definitions`, and `servers` -- `@RouterService`, `@Inject`, and `@Route` annotations +- `@EndpointService`, `@Inject`, and `@Route` annotations --- @@ -31,7 +31,7 @@ Application Framework mode (Track 2) with minimal noise. ```text 03_tenant_management/ ├── application/ -│ └── tenant_service.go # @RouterService + @Inject + @Route +│ └── tenant_service.go # @EndpointService + @Inject + @Route ├── config/ │ └── config.yaml # YAML config (configs, dbpool-definitions, servers) ├── domain/ @@ -154,12 +154,12 @@ Key sections: --- -## 🧩 Annotations: `@RouterService`, `@Inject`, `@Route` +## 🧩 Annotations: `@EndpointService`, `@Inject`, `@Route` `application/tenant_service.go`: ```go -// @RouterService name="tenant-service", +// @EndpointService name="tenant-service", // prefix="${api-auth-prefix:/api/auth}/core/tenants", // middlewares=["recovery", "request_logger"] type TenantService struct { @@ -170,7 +170,7 @@ type TenantService struct { What this does: -- **`@RouterService`** +- **`@EndpointService`** - Registers a service named `tenant-service` in the Lokstra registry. - Generates an HTTP router with base path: - `${api-auth-prefix:/api/auth}/core/tenants` @@ -230,7 +230,7 @@ func (s *TenantService) SuspendTenant(...) The **autogenerated** `zz_generated.lokstra.go` file: -- Parses `@RouterService`, `@Inject`, and `@Route` annotations. +- Parses `@EndpointService`, `@Inject`, and `@Route` annotations. - Registers `tenant-service` in the registry. - Generates the HTTP router that: - Binds path/query/body parameters to request DTOs. diff --git a/project_templates/02_app_framework/03_tenant_management/TRANSACTION_GUIDE.md b/project_templates/02_app_framework/03_tenant_management/TRANSACTION_GUIDE.md index 7eae783b..6dcb0d09 100644 --- a/project_templates/02_app_framework/03_tenant_management/TRANSACTION_GUIDE.md +++ b/project_templates/02_app_framework/03_tenant_management/TRANSACTION_GUIDE.md @@ -58,7 +58,7 @@ defer finishTx → COMMIT or ROLLBACK ### Basic Transaction (Recommended Pattern) ```go -// @RouterService name="tenant-service" +// @EndpointService name="tenant-service" type TenantService struct { // @Inject "@store.tenant-store" TenantStore repository.TenantStore diff --git a/project_templates/02_app_framework/03_tenant_management/application/tenant_service.go b/project_templates/02_app_framework/03_tenant_management/application/tenant_service.go index 29eee641..416158e4 100644 --- a/project_templates/02_app_framework/03_tenant_management/application/tenant_service.go +++ b/project_templates/02_app_framework/03_tenant_management/application/tenant_service.go @@ -9,7 +9,7 @@ import ( "github.com/primadi/lokstra/project_templates/02_app_framework/03_tenant_management/repository" ) -// @RouterService name="tenant-service", prefix="${api-auth-prefix:/api/auth}/core/tenants", middlewares=["recovery", "request_logger"] +// @EndpointService name="tenant-service", prefix="${api-auth-prefix:/api/auth}/core/tenants", middlewares=["recovery", "request_logger"] type TenantService struct { // @Inject "@store.tenant-store" TenantStore repository.TenantStore diff --git a/project_templates/02_app_framework/03_tenant_management/application/zz_generated.lokstra.go b/project_templates/02_app_framework/03_tenant_management/application/zz_generated.lokstra.go index 5d755d30..5bc93dfa 100644 --- a/project_templates/02_app_framework/03_tenant_management/application/zz_generated.lokstra.go +++ b/project_templates/02_app_framework/03_tenant_management/application/zz_generated.lokstra.go @@ -1,6 +1,6 @@ // AUTO-GENERATED CODE - DO NOT EDIT // Generated by lokstra-annotation from annotations in this folder -// Annotations: @RouterService, @Service, @Inject, @Route +// Annotations: @EndpointService, @Service, @Inject, @Route // // @Inject supports: // - "service-name" : Direct service injection @@ -13,8 +13,8 @@ package application import ( "github.com/primadi/lokstra/core/deploy" "github.com/primadi/lokstra/core/proxy" - "github.com/primadi/lokstra/lokstra_registry" request "github.com/primadi/lokstra/core/request" + "github.com/primadi/lokstra/lokstra_registry" domain "github.com/primadi/lokstra/project_templates/02_app_framework/03_tenant_management/domain" repository "github.com/primadi/lokstra/project_templates/02_app_framework/03_tenant_management/repository" ) @@ -83,18 +83,17 @@ func (s *TenantServiceRemote) UpdateTenant(p *request.Context) (*domain.Tenant, return proxy.CallWithData[*domain.Tenant](s.proxyService, "UpdateTenant", p) } - func TenantServiceFactory(deps map[string]any, config map[string]any) any { svc := &TenantService{ TenantStore: deps["@store.tenant-store"].(repository.TenantStore), - UserStore: deps["@store.user-store"].(repository.UserStore), + UserStore: deps["@store.user-store"].(repository.UserStore), } - + return svc } // TenantServiceRemoteFactory creates a remote HTTP client for TenantServiceInterface -// Auto-generated from @RouterService annotation +// Auto-generated from @EndpointService annotation func TenantServiceRemoteFactory(deps, config map[string]any) any { proxyService, ok := config["remote"].(*proxy.Service) if !ok { @@ -105,7 +104,7 @@ func TenantServiceRemoteFactory(deps, config map[string]any) any { // RegisterTenantService registers the tenant-service with the registry // Auto-generated from annotations: -// - @RouterService name="tenant-service", prefix="${api-auth-prefix:/api/auth}/core/tenants" +// - @EndpointService name="tenant-service", prefix="${api-auth-prefix:/api/auth}/core/tenants" // - @Inject annotations // - @Route annotations on methods func RegisterTenantService() { @@ -115,7 +114,7 @@ func RegisterTenantService() { TenantServiceRemoteFactory, &deploy.ServiceTypeConfig{ PathPrefix: "${api-auth-prefix:/api/auth}/core/tenants", - Middlewares: []string{ "recovery", "request_logger" }, + Middlewares: []string{"recovery", "request_logger"}, RouteOverrides: map[string]deploy.RouteConfig{ "ActivateTenant": { Path: "POST /{id}/activate", @@ -146,8 +145,6 @@ func RegisterTenantService() { lokstra_registry.RegisterLazyService("tenant-service", "tenant-service-factory", map[string]any{ - "depends-on": []string{ "@store.tenant-store", "@store.user-store", }, + "depends-on": []string{"@store.tenant-store", "@store.user-store"}, }) } - - diff --git a/project_templates/02_app_framework/03_tenant_management/repository/zz_generated.lokstra.go b/project_templates/02_app_framework/03_tenant_management/repository/zz_generated.lokstra.go index 93de03eb..359c23a0 100644 --- a/project_templates/02_app_framework/03_tenant_management/repository/zz_generated.lokstra.go +++ b/project_templates/02_app_framework/03_tenant_management/repository/zz_generated.lokstra.go @@ -1,6 +1,6 @@ // AUTO-GENERATED CODE - DO NOT EDIT // Generated by lokstra-annotation from annotations in this folder -// Annotations: @RouterService, @Service, @Inject, @Route +// Annotations: @EndpointService, @Service, @Inject, @Route // // @Inject supports: // - "service-name" : Direct service injection @@ -34,14 +34,13 @@ func RegisterPostgresTenantStore() { svc := &PostgresTenantStore{ dbPool: deps["db_auth"].(serviceapi.DbPool), } - + return svc }, map[string]any{ - "depends-on": []string{ "db_auth", }, + "depends-on": []string{"db_auth"}, }) } - // ============================================================ // FILE: user_store.go // ============================================================ @@ -55,11 +54,9 @@ func RegisterPostgresUserStore() { svc := &PostgresUserStore{ dbPool: deps["db_auth"].(serviceapi.DbPool), } - + return svc }, map[string]any{ - "depends-on": []string{ "db_auth", }, + "depends-on": []string{"db_auth"}, }) } - - diff --git a/project_templates/02_app_framework/03_tenant_management/zz_lokstra_imports.go b/project_templates/02_app_framework/03_tenant_management/zz_lokstra_imports.go index e26ea60c..4df10384 100644 --- a/project_templates/02_app_framework/03_tenant_management/zz_lokstra_imports.go +++ b/project_templates/02_app_framework/03_tenant_management/zz_lokstra_imports.go @@ -1,6 +1,6 @@ // AUTO-GENERATED CODE - DO NOT EDIT // Generated by lokstra-annotation to auto-register services via init() -// This file imports all packages containing @Service or @RouterService annotations +// This file imports all packages containing @Service or @EndpointService annotations package main diff --git a/project_templates/02_app_framework/README.md b/project_templates/02_app_framework/README.md index c97970fd..6581e0ad 100644 --- a/project_templates/02_app_framework/README.md +++ b/project_templates/02_app_framework/README.md @@ -17,7 +17,7 @@ including **DDD modules**, **annotation-based routers**, and **config-driven dep This template shows how to build a **large, modular application** with: - ✅ **DDD modules** (`modules/user`, `modules/order`, `modules/shared`) -- ✅ `@RouterService` / `@Route` annotations and **generated routers** +- ✅ `@EndpointService` / `@Route` annotations and **generated routers** - ✅ Per-environment deployments (`development` vs `microservice`) via `config/deployment.yaml` - ✅ Custom middleware: `request-logger`, `simple-auth`, `mw-test` - ✅ New bootstrap flow using `lokstra_init.BootstrapAndRun` diff --git a/services/email_smtp/EXAMPLES.md b/services/email_smtp/EXAMPLES.md index 910a3d2e..1061a97c 100644 --- a/services/email_smtp/EXAMPLES.md +++ b/services/email_smtp/EXAMPLES.md @@ -124,7 +124,7 @@ type SendEmailParams struct { Message string `json:"message" validate:"required"` } -// @RouterService name="notification-service", prefix="/api/notifications" +// @EndpointService name="notification-service", prefix="/api/notifications" type NotificationService struct { // @Inject "email-service" EmailSender serviceapi.EmailSender @@ -154,7 +154,7 @@ import ( "github.com/primadi/lokstra/serviceapi" ) -// @RouterService name="user-service", prefix="/api/users" +// @EndpointService name="user-service", prefix="/api/users" type UserService struct { // @Inject "email-service" EmailSender serviceapi.EmailSender @@ -214,7 +214,7 @@ import ( "github.com/primadi/lokstra/serviceapi" ) -// @RouterService name="invoice-service", prefix="/api/invoices" +// @EndpointService name="invoice-service", prefix="/api/invoices" type InvoiceService struct { // @Inject "email-service" EmailSender serviceapi.EmailSender @@ -274,7 +274,7 @@ import ( "github.com/primadi/lokstra/serviceapi" ) -// @RouterService name="newsletter-service", prefix="/api/newsletter" +// @EndpointService name="newsletter-service", prefix="/api/newsletter" type NewsletterService struct { // @Inject "email-service" EmailSender serviceapi.EmailSender @@ -324,7 +324,7 @@ import ( "github.com/primadi/lokstra/serviceapi" ) -// @RouterService name="auth-service", prefix="/api/auth" +// @EndpointService name="auth-service", prefix="/api/auth" type AuthService struct { // @Inject "email-service" EmailSender serviceapi.EmailSender @@ -390,7 +390,7 @@ import ( "github.com/primadi/lokstra/serviceapi" ) -// @RouterService name="template-email-service", prefix="/api/emails" +// @EndpointService name="template-email-service", prefix="/api/emails" type TemplateEmailService struct { // @Inject "email-service" EmailSender serviceapi.EmailSender diff --git a/services/email_smtp/README.md b/services/email_smtp/README.md index 74e76203..f61c42bb 100644 --- a/services/email_smtp/README.md +++ b/services/email_smtp/README.md @@ -76,10 +76,10 @@ func (s *MyService) SendWelcomeEmail(userEmail, userName string) error { } ``` -### With Annotation (@RouterService) +### With Annotation (@EndpointService) ```go -// @RouterService name="notification-service" +// @EndpointService name="notification-service" type NotificationService struct { // @Inject "email-service" EmailSender serviceapi.EmailSender diff --git a/services/email_smtp/example/zz_cache.lokstra.json b/services/email_smtp/example/zz_cache.lokstra.json index 2c3a72e6..ddc9344e 100644 --- a/services/email_smtp/example/zz_cache.lokstra.json +++ b/services/email_smtp/example/zz_cache.lokstra.json @@ -5,13 +5,13 @@ "filename": "email_service.go", "checksum": "716100fc8d6cb71638a21bd2f041dea435a0d2650524674615da6f48ada48d0a", "annotations": 2, - "last_scan": "2025-12-10T00:57:39.636288+07:00", + "last_scan": "2026-01-09T17:59:08.0508214+07:00", "generated": [ "zz_generated.lokstra.go" ], - "generated_mod_time": "2025-12-10T00:57:39.636288+07:00" + "generated_mod_time": "2026-01-09T17:59:08.0508214+07:00" } }, - "updated_at": "2025-12-10T02:21:23.8105112+07:00", - "generated_checksum": "4c13f9c613fb22927021405ebde338c0196d30513178f207b9cd1edcc336cbb9" + "updated_at": "2026-01-09T17:59:08.0508214+07:00", + "generated_checksum": "6adb2d40f4fe532d9a9432b104bc1e6bd3ab10f19f9bca680dd0053d0b3113cb" } \ No newline at end of file diff --git a/services/email_smtp/example/zz_generated.lokstra.go b/services/email_smtp/example/zz_generated.lokstra.go index 20d3e4b2..33b38346 100644 --- a/services/email_smtp/example/zz_generated.lokstra.go +++ b/services/email_smtp/example/zz_generated.lokstra.go @@ -1,6 +1,12 @@ // AUTO-GENERATED CODE - DO NOT EDIT // Generated by lokstra-annotation from annotations in this folder -// Annotations: @RouterService, @Service, @Inject, @InjectCfgValue, @Route +// Annotations: @EndpointService, @Service, @Inject, @Route +// +// @Inject supports: +// - "service-name" : Direct service injection +// - "@config.key" : Service name from config value +// - "cfg:config.key" : Config value injection +// - "cfg:@config.key" : Config value via indirection package main @@ -27,11 +33,9 @@ func RegisterEmailService() { svc := &EmailService{ EmailSender: deps["email_smtp"].(serviceapi.EmailSender), } - + return svc }, map[string]any{ - "depends-on": []string{ "email_smtp", }, + "depends-on": []string{"email_smtp"}, }) } - - diff --git a/services/sync_config_pg/README.md b/services/sync_config_pg/README.md index dd33fc9d..2c796ad2 100644 --- a/services/sync_config_pg/README.md +++ b/services/sync_config_pg/README.md @@ -137,10 +137,10 @@ maxUsers := configService.GetInt(ctx, "max_users", 100) appName := configService.GetString(ctx, "app_name", "MyApp") ``` -### With Annotation (@RouterService) +### With Annotation (@EndpointService) ```go -// @RouterService name="settings-service" +// @EndpointService name="settings-service" type SettingsService struct { // @Inject "config-service" Config serviceapi.SyncConfig