diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index c820ae11..8079058d 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -51,11 +51,15 @@ func main() { ```go // @RouterService name="user-service", prefix="/api/users" type UserService struct { - // @Inject "user-repository" + // @Inject "user-repository" - Direct service injection UserRepo UserRepository -} -// @Route "GET /{id}" + // @Inject "cfg:store.implementation" - Service from config (NEW!) + Store Store // Injected service name from config: store.implementation = "postgres-store" + + // @InjectCfgValue "app.name" - Config value injection + AppName string +}// @Route "GET /{id}" func (s *UserService) GetByID(p *GetUserParams) (*User, error) { return s.UserRepo.GetByID(p.ID) } @@ -89,7 +93,92 @@ go run . --generate-only # Force rebuild all **Per-route middleware:** Add `middlewares=["mw1", "mw2"]` to `@Route` -### 3. Manual Registration (Advanced/Infrastructure Services) +### 3. Interface Injection Pattern (Config-Based Selection) + +**Use case:** Multiple implementations of an interface, selectable via config. + +**Domain interface:** + +```go +// domain/store.go +type Store interface { + GetUser(id string) (*User, error) + SaveUser(user *User) error +} +``` + +**Implementations with @Service:** + +```go +// infrastructure/postgres_store.go +// @Service "postgres-store" +type PostgresStore struct { + // @Inject "db-pool" + DB *sql.DB +} + +var _ Store = (*PostgresStore)(nil) + +func (s *PostgresStore) GetUser(id string) (*User, error) { /* ... */ } + +// infrastructure/mysql_store.go +// @Service "mysql-store" +type MySQLStore struct { + // @Inject "db-pool" + DB *sql.DB +} + +var _ Store = (*MySQLStore)(nil) + +func (s *MySQLStore) GetUser(id string) (*User, error) { /* ... */ } +``` + +**Business service using config-based injection:** + +```go +// application/user_service.go +// @RouterService name="user-service", prefix="/api/users" +type UserService struct { + // @Inject "cfg:store.implementation" + Store Store // Actual service injected based on config! +} + +// @Route "GET /{id}" +func (s *UserService) GetUser(id string) (*User, error) { + return s.Store.GetUser(id) +} +``` + +**config.yaml:** + +```yaml +configs: + store: + implementation: "postgres-store" # Switch to "mysql-store" here! + +service-definitions: + postgres-store: + type: postgres-store + + mysql-store: + type: mysql-store + +deployments: + development: + servers: + api: + addr: ":8080" + published-services: [user-service] +``` + +**Benefits:** + +- Switch implementation by changing ONE line in config +- No code changes needed +- Type-safe (compile-time interface checking) +- Perfect for: database drivers, cache providers, storage backends + +### 4. Manual Registration (Advanced/Infrastructure Services) **For infrastructure services or custom factories:** @@ -135,7 +224,7 @@ deployments: published-services: [user-service] ``` -### 4. Handler Signatures (29+ supported) +### 5. Handler Signatures (29+ supported) ```go // Simple @@ -155,16 +244,7 @@ func(ctx *request.Context, id string, params *UpdateParams) error { } ``` -### 4. Domain Models - -```go -type CreateUserParams struct { - Name string `json:"name" validate:"required,min=3,max=50"` - Email string `json:"email" validate:"required,email"` -} -``` - -### 5. Domain Models +### 6. Domain Models ```go type CreateUserParams struct { diff --git a/bootstrap.go b/bootstrap.go index 08fd306b..b0eba787 100644 --- a/bootstrap.go +++ b/bootstrap.go @@ -260,10 +260,9 @@ func autoCreateDbPoolManager() { if useSync { // Create SyncMaps for tenant and named pools using syncmap package - tenantPools := syncmap.NewSyncMap[*dbpool_manager.DsnSchema]("tenant") - namedPools := syncmap.NewSyncMap[*dbpool_manager.DsnSchema]("pool") + dbPools := syncmap.NewSyncMap[*dbpool_manager.DsnSchema]("db-pools") - pm = dbpool_manager.NewPgxSyncPoolManager(tenantPools, namedPools) + pm = dbpool_manager.NewPgxSyncPoolManager(dbPools) deploy.LogDebug("[Lokstra] DbPoolManager initialized with distributed sync") } else { // Default: use regular pool manager (local sync.Map) diff --git a/core/annotation/codegen.go b/core/annotation/codegen.go index 7686d80a..5214bfe7 100644 --- a/core/annotation/codegen.go +++ b/core/annotation/codegen.go @@ -278,7 +278,7 @@ func processFileForCodeGen(file *FileToProcess, ctx *RouterServiceContext) error return err } - // Find @InjectCfg annotations for config dependencies + // Find @InjectCfgValue annotations for config dependencies if err := extractConfigDependencies(file, service); err != nil { return err } @@ -569,8 +569,10 @@ func extractDependencies(file *FileToProcess, service *ServiceGeneration) error } // Supported formats: - // @Inject "user-repository" - // @Inject service="user-repository" + // @Inject "user-repository" - Direct service injection + // @Inject service="user-repository" - Direct service injection (named param) + // @Inject "cfg:store.implementation" - Service name from config + // @Inject service="cfg:store.implementation" - Service name from config (named param) args, err := ann.ReadArgs("service") if err != nil { return fmt.Errorf("@Inject on line %d: %w", ann.Line, err) @@ -585,10 +587,25 @@ func extractDependencies(file *FileToProcess, service *ServiceGeneration) error // ann.TargetName is field name fieldType := fieldTypes[ann.TargetName] - service.Dependencies[serviceName] = &DependencyInfo{ - ServiceName: serviceName, - FieldName: ann.TargetName, - FieldType: fieldType, + // Check if this is config-based injection (cfg: prefix) + if strings.HasPrefix(serviceName, "cfg:") { + configKey := strings.TrimPrefix(serviceName, "cfg:") + service.Dependencies[configKey] = &DependencyInfo{ + ServiceName: "", // Will be resolved from config at runtime + FieldName: ann.TargetName, + FieldType: fieldType, + IsConfigBased: true, + ConfigKey: configKey, + } + } else { + // Direct service injection (existing behavior) + service.Dependencies[serviceName] = &DependencyInfo{ + ServiceName: serviceName, + FieldName: ann.TargetName, + FieldType: fieldType, + IsConfigBased: false, + ConfigKey: "", + } } } } @@ -650,7 +667,7 @@ func checkInitMethod(file *FileToProcess, service *ServiceGeneration) { } } -// extractConfigDependencies finds all @InjectCfg annotations and field info +// extractConfigDependencies finds all @InjectCfgValue annotations and field info func extractConfigDependencies(file *FileToProcess, service *ServiceGeneration) error { // Parse file to get field types fset := token.NewFileSet() @@ -695,9 +712,9 @@ func extractConfigDependencies(file *FileToProcess, service *ServiceGeneration) } } - // Now process @InjectCfg annotations - ONLY for fields belonging to THIS struct + // Now process @InjectCfgValue annotations - ONLY for fields belonging to THIS struct for _, ann := range file.Annotations { - if ann.Name != "InjectCfg" && ann.Name != "injectcfg" { + if ann.Name != "InjectCfgValue" && ann.Name != "injectcfgvalue" { continue } @@ -707,13 +724,13 @@ func extractConfigDependencies(file *FileToProcess, service *ServiceGeneration) } // Supported formats: - // @InjectCfg "app.jwt-secret" - // @InjectCfg key="app.jwt-secret" - // @InjectCfg key="app.jwt-secret", default="secret" - // @InjectCfg "app.timeout", "30" (positional: key, default) + // @InjectCfgValue "app.jwt-secret" + // @InjectCfgValue key="app.jwt-secret" + // @InjectCfgValue key="app.jwt-secret", default="secret" + // @InjectCfgValue "app.timeout", "30" (positional: key, default) args, err := ann.ReadArgs("key", "default") if err != nil { - return fmt.Errorf("@InjectCfg on line %d: %w", ann.Line, err) + return fmt.Errorf("@InjectCfgValue on line %d: %w", ann.Line, err) } var configKey string @@ -814,12 +831,12 @@ func writeGenFile(path string, ctx *RouterServiceContext, existingImports map[st // Collect used packages from method signatures, dependencies, and struct name usedPackages := make(map[string]bool) for _, service := range ctx.GeneratedCode.Services { - // From method signatures + // From method signatures - ONLY from actual method parameters/returns for _, method := range service.Methods { collectPackagesFromType(method.ParamType, usedPackages) collectPackagesFromType(method.ReturnType, usedPackages) } - // From dependencies + // From dependencies - ONLY injected fields for _, dep := range service.Dependencies { collectPackagesFromType(dep.FieldType, usedPackages) } @@ -829,6 +846,8 @@ func writeGenFile(path string, ctx *RouterServiceContext, existingImports map[st usedPackages["time"] = true } } + // From struct name itself (e.g., if service struct is domain.UserService) + collectPackagesFromType(service.StructName, usedPackages) } // Also collect packages from preserved sections (unchanged files) @@ -854,6 +873,7 @@ func writeGenFile(path string, ctx *RouterServiceContext, existingImports map[st 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) { @@ -941,15 +961,20 @@ func collectPackagesFromType(typeStr string, packages map[string]bool) { return } - // Handle generics first: Type[Param1, Param2] - if start := strings.Index(typeStr, "["); start != -1 { - if end := strings.LastIndex(typeStr, "]"); end > start { + // Remove pointer and array prefixes FIRST before checking for generics + // This handles: []*domain.User, *[]domain.User, etc. + cleanType := strings.TrimLeft(typeStr, "*[]") + + // Handle generics: Type[Param1, Param2] + // Generic brackets appear AFTER the type name, not at the start + if start := strings.Index(cleanType, "["); start != -1 { + if end := strings.LastIndex(cleanType, "]"); end > start { // Process the base type (before '[') - baseType := typeStr[:start] + baseType := cleanType[:start] collectPackagesFromType(baseType, packages) // Process inner type parameters - innerTypes := typeStr[start+1 : end] + innerTypes := cleanType[start+1 : end] // Split by comma for multiple type params for _, inner := range strings.Split(innerTypes, ",") { collectPackagesFromType(strings.TrimSpace(inner), packages) @@ -958,12 +983,9 @@ func collectPackagesFromType(typeStr string, packages map[string]bool) { } } - // Remove pointer and array prefixes - typeStr = strings.TrimLeft(typeStr, "*[]") - // Extract package prefix (everything before last dot) - if idx := strings.LastIndex(typeStr, "."); idx != -1 { - pkg := typeStr[:idx] + if idx := strings.LastIndex(cleanType, "."); idx != -1 { + pkg := cleanType[:idx] // Handle nested packages (e.g., "github.com/user/repo.Type" -> "repo") // Only take the last segment as the alias if lastSlash := strings.LastIndex(pkg, "/"); lastSlash != -1 { @@ -1139,7 +1161,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, @InjectCfg, @Route +// Annotations: @RouterService, @Service, @Inject, @InjectCfgValue, @Route package {{.Package}} @@ -1174,15 +1196,19 @@ func init() { // - @Inject annotations {{- end}} {{- if $service.ConfigDependencies}} -// - @InjectCfg annotations +// - @InjectCfgValue annotations {{- end}} func Register{{$service.StructName}}() { lokstra_registry.RegisterLazyService({{quote $service.ServiceName}}, func(deps map[string]any, cfg map[string]any) any { svc := &{{$service.StructName}}{ {{- range $key := sortedKeys $service.Dependencies }} {{- $dep := index $service.Dependencies $key }} +{{- if $dep.IsConfigBased }} + {{$dep.FieldName}}: deps[cfg[{{quote $dep.ConfigKey}}].(string)].({{$dep.FieldType}}), +{{- else }} {{$dep.FieldName}}: deps[{{quote $dep.ServiceName}}].({{$dep.FieldType}}), {{- end }} +{{- end }} {{- range $key := sortedKeys $service.ConfigDependencies }} {{- $cfg := index $service.ConfigDependencies $key }} {{$cfg.FieldName}}: cfg[{{quote $cfg.ConfigKey}}].({{$cfg.FieldType}}), @@ -1200,7 +1226,13 @@ func Register{{$service.StructName}}() { }, map[string]any{ {{- if or $service.Dependencies $service.ConfigDependencies }} {{- if $service.Dependencies }} - "depends-on": []string{ {{range $key := sortedKeys $service.Dependencies}}{{$dep := index $service.Dependencies $key}}{{quote $dep.ServiceName}}, {{end}}}, + "depends-on": []string{ {{range $key := sortedKeys $service.Dependencies}}{{$dep := index $service.Dependencies $key}}{{if not $dep.IsConfigBased}}{{quote $dep.ServiceName}}, {{end}}{{end}}}, +{{- end }} +{{- range $key := sortedKeys $service.Dependencies }} +{{- $dep := index $service.Dependencies $key }} +{{- if $dep.IsConfigBased }} + {{quote $dep.ConfigKey}}: lokstra_registry.GetConfig({{quote $dep.ConfigKey}}, ""), +{{- end }} {{- end }} {{- range $key := sortedKeys $service.ConfigDependencies }} {{- $cfg := index $service.ConfigDependencies $key }} @@ -1246,8 +1278,12 @@ func {{$service.StructName}}Factory(deps map[string]any, config map[string]any) svc := &{{$service.StructName}}{ {{- range $key := sortedKeys $service.Dependencies }} {{- $dep := index $service.Dependencies $key }} +{{- if $dep.IsConfigBased }} + {{$dep.FieldName}}: deps[config[{{quote $dep.ConfigKey}}].(string)].({{$dep.FieldType}}), +{{- else }} {{$dep.FieldName}}: deps[{{quote $dep.ServiceName}}].({{$dep.FieldType}}), {{- end }} +{{- end }} {{- range $key := sortedKeys $service.ConfigDependencies }} {{- $cfg := index $service.ConfigDependencies $key }} {{$cfg.FieldName}}: config[{{quote $cfg.ConfigKey}}].({{$cfg.FieldType}}), @@ -1281,7 +1317,7 @@ func {{$service.RemoteTypeName}}Factory(deps, config map[string]any) any { // - @Inject annotations {{- end}} {{- if $service.ConfigDependencies}} -// - @InjectCfg annotations +// - @InjectCfgValue annotations {{- end}} // - @Route annotations on methods func Register{{$service.StructName}}() { @@ -1312,7 +1348,13 @@ func Register{{$service.StructName}}() { "{{$service.ServiceName}}-factory", map[string]any{ {{- if $service.Dependencies }} - "depends-on": []string{ {{range $key := sortedKeys $service.Dependencies}}{{$dep := index $service.Dependencies $key}}{{quote $dep.ServiceName}}, {{end}}}, + "depends-on": []string{ {{range $key := sortedKeys $service.Dependencies}}{{$dep := index $service.Dependencies $key}}{{if not $dep.IsConfigBased}}{{quote $dep.ServiceName}}, {{end}}{{end}}}, +{{- end }} +{{- range $key := sortedKeys $service.Dependencies }} +{{- $dep := index $service.Dependencies $key }} +{{- if $dep.IsConfigBased }} + {{quote $dep.ConfigKey}}: lokstra_registry.GetConfig({{quote $dep.ConfigKey}}, ""), +{{- end }} {{- end }} {{- range $key := sortedKeys $service.ConfigDependencies }} {{- $cfg := index $service.ConfigDependencies $key }} diff --git a/core/annotation/complex_processor.go b/core/annotation/complex_processor.go index 6fdbac6b..a97ab3ab 100644 --- a/core/annotation/complex_processor.go +++ b/core/annotation/complex_processor.go @@ -317,7 +317,7 @@ 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 @InjectCfg) + ConfigDependencies map[string]*ConfigInfo // configKey -> config field info (for @InjectCfgValue) Imports map[string]string // alias -> import path (e.g., "domain" -> ".../.../domain") StructName string InterfaceName string @@ -329,12 +329,14 @@ type ServiceGeneration struct { // DependencyInfo holds field injection information type DependencyInfo struct { - ServiceName string // e.g., "user-repository" - FieldName string // e.g., "UserRepo" - FieldType string // e.g., "domain.UserRepository" (interface type) + ServiceName string // e.g., "user-repository" (direct injection) + FieldName string // e.g., "UserRepo" + FieldType string // e.g., "domain.UserRepository" (interface type) + IsConfigBased bool // true if service name comes from config (cfg: prefix) + ConfigKey string // e.g., "store.implementation" (only if IsConfigBased=true) } -// ConfigInfo holds config injection information for @InjectCfg +// ConfigInfo holds config injection information for @InjectCfgValue type ConfigInfo struct { ConfigKey string // e.g., "auth.jwt-secret" FieldName string // e.g., "jwtSecret" @@ -722,8 +724,18 @@ func generateImportFile(startPath string, packages []string) error { } buf.WriteString(")\n") - // Write file - if err := os.WriteFile(importFilePath, buf.Bytes(), 0644); err != nil { + newContent := buf.Bytes() + + // Check if file exists and content is identical (lightweight cache) + if existingContent, err := os.ReadFile(importFilePath); err == nil { + if bytes.Equal(existingContent, newContent) { + // Content identical - skip write to avoid unnecessary file modification + return nil + } + } + + // Write file only if content changed + if err := os.WriteFile(importFilePath, newContent, 0644); err != nil { return fmt.Errorf("failed to write %s: %w", importFilePath, err) } diff --git a/core/annotation/config_injection_test.go b/core/annotation/config_injection_test.go new file mode 100644 index 00000000..868f895a --- /dev/null +++ b/core/annotation/config_injection_test.go @@ -0,0 +1,131 @@ +package annotation_test + +// Example: Config-based service injection with cfg: prefix +// This demonstrates the new @Inject "cfg:..." feature + +// Domain interface +type Store interface { + GetUser(id string) (*User, error) + SaveUser(user *User) error +} + +type User struct { + ID string + Name string +} + +// @Service "postgres-store" +type PostgresStore struct { + // @Inject "db-pool" + DB any +} + +var _ Store = (*PostgresStore)(nil) + +func (s *PostgresStore) GetUser(id string) (*User, error) { + return &User{ID: id, Name: "User from Postgres"}, nil +} + +func (s *PostgresStore) SaveUser(user *User) error { + return nil +} + +// @Service "mysql-store" +type MySQLStore struct { + // @Inject "db-pool" + DB any +} + +var _ Store = (*MySQLStore)(nil) + +func (s *MySQLStore) GetUser(id string) (*User, error) { + return &User{ID: id, Name: "User from MySQL"}, nil +} + +func (s *MySQLStore) SaveUser(user *User) error { + return nil +} + +// @RouterService name="user-service", prefix="/api/users" +type UserService struct { + // Config-based injection - service name from config! + // @Inject "cfg:store.implementation" + Store Store + + // Direct injection (existing behavior) + // @Inject "logger" + Logger any + + // Config value injection (existing @InjectCfgValue) + // @InjectCfgValue "app.name" + AppName string +} + +// @Route "GET /{id}" +func (s *UserService) GetUser(id string) (*User, error) { + return s.Store.GetUser(id) +} + +/* +Expected generated code in zz_generated.lokstra.go: + +func UserServiceFactory(deps map[string]any, config map[string]any) any { + svc := &UserService{ + // Config-based: reads config["store.implementation"] -> "postgres-store" + // Then injects deps["postgres-store"] + Store: deps[config["store.implementation"].(string)].(Store), + + // Direct injection (as before) + Logger: deps["logger"].(any), + + // Config value (as before) + AppName: config["app.name"].(string), + } + return svc +} + +func RegisterUserService() { + lokstra_registry.RegisterRouterServiceType("user-service-factory", + UserServiceFactory, + UserServiceRemoteFactory, + &deploy.ServiceTypeConfig{...}) + + lokstra_registry.RegisterLazyService("user-service", + "user-service-factory", + map[string]any{ + // Only direct dependencies + "depends-on": []string{"logger"}, + + // Config-based dependency resolved at runtime + "store.implementation": lokstra_registry.GetConfig("store.implementation", ""), + + // Config values + "app.name": lokstra_registry.GetConfig("app.name", ""), + }) +} + +config.yaml: +--- +configs: + store: + implementation: "postgres-store" # Change to "mysql-store" to switch! + app: + name: "MyApp" + +service-definitions: + db-pool: + type: db-pool + + postgres-store: + type: postgres-store + + mysql-store: + type: mysql-store + +deployments: + development: + servers: + api: + addr: ":8080" + published-services: [user-service] +*/ diff --git a/core/annotation/test/unused_imports_test.go b/core/annotation/test/unused_imports_test.go new file mode 100644 index 00000000..acd9077e --- /dev/null +++ b/core/annotation/test/unused_imports_test.go @@ -0,0 +1,262 @@ +package annotation_test + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/primadi/lokstra/core/annotation" + "github.com/primadi/lokstra/core/annotation/internal" +) + +// TestUnusedImportsNotIncluded verifies that imports from source file +// that are not used in handler methods/dependencies are not included in generated code +func TestUnusedImportsNotIncluded(t *testing.T) { + tmpDir := t.TempDir() + + // Create a service file with imports that are NOT used in handlers + serviceCode := `package application + +import ( + "github.com/primadi/lokstra-auth/credential/domain" // NOT USED in handlers + "github.com/primadi/lokstra/core/request" // NOT USED in handlers + core_repository "github.com/primadi/lokstra-auth/infrastructure/repository" // USED in dependency +) + +// @RouterService name="test-service", prefix="/api" +type TestService struct { + // @Inject "user-repository" + Repo core_repository.UserRepository +} + +// @Route "GET /users/{id}" +// Returns string, no domain types used +func (s *TestService) GetUser(id string) (string, error) { + // Method doesn't use domain or request packages + return "user-" + id, nil +} + +// @Route "POST /users" +// Takes simple struct, no domain types +func (s *TestService) CreateUser(p *CreateUserParams) (string, error) { + return "created", nil +} + +type CreateUserParams struct { + Name string +} +` + + // Write service file + servicePath := filepath.Join(tmpDir, "test_service.go") + if err := os.WriteFile(servicePath, []byte(serviceCode), 0644); err != nil { + t.Fatalf("Failed to create service file: %v", err) + } + + // Parse annotations + annotations, err := annotation.ParseFileAnnotations(servicePath) + if err != nil { + t.Fatalf("ParseFileAnnotations failed: %v", err) + } + + // Create context + ctx := &annotation.RouterServiceContext{ + FolderPath: tmpDir, + UpdatedFiles: []*annotation.FileToProcess{ + { + Filename: "test_service.go", + FullPath: servicePath, + Annotations: annotations, + }, + }, + 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 + genPath := filepath.Join(tmpDir, internal.GeneratedFileName) + content, err := os.ReadFile(genPath) + if err != nil { + t.Fatalf("Failed to read generated file: %v", err) + } + + genCode := string(content) + + // Verify that unused imports are NOT included + if strings.Contains(genCode, `"github.com/primadi/lokstra-auth/credential/domain"`) { + t.Error("Generated code should NOT import unused package 'domain'") + } + + if strings.Contains(genCode, `"github.com/primadi/lokstra/core/request"`) { + t.Error("Generated code should NOT import unused package 'request'") + } + + // Verify that used import IS included + if !strings.Contains(genCode, `"github.com/primadi/lokstra-auth/infrastructure/repository"`) { + t.Error("Generated code should import used package 'core_repository'") + } + + // Verify core lokstra imports are included + if !strings.Contains(genCode, `"github.com/primadi/lokstra/lokstra_registry"`) { + t.Error("Generated code should import lokstra_registry") + } + + // Should include deploy and proxy for @RouterService + if !strings.Contains(genCode, `"github.com/primadi/lokstra/core/deploy"`) { + t.Error("Generated code should import deploy for @RouterService") + } + + if !strings.Contains(genCode, `"github.com/primadi/lokstra/core/proxy"`) { + t.Error("Generated code should import proxy for @RouterService") + } + + t.Logf("✅ Generated code correctly filtered unused imports") + t.Logf("Generated file:\n%s", genCode) +} + +// TestOnlyMethodTypesIncluded verifies that only types used in method signatures +// are considered when filtering imports +func TestOnlyMethodTypesIncluded(t *testing.T) { + tmpDir := t.TempDir() + + // Create domain package + domainDir := filepath.Join(tmpDir, "domain") + if err := os.MkdirAll(domainDir, 0755); err != nil { + t.Fatalf("Failed to create domain dir: %v", err) + } + + domainCode := `package domain + +type User struct { + ID string + Name string +} +` + if err := os.WriteFile(filepath.Join(domainDir, "models.go"), []byte(domainCode), 0644); err != nil { + t.Fatalf("Failed to create domain models: %v", err) + } + + // Create helper package + helperDir := filepath.Join(tmpDir, "helper") + if err := os.MkdirAll(helperDir, 0755); err != nil { + t.Fatalf("Failed to create helper dir: %v", err) + } + + helperCode := `package helper + +func Query(q string) string { + return "" +} +` + if err := os.WriteFile(filepath.Join(helperDir, "helper.go"), []byte(helperCode), 0644); err != nil { + t.Fatalf("Failed to create helper: %v", err) + } + + // Create application package + appDir := filepath.Join(tmpDir, "application") + if err := os.MkdirAll(appDir, 0755); err != nil { + t.Fatalf("Failed to create application dir: %v", err) + } + + // Create a service file with helper types not used in handlers + serviceCode := `package application + +import ( + "testapp/domain" // Used in handler + "testapp/helper" // NOT used in handler, only in helper method +) + +// @RouterService name="test-service", prefix="/api" +type TestService struct { +} + +// @Route "GET /users" +func (s *TestService) GetUsers() ([]*domain.User, error) { + users := s.fetchFromDB() // Uses helper internally + return users, nil +} + +// Helper method (not a handler) +func (s *TestService) fetchFromDB() []*domain.User { + // This uses helper.Query but this method is not a handler + _ = helper.Query("SELECT * FROM users") + return nil +} +` + + // Write service file + servicePath := filepath.Join(appDir, "test_service.go") + if err := os.WriteFile(servicePath, []byte(serviceCode), 0644); err != nil { + t.Fatalf("Failed to create service file: %v", err) + } + + // Create go.mod for module resolution + goModContent := `module testapp + +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 + annotations, err := annotation.ParseFileAnnotations(servicePath) + if err != nil { + t.Fatalf("ParseFileAnnotations failed: %v", err) + } + + // Create context + ctx := &annotation.RouterServiceContext{ + FolderPath: appDir, + UpdatedFiles: []*annotation.FileToProcess{ + { + Filename: "test_service.go", + FullPath: servicePath, + Annotations: annotations, + }, + }, + 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 + genPath := filepath.Join(appDir, internal.GeneratedFileName) + content, err := os.ReadFile(genPath) + if err != nil { + t.Fatalf("Failed to read generated file: %v", err) + } + + genCode := string(content) + + // Verify that domain (used in handler) IS included + if !strings.Contains(genCode, `"testapp/domain"`) { + t.Error("Generated code should import 'domain' used in handler") + t.Logf("Generated imports:\n%s", genCode[:strings.Index(genCode, "// Auto-register")]) + } + + // Verify that helper (NOT used in handler signature) is NOT included + if strings.Contains(genCode, `"testapp/helper"`) { + t.Error("Generated code should NOT import 'helper' - only used in non-handler methods") + } + + t.Logf("✅ Generated code only includes types from handler signatures") +} diff --git a/docs/00-introduction/examples/annotations/README.md b/docs/00-introduction/examples/annotations/README.md index 336a83f9..1e87fbfd 100644 --- a/docs/00-introduction/examples/annotations/README.md +++ b/docs/00-introduction/examples/annotations/README.md @@ -10,16 +10,16 @@ Pure services without HTTP endpoints, demonstrating: **AuthService:** - ✅ Required dependencies: `@Inject "user-repository"`, `@Inject "cache-service"` -- ✅ String config: `@InjectCfg "auth.jwt-secret"` -- ✅ Duration config: `@InjectCfg key="auth.token-expiry", default="24h"` -- ✅ Int config: `@InjectCfg key="auth.max-attempts", default=5` -- ✅ Bool config: `@InjectCfg key="auth.debug-mode", default=false` +- ✅ 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: `@InjectCfg "smtp.host"` -- ✅ Int config with default: `@InjectCfg key="smtp.port", default=587` -- ✅ String config with default: `@InjectCfg key="smtp.from-email", default="noreply@example.com"` -- ✅ Bool config with default: `@InjectCfg key="notification.enabled", default=true` +- ✅ 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 @@ -37,7 +37,7 @@ HTTP service with routes, demonstrating: Service with post-initialization setup: **CacheManager:** -- ✅ Config injection: `@InjectCfg key="cache.max-size", default=1000` +- ✅ Config injection: `@InjectCfgValue key="cache.max-size", default=1000` - ✅ `Init() error` method called after dependency injection - ✅ Internal state initialization (maps, slices) - ✅ Configuration validation @@ -49,7 +49,7 @@ HTTP service with initialization: **ProductAPIService:** - ✅ Dependency injection: `@Inject "product-repository"` -- ✅ Config injection: `@InjectCfg key="api.products.max-items", default=100` +- ✅ Config injection: `@InjectCfgValue key="api.products.max-items", default=100` - ✅ `Init() error` method for cache setup - ✅ HTTP routes with internal state @@ -203,20 +203,20 @@ Field ServiceType - **All dependencies are mandatory** - framework panics if not found - Generates: `deps["service-name"].(ServiceType)` -### @InjectCfg +### @InjectCfgValue ```go // Required config -// @InjectCfg "config.key" +// @InjectCfgValue "config.key" Field string // With default (unquoted for non-string types) -// @InjectCfg key="config.key", default=100 +// @InjectCfgValue key="config.key", default=100 Field int -// @InjectCfg key="config.key", default=true +// @InjectCfgValue key="config.key", default=true Field bool -// @InjectCfg key="config.key", default="24h" +// @InjectCfgValue key="config.key", default="24h" Field time.Duration ``` - Injects configuration from `config.yaml` @@ -241,7 +241,7 @@ func (s *Service) Method(p *Params) (*Result, error) ```go // @Service name="my-service" type MyService struct { - // @InjectCfg key="max-size", default=100 + // @InjectCfgValue key="max-size", default=100 MaxSize int // Internal state (not injected) @@ -317,7 +317,7 @@ func (s *MyService) Init() error { ```go // @Service name="email-service" type EmailService struct { - // @InjectCfg "smtp.host" + // @InjectCfgValue "smtp.host" SMTPHost string } ``` @@ -329,7 +329,7 @@ type AuthService struct { // @Inject "user-repository" UserRepo UserRepository - // @InjectCfg "auth.jwt-secret" + // @InjectCfgValue "auth.jwt-secret" JwtSecret string } ``` @@ -338,7 +338,7 @@ type AuthService struct { ```go // @Service name="cache-manager" type CacheManager struct { - // @InjectCfg key="cache.max-size", default=1000 + // @InjectCfgValue key="cache.max-size", default=1000 MaxSize int cache map[string]any @@ -477,14 +477,14 @@ Field ServiceType // nil if not found - Injects service dependencies - Works with both `@Service` and `@RouterService` -### @InjectCfg +### @InjectCfgValue ```go // Required -// @InjectCfg "config.key" +// @InjectCfgValue "config.key" Field string // With default -// @InjectCfg key="config.key", default="value" +// @InjectCfgValue key="config.key", default="value" Field string ``` - Injects configuration from `config.yaml` diff --git a/docs/00-introduction/examples/annotations/config.example.yaml b/docs/00-introduction/examples/annotations/config.example.yaml index 098749f2..e27ae583 100644 --- a/docs/00-introduction/examples/annotations/config.example.yaml +++ b/docs/00-introduction/examples/annotations/config.example.yaml @@ -1,4 +1,4 @@ -# Example configuration for @Service and @RouterService with @InjectCfg +# Example configuration for @Service and @RouterService with @InjectCfgValue # Configuration for @Service examples (service_example.go) configs: diff --git a/docs/00-introduction/examples/annotations/init_example.go b/docs/00-introduction/examples/annotations/init_example.go index 881d7d2a..faf26838 100644 --- a/docs/00-introduction/examples/annotations/init_example.go +++ b/docs/00-introduction/examples/annotations/init_example.go @@ -9,10 +9,10 @@ import ( // @Service name="cache-manager" type CacheManager struct { - // @InjectCfg key="cache.max-size", default=1000 + // @InjectCfgValue key="cache.max-size", default=1000 MaxSize int - // @InjectCfg key="cache.ttl-seconds", default=300 + // @InjectCfgValue key="cache.ttl-seconds", default=300 TTLSeconds int // Internal state (not injected) diff --git a/docs/00-introduction/examples/annotations/mixed_services_example.go b/docs/00-introduction/examples/annotations/mixed_services_example.go index 76e24494..c373d74b 100644 --- a/docs/00-introduction/examples/annotations/mixed_services_example.go +++ b/docs/00-introduction/examples/annotations/mixed_services_example.go @@ -12,19 +12,19 @@ import ( // @Service name="email-service" type EmailService struct { - // @InjectCfg key="smtp.host" + // @InjectCfgValue key="smtp.host" SMTPHost string - // @InjectCfg key="smtp.port", default=587 + // @InjectCfgValue key="smtp.port", default=587 SMTPPort int - // @InjectCfg key="smtp.username" + // @InjectCfgValue key="smtp.username" SMTPUsername string - // @InjectCfg key="smtp.password" + // @InjectCfgValue key="smtp.password" SMTPPassword string - // @InjectCfg key="email.from", default="noreply@example.com" + // @InjectCfgValue key="email.from", default="noreply@example.com" FromEmail string } @@ -43,13 +43,13 @@ type BackgroundJobService struct { // @Inject "email-service" EmailService *EmailService - // @InjectCfg key="jobs.max-workers", default=10 + // @InjectCfgValue key="jobs.max-workers", default=10 MaxWorkers int - // @InjectCfg key="jobs.retry-limit", default=3 + // @InjectCfgValue key="jobs.retry-limit", default=3 RetryLimit int - // @InjectCfg key="jobs.retry-delay", default="5s" + // @InjectCfgValue key="jobs.retry-delay", default="5s" RetryDelay time.Duration } @@ -70,10 +70,10 @@ type AdminAPIService struct { // @Inject service="email-service" EmailService *EmailService - // @InjectCfg key="admin.allow-job-restart", default=true + // @InjectCfgValue key="admin.allow-job-restart", default=true AllowJobRestart bool - // @InjectCfg key="admin.max-jobs-per-page", default=50 + // @InjectCfgValue key="admin.max-jobs-per-page", default=50 MaxJobsPerPage int } 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 index fc28d3e7..950cb3d5 100644 --- a/docs/00-introduction/examples/annotations/router_service_with_config_example.go +++ b/docs/00-introduction/examples/annotations/router_service_with_config_example.go @@ -42,7 +42,7 @@ type UserListResponse struct { Metadata map[string]any `json:"metadata,omitempty"` } -// Example: @RouterService with @Inject (optional), @InjectCfg, and @Route +// Example: @RouterService with @Inject (optional), @InjectCfgValue, and @Route // @RouterService name="user-api-service", prefix="/api/v1/users", middlewares=["recovery", "request-logger"] type UserAPIService struct { @@ -55,31 +55,31 @@ type UserAPIService struct { Cache CacheService // Configuration: API rate limiting - // @InjectCfg key="api.rate-limit.enabled", default=true + // @InjectCfgValue key="api.rate-limit.enabled", default=true RateLimitEnabled bool - // @InjectCfg key="api.rate-limit.max-requests", default=100 + // @InjectCfgValue key="api.rate-limit.max-requests", default=100 MaxRequests int - // @InjectCfg "api.rate-limit.window", "1m" + // @InjectCfgValue "api.rate-limit.window", "1m" RateLimitWindow time.Duration // Configuration: Pagination - // @InjectCfg key="api.pagination.default-page-size", default="20" + // @InjectCfgValue key="api.pagination.default-page-size", default="20" DefaultPageSize int - // @InjectCfg key="api.pagination.max-page-size", default="100" + // @InjectCfgValue key="api.pagination.max-page-size", default="100" MaxPageSize int // Configuration: Response - // @InjectCfg key="api.response.include-metadata", default="true" + // @InjectCfgValue key="api.response.include-metadata", default="true" IncludeMetadata bool // Configuration: Authentication - // @InjectCfg "api.jwt-secret" + // @InjectCfgValue "api.jwt-secret" JwtSecret string - // @InjectCfg key="api.token-expiry", default="24h" + // @InjectCfgValue key="api.token-expiry", default="24h" TokenExpiry time.Duration } diff --git a/docs/00-introduction/examples/annotations/router_with_init_example.go b/docs/00-introduction/examples/annotations/router_with_init_example.go index 78e01595..68b69bdf 100644 --- a/docs/00-introduction/examples/annotations/router_with_init_example.go +++ b/docs/00-introduction/examples/annotations/router_with_init_example.go @@ -12,7 +12,7 @@ type ProductAPIService struct { // @Inject "product-repository" ProductRepo ProductRepository - // @InjectCfg key="api.products.max-items", default=100 + // @InjectCfgValue key="api.products.max-items", default=100 MaxItems int // Internal cache (initialized in Init()) diff --git a/docs/00-introduction/examples/annotations/service_example.go b/docs/00-introduction/examples/annotations/service_example.go index 2b082856..ea3d91c4 100644 --- a/docs/00-introduction/examples/annotations/service_example.go +++ b/docs/00-introduction/examples/annotations/service_example.go @@ -4,7 +4,7 @@ import ( "time" ) -// Example: @Service annotation with @Inject and @InjectCfg +// Example: @Service annotation with @Inject and @InjectCfgValue // @Service name="auth-service" type AuthService struct { @@ -14,16 +14,16 @@ type AuthService struct { // @Inject "cache-service" Cache CacheService - // @InjectCfg "auth.jwt-secret" + // @InjectCfgValue "auth.jwt-secret" JwtSecret string - // @InjectCfg key="auth.token-expiry", default="24h" + // @InjectCfgValue key="auth.token-expiry", default="24h" TokenExpiry time.Duration - // @InjectCfg key="auth.max-attempts", default=5 + // @InjectCfgValue key="auth.max-attempts", default=5 MaxAttempts int - // @InjectCfg key="auth.debug-mode", default="false" + // @InjectCfgValue key="auth.debug-mode", default="false" DebugMode bool } @@ -64,16 +64,16 @@ func (s *AuthService) Login(email, password string) (string, error) { // @Service name="notification-service" type NotificationService struct { - // @InjectCfg "smtp.host" + // @InjectCfgValue "smtp.host" SMTPHost string - // @InjectCfg key="smtp.port", default="587" + // @InjectCfgValue key="smtp.port", default="587" SMTPPort int - // @InjectCfg key="smtp.from-email", default="noreply@example.com" + // @InjectCfgValue key="smtp.from-email", default="noreply@example.com" FromEmail string - // @InjectCfg key="notification.enabled", default="true" + // @InjectCfgValue key="notification.enabled", default="true" Enabled bool } diff --git a/docs/00-introduction/examples/annotations/zz_generated.lokstra.go b/docs/00-introduction/examples/annotations/zz_generated.lokstra.go index 72e1fd44..269ae504 100644 --- a/docs/00-introduction/examples/annotations/zz_generated.lokstra.go +++ b/docs/00-introduction/examples/annotations/zz_generated.lokstra.go @@ -1,14 +1,15 @@ // AUTO-GENERATED CODE - DO NOT EDIT // Generated by lokstra-annotation from annotations in this folder -// Annotations: @RouterService, @Service, @Inject, @InjectCfg, @Route +// 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" - time "time" ) // Auto-register on package import @@ -52,15 +53,14 @@ func (s *AdminAPIServiceRemote) RestartJob(p *RestartJobRequest) (*JobResponse, 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), + 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), + MaxJobsPerPage: config["admin.max-jobs-per-page"].(int), } - + return svc } @@ -78,7 +78,7 @@ func AdminAPIServiceRemoteFactory(deps, config map[string]any) any { // Auto-generated from annotations: // - @RouterService name="admin-api-service", prefix="/api/admin" // - @Inject annotations -// - @InjectCfg annotations +// - @InjectCfgValue annotations // - @Route annotations on methods func RegisterAdminAPIService() { // Register service type with router configuration @@ -87,7 +87,7 @@ func RegisterAdminAPIService() { AdminAPIServiceRemoteFactory, &deploy.ServiceTypeConfig{ PathPrefix: "/api/admin", - Middlewares: []string{ "recovery", "auth", "admin" }, + Middlewares: []string{"recovery", "auth", "admin"}, RouteOverrides: map[string]deploy.RouteConfig{ "ListJobs": { Path: "GET /jobs", @@ -103,13 +103,12 @@ func RegisterAdminAPIService() { lokstra_registry.RegisterLazyService("admin-api-service", "admin-api-service-factory", map[string]any{ - "depends-on": []string{ "background-job-service", "email-service", }, + "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 // ============================================================ @@ -118,29 +117,28 @@ func RegisterAdminAPIService() { // Auto-generated from annotations: // - @Service name="auth-service" // - @Inject annotations -// - @InjectCfg 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), + 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", ""), + "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 // ============================================================ @@ -149,26 +147,25 @@ func RegisterAuthService() { // Auto-generated from annotations: // - @Service name="background-job-service" // - @Inject annotations -// - @InjectCfg 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), + 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", }, + "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 // ============================================================ @@ -176,27 +173,26 @@ func RegisterBackgroundJobService() { // RegisterCacheManager registers the cache-manager with the registry // Auto-generated from annotations: // - @Service name="cache-manager" -// - @InjectCfg annotations +// - @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), + 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.max-size": lokstra_registry.GetConfig("cache.max-size", 1000), "cache.ttl-seconds": lokstra_registry.GetConfig("cache.ttl-seconds", 300), }) } - // ============================================================ // FILE: mixed_services_example.go // ============================================================ @@ -204,28 +200,27 @@ func RegisterCacheManager() { // RegisterEmailService registers the email-service with the registry // Auto-generated from annotations: // - @Service name="email-service" -// - @InjectCfg annotations +// - @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), + FromEmail: cfg["email.from"].(string), + SMTPHost: cfg["smtp.host"].(string), SMTPPassword: cfg["smtp.password"].(string), - SMTPPort: cfg["smtp.port"].(int), + 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", ""), + "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.port": lokstra_registry.GetConfig("smtp.port", 587), "smtp.username": lokstra_registry.GetConfig("smtp.username", ""), }) } - // ============================================================ // FILE: service_example.go // ============================================================ @@ -233,26 +228,25 @@ func RegisterEmailService() { // RegisterNotificationService registers the notification-service with the registry // Auto-generated from annotations: // - @Service name="notification-service" -// - @InjectCfg annotations +// - @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), + Enabled: cfg["notification.enabled"].(bool), FromEmail: cfg["smtp.from-email"].(string), - SMTPHost: cfg["smtp.host"].(string), - SMTPPort: cfg["smtp.port"].(int), + 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), + "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 // ============================================================ @@ -282,18 +276,17 @@ func (s *ProductAPIServiceRemote) ListProducts(p *ListProductsRequest) (*Product 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), + 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 } @@ -311,7 +304,7 @@ func ProductAPIServiceRemoteFactory(deps, config map[string]any) any { // Auto-generated from annotations: // - @RouterService name="product-api", prefix="/api/products" // - @Inject annotations -// - @InjectCfg annotations +// - @InjectCfgValue annotations // - @Route annotations on methods func RegisterProductAPIService() { // Register service type with router configuration @@ -320,7 +313,7 @@ func RegisterProductAPIService() { ProductAPIServiceRemoteFactory, &deploy.ServiceTypeConfig{ PathPrefix: "/api/products", - Middlewares: []string{ }, + Middlewares: []string{}, RouteOverrides: map[string]deploy.RouteConfig{ "GetProduct": { Path: "GET /{id}", @@ -336,12 +329,11 @@ func RegisterProductAPIService() { lokstra_registry.RegisterLazyService("product-api", "product-api-factory", map[string]any{ - "depends-on": []string{ "product-repository", }, + "depends-on": []string{"product-repository"}, "api.products.max-items": lokstra_registry.GetConfig("api.products.max-items", 100), }) } - // ============================================================ // FILE: router_service_with_config_example.go // ============================================================ @@ -389,21 +381,20 @@ func (s *UserAPIServiceRemote) Update(p *UpdateUserRequest) (*UserResponse, erro 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), + 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), + 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 } @@ -421,7 +412,7 @@ func UserAPIServiceRemoteFactory(deps, config map[string]any) any { // Auto-generated from annotations: // - @RouterService name="user-api-service", prefix="/api/v1/users" // - @Inject annotations -// - @InjectCfg annotations +// - @InjectCfgValue annotations // - @Route annotations on methods func RegisterUserAPIService() { // Register service type with router configuration @@ -430,15 +421,15 @@ func RegisterUserAPIService() { UserAPIServiceRemoteFactory, &deploy.ServiceTypeConfig{ PathPrefix: "/api/v1/users", - Middlewares: []string{ "recovery", "request-logger" }, + Middlewares: []string{"recovery", "request-logger"}, RouteOverrides: map[string]deploy.RouteConfig{ "Create": { - Path: "POST /", - Middlewares: []string{ "auth" }, + Path: "POST /", + Middlewares: []string{"auth"}, }, "Delete": { - Path: "DELETE /{id}", - Middlewares: []string{ "auth", "admin" }, + Path: "DELETE /{id}", + Middlewares: []string{"auth", "admin"}, }, "GetByID": { Path: "GET /{id}", @@ -447,8 +438,8 @@ func RegisterUserAPIService() { Path: "GET /", }, "Update": { - Path: "PUT /{id}", - Middlewares: []string{ "auth" }, + Path: "PUT /{id}", + Middlewares: []string{"auth"}, }, }, }, @@ -458,16 +449,14 @@ func RegisterUserAPIService() { 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", ""), + "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), + "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/02-framework-guide/06-service-annotation.md b/docs/02-framework-guide/06-service-annotation.md index 127614a4..330f40cc 100644 --- a/docs/02-framework-guide/06-service-annotation.md +++ b/docs/02-framework-guide/06-service-annotation.md @@ -91,16 +91,16 @@ func RegisterAuthService() { } ``` -### 3. Configuration Injection with @InjectCfg +### 3. Configuration Injection with @InjectCfgValue **Basic config:** ```go // @Service name="auth-service" type AuthService struct { - // @InjectCfg "auth.jwt-secret" + // @InjectCfgValue "auth.jwt-secret" JwtSecret string - // @InjectCfg key="auth.token-expiry" + // @InjectCfgValue key="auth.token-expiry" TokenExpiry time.Duration } ``` @@ -109,13 +109,13 @@ type AuthService struct { ```go // @Service name="email-service" type EmailService struct { - // @InjectCfg key="smtp.host", default="localhost" + // @InjectCfgValue key="smtp.host", default="localhost" SMTPHost string - // @InjectCfg key="smtp.port", default="587" + // @InjectCfgValue key="smtp.port", default="587" SMTPPort int - // @InjectCfg key="smtp.enabled", default="true" + // @InjectCfgValue key="smtp.enabled", default="true" Enabled bool } ``` @@ -163,17 +163,17 @@ type AuthService struct { Cache domain.CacheService // Required config (no default) - // @InjectCfg "auth.jwt-secret" + // @InjectCfgValue "auth.jwt-secret" JwtSecret string // Config with defaults - // @InjectCfg key="auth.token-expiry", default="24h" + // @InjectCfgValue key="auth.token-expiry", default="24h" TokenExpiry time.Duration - // @InjectCfg key="auth.max-attempts", default="5" + // @InjectCfgValue key="auth.max-attempts", default="5" MaxAttempts int - // @InjectCfg key="auth.debug-mode", default="false" + // @InjectCfgValue key="auth.debug-mode", default="false" DebugMode bool } @@ -248,7 +248,7 @@ func init() { // Auto-generated from annotations: // - @Service name="auth-service" // - @Inject annotations -// - @InjectCfg annotations +// - @InjectCfgValue annotations func RegisterAuthService() { lokstra_registry.RegisterLazyService("auth-service", func(deps map[string]any, cfg map[string]any) any { return &AuthService{ @@ -320,10 +320,10 @@ type PaymentProcessor struct { ```go // @Service name="sms-service" type SMSService struct { - // @InjectCfg key="sms.api-key" + // @InjectCfgValue key="sms.api-key" APIKey string - // @InjectCfg key="sms.endpoint", default="https://api.sms.com" + // @InjectCfgValue key="sms.endpoint", default="https://api.sms.com" Endpoint string } ``` @@ -372,13 +372,13 @@ func (s *UserService) GetUser(id string) (*User, error) { ```go // @Service name="config-service" type ConfigService struct { - // @InjectCfg key="server.port", default="8080" + // @InjectCfgValue key="server.port", default="8080" Port int // Auto-uses GetConfigInt - // @InjectCfg key="cache.ttl", default="5m" + // @InjectCfgValue key="cache.ttl", default="5m" CacheTTL time.Duration // Auto-uses GetConfigDuration - // @InjectCfg key="debug", default="false" + // @InjectCfgValue key="debug", default="false" Debug bool // Auto-uses GetConfigBool } ``` @@ -389,7 +389,7 @@ type ConfigService struct { |---------|----------|----------------| | HTTP Routes | ❌ No | ✅ Yes (@Route) | | Dependency Injection | ✅ @Inject | ✅ @Inject | -| Config Injection | ✅ @InjectCfg | ✅ @InjectCfg | +| Config Injection | ✅ @InjectCfgValue | ✅ @InjectCfgValue | | Optional Dependencies | ✅ Yes | ✅ Yes | | Use Case | Business logic, utilities | HTTP controllers | | Generated Code | `RegisterLazyService` | `RegisterRouterServiceType` | @@ -421,5 +421,5 @@ go run . --generate-only - [@RouterService](05-router-service-annotation.md) - For HTTP endpoints - [@Inject](07-inject-annotation.md) - Dependency injection details -- [@InjectCfg](08-inject-cfg-annotation.md) - Configuration injection +- [@InjectCfgValue](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 ee3efa98..a82d474d 100644 --- a/docs/02-framework-guide/07-inject-annotation.md +++ b/docs/02-framework-guide/07-inject-annotation.md @@ -406,5 +406,5 @@ type B struct { - [@Service](06-service-annotation.md) - Service registration - [@RouterService](05-router-service-annotation.md) - HTTP services -- [@InjectCfg](08-inject-cfg-annotation.md) - Configuration injection +- [@InjectCfgValue](08-inject-cfg-annotation.md) - Configuration injection - [Service Registry](09-service-registry.md) - Manual service registration diff --git a/docs/02-framework-guide/08-inject-cfg-annotation.md b/docs/02-framework-guide/08-inject-cfg-annotation.md index 2b89c34d..e468afc5 100644 --- a/docs/02-framework-guide/08-inject-cfg-annotation.md +++ b/docs/02-framework-guide/08-inject-cfg-annotation.md @@ -1,24 +1,24 @@ --- layout: default -title: "@InjectCfg Annotation" +title: "@InjectCfgValue Annotation" parent: Framework Guide nav_order: 8 --- -# @InjectCfg Annotation +# @InjectCfgValue Annotation ## Overview -The `@InjectCfg` 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 `@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. ## Basic Syntax ```go -// @InjectCfg "config.key" +// @InjectCfgValue "config.key" FieldName FieldType // or with default value -// @InjectCfg key="config.key", default="value" +// @InjectCfgValue key="config.key", default="value" FieldName FieldType ``` @@ -27,20 +27,20 @@ FieldName FieldType ### 1. Positional Arguments ```go -// @InjectCfg "smtp.host" +// @InjectCfgValue "smtp.host" SMTPHost string -// @InjectCfg "smtp.host", "localhost" +// @InjectCfgValue "smtp.host", "localhost" SMTPHost string ``` ### 2. Named Arguments ```go -// @InjectCfg key="smtp.host" +// @InjectCfgValue key="smtp.host" SMTPHost string -// @InjectCfg key="smtp.host", default="localhost" +// @InjectCfgValue key="smtp.host", default="localhost" SMTPHost string ``` @@ -65,11 +65,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 - // @InjectCfg "smtp.host" + // @InjectCfgValue "smtp.host" SMTPHost string // With default - // @InjectCfg key="smtp.from", default="noreply@example.com" + // @InjectCfgValue key="smtp.from", default="noreply@example.com" FromEmail string } ``` @@ -93,10 +93,10 @@ configs: ```go // @Service name="rate-limiter" type RateLimiter struct { - // @InjectCfg key="rate.max-requests", default="100" + // @InjectCfgValue key="rate.max-requests", default="100" MaxRequests int - // @InjectCfg key="rate.window-seconds", default="60" + // @InjectCfgValue key="rate.window-seconds", default="60" WindowSeconds int64 } ``` @@ -112,10 +112,10 @@ WindowSeconds: lokstra_registry.GetConfigInt("rate.window-seconds", 60), ```go // @Service name="feature-flags" type FeatureFlags struct { - // @InjectCfg key="features.new-ui", default="false" + // @InjectCfgValue key="features.new-ui", default="false" EnableNewUI bool - // @InjectCfg key="features.debug-mode", default="false" + // @InjectCfgValue key="features.debug-mode", default="false" DebugMode bool } ``` @@ -131,10 +131,10 @@ DebugMode: lokstra_registry.GetConfigBool("features.debug-mode", false), ```go // @Service name="cache-service" type CacheService struct { - // @InjectCfg key="cache.ttl", default="5m" + // @InjectCfgValue key="cache.ttl", default="5m" TTL time.Duration - // @InjectCfg key="cache.cleanup-interval", default="1h" + // @InjectCfgValue key="cache.cleanup-interval", default="1h" CleanupInterval time.Duration } ``` @@ -150,10 +150,10 @@ CleanupInterval: lokstra_registry.GetConfigDuration("cache.cleanup-interval", 1* ```go // @Service name="payment-service" type PaymentService struct { - // @InjectCfg key="payment.fee-percentage", default="2.5" + // @InjectCfgValue key="payment.fee-percentage", default="2.5" FeePercentage float64 - // @InjectCfg key="payment.min-amount", default="10.0" + // @InjectCfgValue key="payment.min-amount", default="10.0" MinAmount float32 } ``` @@ -176,35 +176,35 @@ import "time" // @Service name="app-config" type AppConfig struct { // String configs - // @InjectCfg key="app.name", default="MyApp" + // @InjectCfgValue key="app.name", default="MyApp" AppName string - // @InjectCfg "app.version" + // @InjectCfgValue "app.version" Version string // Required, no default // Integer configs - // @InjectCfg key="server.port", default="8080" + // @InjectCfgValue key="server.port", default="8080" ServerPort int - // @InjectCfg key="server.max-connections", default="1000" + // @InjectCfgValue key="server.max-connections", default="1000" MaxConnections int64 // Boolean configs - // @InjectCfg key="server.enable-gzip", default="true" + // @InjectCfgValue key="server.enable-gzip", default="true" EnableGzip bool - // @InjectCfg key="server.debug", default="false" + // @InjectCfgValue key="server.debug", default="false" Debug bool // Duration configs - // @InjectCfg key="server.read-timeout", default="30s" + // @InjectCfgValue key="server.read-timeout", default="30s" ReadTimeout time.Duration - // @InjectCfg key="server.write-timeout", default="30s" + // @InjectCfgValue key="server.write-timeout", default="30s" WriteTimeout time.Duration // Float configs - // @InjectCfg key="cache.eviction-ratio", default="0.1" + // @InjectCfgValue key="cache.eviction-ratio", default="0.1" CacheEvictionRatio float64 } @@ -262,7 +262,7 @@ func RegisterAppConfig() { If config key is missing in `config.yaml`, uses the default: ```go -// @InjectCfg key="smtp.host", default="localhost" +// @InjectCfgValue key="smtp.host", default="localhost" SMTPHost string // "localhost" if not in config ``` @@ -271,13 +271,13 @@ SMTPHost string // "localhost" if not in config If config key is missing, uses type's zero value: ```go -// @InjectCfg "smtp.host" +// @InjectCfgValue "smtp.host" SMTPHost string // "" if not in config -// @InjectCfg "server.port" +// @InjectCfgValue "server.port" Port int // 0 if not in config -// @InjectCfg "debug" +// @InjectCfgValue "debug" Debug bool // false if not in config ``` @@ -287,19 +287,19 @@ Debug bool // false if not in config ✅ **Good:** ```go -// @InjectCfg key="database.connection-timeout", default="30s" +// @InjectCfgValue key="database.connection-timeout", default="30s" DBTimeout time.Duration -// @InjectCfg key="auth.jwt-secret" +// @InjectCfgValue key="auth.jwt-secret" JWTSecret string ``` ❌ **Bad:** ```go -// @InjectCfg "timeout" // Too vague +// @InjectCfgValue "timeout" // Too vague Timeout time.Duration -// @InjectCfg "secret" // Not descriptive +// @InjectCfgValue "secret" // Not descriptive Secret string ``` @@ -307,10 +307,10 @@ Secret string ✅ **Good:** ```go -// @InjectCfg key="server.port", default="8080" +// @InjectCfgValue key="server.port", default="8080" Port int -// @InjectCfg key="cache.ttl", default="5m" +// @InjectCfgValue key="cache.ttl", default="5m" CacheTTL time.Duration ``` @@ -339,13 +339,13 @@ configs: ```go // @Service name="db-service" type DBService struct { - // @InjectCfg key="database.host", default="localhost" + // @InjectCfgValue key="database.host", default="localhost" Host string - // @InjectCfg key="database.port", default="5432" + // @InjectCfgValue key="database.port", default="5432" Port int - // @InjectCfg key="database.timeout", default="30s" + // @InjectCfgValue key="database.timeout", default="30s" Timeout time.Duration } ``` @@ -356,16 +356,16 @@ type DBService struct { // @Service name="payment-service" type PaymentService struct { // REQUIRED - no default - // @InjectCfg "payment.api-key" + // @InjectCfgValue "payment.api-key" APIKey string // Optional - has default - // @InjectCfg key="payment.timeout", default="60s" + // @InjectCfgValue key="payment.timeout", default="60s" Timeout time.Duration } ``` -## Combining @Inject and @InjectCfg +## Combining @Inject and @InjectCfgValue ```go // @Service name="notification-service" @@ -378,13 +378,13 @@ type NotificationService struct { EmailSvc EmailService // Configuration - // @InjectCfg key="notifications.enabled", default="true" + // @InjectCfgValue key="notifications.enabled", default="true" Enabled bool - // @InjectCfg key="notifications.batch-size", default="100" + // @InjectCfgValue key="notifications.batch-size", default="100" BatchSize int - // @InjectCfg key="notifications.retry-attempts", default="3" + // @InjectCfgValue key="notifications.retry-attempts", default="3" RetryAttempts int } diff --git a/docs/AI-AGENT-GUIDE.md b/docs/AI-AGENT-GUIDE.md index 13ff5ae1..213cd5e6 100644 --- a/docs/AI-AGENT-GUIDE.md +++ b/docs/AI-AGENT-GUIDE.md @@ -880,16 +880,16 @@ type AuthService struct { Cache domain.CacheService // Configuration injection (type-safe) - // @InjectCfg "auth.jwt-secret" + // @InjectCfgValue "auth.jwt-secret" JwtSecret string - // @InjectCfg key="auth.token-expiry", default="24h" + // @InjectCfgValue key="auth.token-expiry", default="24h" TokenExpiry time.Duration - // @InjectCfg key="auth.max-attempts", default="5" + // @InjectCfgValue key="auth.max-attempts", default="5" MaxAttempts int - // @InjectCfg key="auth.debug-mode", default="false" + // @InjectCfgValue key="auth.debug-mode", default="false" DebugMode bool } @@ -955,7 +955,7 @@ go run . --generate-only | `@RouterService` | HTTP service with routes | `@RouterService 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` | -| `@InjectCfg` | Config injection | `@InjectCfg "jwt-secret"` or `@InjectCfg key="timeout", default="30s"` | +| `@InjectCfgValue` | Config injection | `@InjectCfgValue "jwt-secret"` or `@InjectCfgValue key="timeout", default="30s"` | | `@Route` | HTTP route mapping | `@Route "GET /users/{id}"` | **@RouterService Parameters:** @@ -971,7 +971,7 @@ go run . --generate-only - `service`: Service name (required, positional or named) - `optional`: Boolean, default `false` - set to `true` for optional dependencies -**@InjectCfg Parameters:** +**@InjectCfgValue Parameters:** - `key`: Config key (required, positional or named) - `default`: Default value (optional) - Type auto-detected: `string`, `int`, `bool`, `float64`, `time.Duration` diff --git a/docs/QUICK-REFERENCE.md b/docs/QUICK-REFERENCE.md index b0b2f669..85fdbd5b 100644 --- a/docs/QUICK-REFERENCE.md +++ b/docs/QUICK-REFERENCE.md @@ -178,13 +178,13 @@ type AuthService struct { Cache CacheService // Configuration injection - // @InjectCfg "auth.jwt-secret" + // @InjectCfgValue "auth.jwt-secret" JwtSecret string - // @InjectCfg key="auth.token-expiry", default="24h" + // @InjectCfgValue key="auth.token-expiry", default="24h" TokenExpiry time.Duration - // @InjectCfg key="auth.max-attempts", default="5" + // @InjectCfgValue key="auth.max-attempts", default="5" MaxAttempts int } @@ -215,7 +215,7 @@ configs: **@Service supports:** - `@Inject` - Service dependencies (required or optional) -- `@InjectCfg` - Configuration injection (auto-typed) +- `@InjectCfgValue` - Configuration injection (auto-typed) - No HTTP routes (use `@RouterService` for that) **Generated code:** @@ -241,13 +241,13 @@ func RegisterAuthService() { | `@Service` | Pure service (no HTTP) | Above struct | | `@Route` | HTTP endpoint | Above method (RouterService only) | | `@Inject` | Dependency injection | Above field | -| `@InjectCfg` | Config injection | Above field | +| `@InjectCfgValue` | Config injection | Above field | **@Inject parameters:** - `service` (positional or named) - service name - `optional` - `true`/`false` (default: `false`) -**@InjectCfg parameters:** +**@InjectCfgValue parameters:** - `key` (positional or named) - config key - `default` - default value (optional) - Type auto-detected: `string`, `int`, `bool`, `float64`, `time.Duration` diff --git a/docs/fixes/unused-imports-fix.md b/docs/fixes/unused-imports-fix.md new file mode 100644 index 00000000..be6ab82f --- /dev/null +++ b/docs/fixes/unused-imports-fix.md @@ -0,0 +1,89 @@ +# Fix: Unused Imports in Generated Code + +## Problem +Generated code (`zz_generated.lokstra.go`) was including **all** import statements from source files, even if those packages were not used in handler method signatures or injected dependencies. + +### Example +```go +// Source file: credential_service.go +import ( + "github.com/primadi/lokstra-auth/credential/domain" // NOT used in handlers + "github.com/primadi/lokstra/core/request" // NOT used in handlers + core_repository "github.com/primadi/lokstra-auth/infrastructure/repository" // USED in @Inject +) + +// @RouterService name="credential-service", prefix="/api" +type CredentialService struct { + // @Inject "credential-repository" + Repo core_repository.CredentialRepository +} + +// @Route "GET /users/{id}" +func (s *CredentialService) GetUser(id string) (string, error) { + // Method doesn't use domain or request packages + return "user-" + id, nil +} +``` + +**Before fix:** +```go +// zz_generated.lokstra.go +import ( + "github.com/primadi/lokstra-auth/credential/domain" // ❌ UNUSED + "github.com/primadi/lokstra/core/request" // ❌ UNUSED + core_repository "github.com/primadi/lokstra-auth/infrastructure/repository" // ✅ USED +) +``` + +**After fix:** +```go +// zz_generated.lokstra.go +import ( + core_repository "github.com/primadi/lokstra-auth/infrastructure/repository" // ✅ Only used imports +) +``` + +## Root Cause +Bug in `collectPackagesFromType()` function: +```go +// BEFORE (BUGGY): +if start := strings.Index(typeStr, "["); start != -1 { // ❌ Matches array brackets! + // This treats []*domain.User as generic type + // Returns early without extracting "domain" package +} +``` + +The function was checking for `[` at **any position**, which matched array/slice syntax like `[]*domain.User`, treating it as a generic type and returning early without extracting the package name. + +## Solution +Fix the order of operations: +1. **First**: Remove array/pointer prefixes (`*`, `[]`) +2. **Then**: Check for generics (which appear AFTER type name, like `Type[T]`) + +```go +// AFTER (FIXED): +// Remove pointer and array prefixes FIRST before checking for generics +cleanType := strings.TrimLeft(typeStr, "*[]") // []*domain.User -> domain.User + +// Handle generics: Type[Param1, Param2] +// Generic brackets appear AFTER the type name, not at the start +if start := strings.Index(cleanType, "["); start != -1 { + // Now correctly handles: Result[*domain.User], Option[domain.User] + // And skips: domain.User (no brackets after cleaning) +} +``` + +## Test Coverage +Added 2 new tests: +1. `TestUnusedImportsNotIncluded` - Verifies unused imports are filtered out +2. `TestOnlyMethodTypesIncluded` - Verifies only handler method types are included + +## Impact +- ✅ Cleaner generated code +- ✅ No unused import warnings +- ✅ Faster compilation (fewer imports to resolve) +- ✅ Better IDE performance (fewer packages to index) + +## Files Changed +- `core/annotation/codegen.go` - Fixed `collectPackagesFromType()` logic +- `core/annotation/test/unused_imports_test.go` - Added test coverage diff --git a/services/dbpool_manager/pool_manager.go b/services/dbpool_manager/pool_manager.go index 68e3f4ba..17ec7849 100644 --- a/services/dbpool_manager/pool_manager.go +++ b/services/dbpool_manager/pool_manager.go @@ -19,9 +19,8 @@ type DsnSchema struct { type dsnSchema = DsnSchema type PoolManager struct { - pools *sync.Map //map[dsn]serviceapi.DbPool - tenantPools *sync.Map // map[tenant]dsn, schema - namedPools *sync.Map // map[name]dsn, schema + pools *sync.Map // map[dsn]serviceapi.DbPool + aliasPools *sync.Map // map[alias]dsnSchema (unified tenant and named pools) newPoolFunc func(dsn string) (serviceapi.DbPool, error) } @@ -30,17 +29,15 @@ var _ serviceapi.DbPoolManager = (*PoolManager)(nil) func NewPoolManager(newPoolFunc func(dsn string) (serviceapi.DbPool, error)) serviceapi.DbPoolManager { return &PoolManager{ pools: &sync.Map{}, - tenantPools: &sync.Map{}, - namedPools: &sync.Map{}, + aliasPools: &sync.Map{}, newPoolFunc: newPoolFunc, } } func NewPgxPoolManager() serviceapi.DbPoolManager { return &PoolManager{ - pools: &sync.Map{}, - tenantPools: &sync.Map{}, - namedPools: &sync.Map{}, + pools: &sync.Map{}, + aliasPools: &sync.Map{}, newPoolFunc: func(dsn string) (serviceapi.DbPool, error) { return dbpool_pg.NewPgxPostgresPool(context.Background(), dsn) }, @@ -49,38 +46,17 @@ func NewPgxPoolManager() serviceapi.DbPoolManager { // AcquireNamedConn implements serviceapi.DbPoolManager. func (m *PoolManager) AcquireNamedConn(ctx context.Context, name string) (serviceapi.DbConn, error) { - dsn, schema, err := m.GetNamedDsn(name) - if err != nil { - return nil, err - } - pool, err := m.GetDsnPool(dsn) - if err != nil { - return nil, err - } - return pool.Acquire(ctx, schema) + return m.acquireAliasConn(ctx, "named:"+name, false) } // AcquireTenantConn implements serviceapi.DbPoolManager. func (m *PoolManager) AcquireTenantConn(ctx context.Context, tenant string) (serviceapi.DbConn, error) { - dsn, schema, err := m.GetTenantDsn(tenant) - if err != nil { - return nil, err - } - pool, err := m.GetDsnPool(dsn) - if err != nil { - return nil, err - } - return pool.AcquireMultiTenant(ctx, schema, tenant) + return m.acquireAliasConn(ctx, "tenant:"+tenant, true) } // GetNamedDsn implements serviceapi.DbPoolManager. func (m *PoolManager) GetNamedDsn(name string) (string, string, error) { - _ds, ok := m.namedPools.Load(name) - if !ok { - return "", "", fmt.Errorf("named pool not found: %s", name) - } - ds := _ds.(dsnSchema) - return ds.Dsn, ds.Schema, nil + return m.getAliasDsn("named:" + name) } // GetNamedPool implements serviceapi.DbPoolManager. @@ -98,12 +74,7 @@ func (m *PoolManager) GetNamedPool(name string) (serviceapi.DbPoolWithSchema, er // GetTenantDsn implements serviceapi.DbPoolManager. func (m *PoolManager) GetTenantDsn(tenant string) (string, string, error) { - _ds, ok := m.tenantPools.Load(tenant) - if !ok { - return "", "", fmt.Errorf("tenant pool not found: %s", tenant) - } - ds := _ds.(dsnSchema) - return ds.Dsn, ds.Schema, nil + return m.getAliasDsn("tenant:" + tenant) } // GetTenantPool implements serviceapi.DbPoolManager. @@ -121,22 +92,22 @@ func (m *PoolManager) GetTenantPool(tenant string) (serviceapi.DbPoolWithTenant, // RemoveNamed implements serviceapi.DbPoolManager. func (m *PoolManager) RemoveNamed(name string) { - m.namedPools.Delete(name) + m.removeAlias("named:" + name) } // RemoveTenant implements serviceapi.DbPoolManager. func (m *PoolManager) RemoveTenant(tenant string) { - m.tenantPools.Delete(tenant) + m.removeAlias("tenant:" + tenant) } // SetNamedDsn implements serviceapi.DbPoolManager. func (m *PoolManager) SetNamedDsn(name string, dsn string, schema string) { - m.namedPools.Store(name, dsnSchema{Dsn: dsn, Schema: schema}) + m.setAlias("named:"+name, dsn, schema) } // SetTenantDsn implements serviceapi.DbPoolManager. func (m *PoolManager) SetTenantDsn(tenant string, dsn string, schema string) { - m.tenantPools.Store(tenant, dsnSchema{Dsn: dsn, Schema: schema}) + m.setAlias("tenant:"+tenant, dsn, schema) } func (m *PoolManager) GetDsnPool(dsn string) (serviceapi.DbPool, error) { @@ -164,3 +135,41 @@ func (m *PoolManager) Shutdown() error { return nil } + +// ======================================== +// Internal helper methods +// ======================================== + +func (m *PoolManager) setAlias(alias, dsn, schema string) { + m.aliasPools.Store(alias, dsnSchema{Dsn: dsn, Schema: schema}) +} + +func (m *PoolManager) getAliasDsn(alias string) (string, string, error) { + _ds, ok := m.aliasPools.Load(alias) + if !ok { + return "", "", fmt.Errorf("alias pool not found: %s", alias) + } + ds := _ds.(dsnSchema) + return ds.Dsn, ds.Schema, nil +} + +func (m *PoolManager) removeAlias(alias string) { + m.aliasPools.Delete(alias) +} + +func (m *PoolManager) acquireAliasConn(ctx context.Context, alias string, isTenant bool) (serviceapi.DbConn, error) { + dsn, schema, err := m.getAliasDsn(alias) + if err != nil { + return nil, err + } + pool, err := m.GetDsnPool(dsn) + if err != nil { + return nil, err + } + if isTenant { + // Extract tenant ID from alias (remove "tenant:" prefix) + tenantID := alias[7:] // len("tenant:") = 7 + return pool.AcquireMultiTenant(ctx, schema, tenantID) + } + return pool.Acquire(ctx, schema) +} diff --git a/services/dbpool_manager/sync_pool_manager.go b/services/dbpool_manager/sync_pool_manager.go index e4317121..a93c08fe 100644 --- a/services/dbpool_manager/sync_pool_manager.go +++ b/services/dbpool_manager/sync_pool_manager.go @@ -10,35 +10,30 @@ import ( ) type SyncPoolManager struct { - pools *sync.Map // map[dsn]serviceapi.DbPool - tenantPools *syncmap.SyncMap[*dsnSchema] - namedPools *syncmap.SyncMap[*dsnSchema] + pools *sync.Map // map[dsn]serviceapi.DbPool + aliasPools *syncmap.SyncMap[*dsnSchema] // unified tenant and named pools newPoolFunc func(dsn string) (serviceapi.DbPool, error) } var _ serviceapi.DbPoolManager = (*SyncPoolManager)(nil) func NewSyncPoolManager( - tenantPools *syncmap.SyncMap[*dsnSchema], - namedPools *syncmap.SyncMap[*dsnSchema], + aliasPools *syncmap.SyncMap[*dsnSchema], newPoolFunc func(dsn string) (serviceapi.DbPool, error), ) serviceapi.DbPoolManager { return &SyncPoolManager{ pools: &sync.Map{}, - tenantPools: tenantPools, - namedPools: namedPools, + aliasPools: aliasPools, newPoolFunc: newPoolFunc, } } func NewPgxSyncPoolManager( - tenantPools *syncmap.SyncMap[*dsnSchema], - namedPools *syncmap.SyncMap[*dsnSchema], + aliasPools *syncmap.SyncMap[*dsnSchema], ) serviceapi.DbPoolManager { return &SyncPoolManager{ - pools: &sync.Map{}, - tenantPools: tenantPools, - namedPools: namedPools, + pools: &sync.Map{}, + aliasPools: aliasPools, newPoolFunc: func(dsn string) (serviceapi.DbPool, error) { return dbpool_pg.NewPgxPostgresPool(context.Background(), dsn) }, @@ -47,37 +42,17 @@ func NewPgxSyncPoolManager( // AcquireNamedConn implements serviceapi.DbPoolManager. func (m *SyncPoolManager) AcquireNamedConn(ctx context.Context, name string) (serviceapi.DbConn, error) { - dsn, schema, err := m.GetNamedDsn(name) - if err != nil { - return nil, err - } - pool, err := m.GetDsnPool(dsn) - if err != nil { - return nil, err - } - return pool.Acquire(ctx, schema) + return m.acquireAliasConn(ctx, "named:"+name, false) } // AcquireTenantConn implements serviceapi.DbPoolManager. func (m *SyncPoolManager) AcquireTenantConn(ctx context.Context, tenant string) (serviceapi.DbConn, error) { - dsn, schema, err := m.GetTenantDsn(tenant) - if err != nil { - return nil, err - } - pool, err := m.GetDsnPool(dsn) - if err != nil { - return nil, err - } - return pool.AcquireMultiTenant(ctx, schema, tenant) + return m.acquireAliasConn(ctx, "tenant:"+tenant, true) } // GetNamedDsn implements serviceapi.DbPoolManager. func (m *SyncPoolManager) GetNamedDsn(name string) (string, string, error) { - ds, err := m.namedPools.Get(context.Background(), name) - if err != nil { - return "", "", err - } - return ds.Dsn, ds.Schema, nil + return m.getAliasDsn("named:" + name) } // GetNamedPool implements serviceapi.DbPoolManager. @@ -95,11 +70,7 @@ func (m *SyncPoolManager) GetNamedPool(name string) (serviceapi.DbPoolWithSchema // GetTenantDsn implements serviceapi.DbPoolManager. func (m *SyncPoolManager) GetTenantDsn(tenant string) (string, string, error) { - ds, err := m.tenantPools.Get(context.Background(), tenant) - if err != nil { - return "", "", err - } - return ds.Dsn, ds.Schema, nil + return m.getAliasDsn("tenant:" + tenant) } // GetTenantPool implements serviceapi.DbPoolManager. @@ -117,22 +88,22 @@ func (m *SyncPoolManager) GetTenantPool(tenant string) (serviceapi.DbPoolWithTen // RemoveNamed implements serviceapi.DbPoolManager. func (m *SyncPoolManager) RemoveNamed(name string) { - _ = m.namedPools.Delete(context.Background(), name) + m.removeAlias("named:" + name) } // RemoveTenant implements serviceapi.DbPoolManager. func (m *SyncPoolManager) RemoveTenant(tenant string) { - _ = m.tenantPools.Delete(context.Background(), tenant) + m.removeAlias("tenant:" + tenant) } // SetNamedDsn implements serviceapi.DbPoolManager. func (m *SyncPoolManager) SetNamedDsn(name string, dsn string, schema string) { - _ = m.namedPools.Set(context.Background(), name, &dsnSchema{Dsn: dsn, Schema: schema}) + m.setAlias("named:"+name, dsn, schema) } // SetTenantDsn implements serviceapi.DbPoolManager. func (m *SyncPoolManager) SetTenantDsn(tenant string, dsn string, schema string) { - _ = m.tenantPools.Set(context.Background(), tenant, &dsnSchema{Dsn: dsn, Schema: schema}) + m.setAlias("tenant:"+tenant, dsn, schema) } func (m *SyncPoolManager) GetDsnPool(dsn string) (serviceapi.DbPool, error) { @@ -160,3 +131,40 @@ func (m *SyncPoolManager) Shutdown() error { return nil } + +// ======================================== +// Internal helper methods +// ======================================== + +func (m *SyncPoolManager) setAlias(alias, dsn, schema string) { + _ = m.aliasPools.Set(context.Background(), alias, &dsnSchema{Dsn: dsn, Schema: schema}) +} + +func (m *SyncPoolManager) getAliasDsn(alias string) (string, string, error) { + ds, err := m.aliasPools.Get(context.Background(), alias) + if err != nil { + return "", "", err + } + return ds.Dsn, ds.Schema, nil +} + +func (m *SyncPoolManager) removeAlias(alias string) { + _ = m.aliasPools.Delete(context.Background(), alias) +} + +func (m *SyncPoolManager) acquireAliasConn(ctx context.Context, alias string, isTenant bool) (serviceapi.DbConn, error) { + dsn, schema, err := m.getAliasDsn(alias) + if err != nil { + return nil, err + } + pool, err := m.GetDsnPool(dsn) + if err != nil { + return nil, err + } + if isTenant { + // Extract tenant ID from alias (remove "tenant:" prefix) + tenantID := alias[7:] // len("tenant:") = 7 + return pool.AcquireMultiTenant(ctx, schema, tenantID) + } + return pool.Acquire(ctx, schema) +}