diff --git a/application.go b/application.go index ce61f464..43e439d2 100644 --- a/application.go +++ b/application.go @@ -12,52 +12,210 @@ import ( "time" ) -// AppRegistry provides registry functionality for applications +// AppRegistry provides registry functionality for applications. +// This interface provides access to the application's service registry, +// allowing modules and components to access registered services. type AppRegistry interface { - // SvcRegistry retrieves the service svcRegistry + // SvcRegistry retrieves the service registry. + // The service registry contains all services registered by modules + // and the application, providing a central location for service lookup. SvcRegistry() ServiceRegistry } -// Application represents the core application interface with configuration, module management, and service registration +// Application represents the core application interface with configuration, module management, and service registration. +// This is the main interface that modules interact with during initialization and runtime. +// +// The Application provides a complete framework for: +// - Managing module lifecycle (registration, initialization, startup, shutdown) +// - Configuration management with multiple sections and providers +// - Service registry for inter-module communication +// - Dependency injection and resolution +// - Graceful startup and shutdown coordination +// +// Basic usage pattern: +// +// app := modular.NewStdApplication(configProvider, logger) +// app.RegisterModule(&MyModule{}) +// app.RegisterModule(&AnotherModule{}) +// if err := app.Run(); err != nil { +// log.Fatal(err) +// } type Application interface { - // ConfigProvider retrieves the application config provider + // ConfigProvider retrieves the application's main configuration provider. + // This provides access to application-level configuration that isn't + // specific to any particular module. ConfigProvider() ConfigProvider - // SvcRegistry retrieves the service svcRegistry + + // SvcRegistry retrieves the service registry. + // Modules use this to register services they provide and lookup + // services they need from other modules. SvcRegistry() ServiceRegistry - // RegisterModule adds a module to the application + + // RegisterModule adds a module to the application. + // Modules must be registered before calling Init(). The framework + // will handle initialization order based on declared dependencies. + // + // Example: + // app.RegisterModule(&DatabaseModule{}) + // app.RegisterModule(&WebServerModule{}) RegisterModule(module Module) - // RegisterConfigSection registers a configuration section with the application + + // RegisterConfigSection registers a configuration section with the application. + // This allows modules to register their configuration requirements, + // making them available for loading from configuration sources. + // + // Example: + // cfg := &MyModuleConfig{} + // provider := modular.NewStdConfigProvider(cfg) + // app.RegisterConfigSection("mymodule", provider) RegisterConfigSection(section string, cp ConfigProvider) - // ConfigSections retrieves all registered configuration sections + + // ConfigSections retrieves all registered configuration sections. + // Returns a map of section names to their configuration providers. + // Useful for debugging and introspection. ConfigSections() map[string]ConfigProvider - // GetConfigSection retrieves a configuration section + + // GetConfigSection retrieves a specific configuration section. + // Returns an error if the section doesn't exist. + // + // Example: + // provider, err := app.GetConfigSection("database") + // if err != nil { + // return err + // } + // cfg := provider.GetConfig().(*DatabaseConfig) GetConfigSection(section string) (ConfigProvider, error) - // RegisterService adds a service with type checking + + // RegisterService adds a service to the service registry with type checking. + // Services registered here become available to all modules that declare + // them as dependencies. + // + // Returns an error if a service with the same name is already registered. + // + // Example: + // db := &DatabaseConnection{} + // err := app.RegisterService("database", db) RegisterService(name string, service any) error - // GetService retrieves a service with type assertion + + // GetService retrieves a service from the registry with type assertion. + // The target parameter must be a pointer to the expected type. + // The framework will perform type checking and assignment. + // + // Example: + // var db *DatabaseConnection + // err := app.GetService("database", &db) GetService(name string, target any) error - // Init initializes the application with the provided modules + + // Init initializes the application and all registered modules. + // This method: + // - Calls RegisterConfig on all configurable modules + // - Loads configuration from all registered sources + // - Resolves module dependencies + // - Initializes modules in dependency order + // - Registers services provided by modules + // + // Must be called before Start() or Run(). Init() error - // Start starts the application + + // Start starts the application and all startable modules. + // Modules implementing the Startable interface will have their + // Start method called in dependency order. + // + // This is typically used when you want to start the application + // but handle the shutdown logic yourself (rather than using Run()). Start() error - // Stop stops the application + + // Stop stops the application and all stoppable modules. + // Modules implementing the Stoppable interface will have their + // Stop method called in reverse dependency order. + // + // Provides a timeout context for graceful shutdown. Stop() error - // Run starts the application and blocks until termination + + // Run starts the application and blocks until termination. + // This is equivalent to calling Init(), Start(), and then waiting + // for a termination signal (SIGINT, SIGTERM) before calling Stop(). + // + // This is the most common way to run a modular application: + // if err := app.Run(); err != nil { + // log.Fatal(err) + // } Run() error - // Logger retrieves the application's logger + + // Logger retrieves the application's logger. + // This logger is used by the framework and can be used by modules + // for consistent logging throughout the application. Logger() Logger - // SetLogger sets the application's logger + + // SetLogger sets the application's logger. + // Should be called before module registration to ensure + // all framework operations use the new logger. SetLogger(logger Logger) } -// TenantApplication extends Application with multi-tenant functionality +// TenantApplication extends Application with multi-tenant functionality. +// This interface adds tenant-aware capabilities to the standard Application, +// allowing the same application instance to serve multiple tenants with +// isolated configurations and contexts. +// +// Multi-tenant applications can: +// - Maintain separate configurations per tenant +// - Provide tenant-specific service instances +// - Isolate tenant data and operations +// - Support dynamic tenant registration and management +// +// Example usage: +// +// app := modular.NewStdApplication(configProvider, logger) +// // Register tenant service and tenant-aware modules +// tenantCtx, err := app.WithTenant("tenant-123") +// if err != nil { +// return err +// } +// // Use tenant context for tenant-specific operations type TenantApplication interface { Application - // GetTenantService returns the application's tenant service if available + + // GetTenantService returns the application's tenant service if available. + // The tenant service manages tenant registration, lookup, and lifecycle. + // Returns an error if no tenant service has been registered. + // + // Example: + // tenantSvc, err := app.GetTenantService() + // if err != nil { + // return fmt.Errorf("multi-tenancy not configured: %w", err) + // } GetTenantService() (TenantService, error) - // WithTenant creates a tenant context from the application context + + // WithTenant creates a tenant context from the application context. + // Tenant contexts provide scoped access to tenant-specific configurations + // and services, enabling isolation between different tenants. + // + // The returned context can be used for tenant-specific operations + // and will carry tenant identification through the call chain. + // + // Example: + // tenantCtx, err := app.WithTenant("customer-456") + // if err != nil { + // return err + // } + // // Use tenantCtx for tenant-specific operations WithTenant(tenantID TenantID) (*TenantContext, error) - // GetTenantConfig retrieves configuration for a specific tenant and section + + // GetTenantConfig retrieves configuration for a specific tenant and section. + // This allows modules to access tenant-specific configuration that may + // override or extend the default application configuration. + // + // The section parameter specifies which configuration section to retrieve + // (e.g., "database", "cache", etc.), and the framework will return the + // tenant-specific version if available, falling back to defaults otherwise. + // + // Example: + // cfg, err := app.GetTenantConfig("tenant-789", "database") + // if err != nil { + // return err + // } + // dbConfig := cfg.GetConfig().(*DatabaseConfig) GetTenantConfig(tenantID TenantID, section string) (ConfigProvider, error) } @@ -73,7 +231,37 @@ type StdApplication struct { tenantService TenantService // Added tenant service reference } -// NewStdApplication creates a new application instance +// NewStdApplication creates a new application instance with the provided configuration and logger. +// This is the standard way to create a modular application. +// +// Parameters: +// - cp: ConfigProvider for application-level configuration +// - logger: Logger implementation for framework and module logging +// +// The created application will have empty registries that can be populated by +// registering modules and services. The application must be initialized with +// Init() before it can be started. +// +// Example: +// +// // Create configuration +// appConfig := &MyAppConfig{} +// configProvider := modular.NewStdConfigProvider(appConfig) +// +// // Create logger (implement modular.Logger interface) +// logger := &MyLogger{} +// +// // Create application +// app := modular.NewStdApplication(configProvider, logger) +// +// // Register modules +// app.RegisterModule(&DatabaseModule{}) +// app.RegisterModule(&WebServerModule{}) +// +// // Run application +// if err := app.Run(); err != nil { +// log.Fatal(err) +// } func NewStdApplication(cp ConfigProvider, logger Logger) Application { return &StdApplication{ cfgProvider: cp, diff --git a/config_provider.go b/config_provider.go index f1375f43..0fde2204 100644 --- a/config_provider.go +++ b/config_provider.go @@ -9,40 +9,102 @@ import ( const mainConfigSection = "_main" -// LoadAppConfigFunc is the function type for loading application configuration +// LoadAppConfigFunc is the function type for loading application configuration. +// This function is responsible for loading configuration data into the application +// using the registered config feeders and config sections. +// +// The default implementation can be replaced for testing or custom configuration scenarios. type LoadAppConfigFunc func(*StdApplication) error -// AppConfigLoader is the default implementation that can be replaced in tests +// AppConfigLoader is the default implementation that can be replaced in tests. +// This variable allows the configuration loading strategy to be customized, +// which is particularly useful for testing scenarios where you want to +// control how configuration is loaded. +// +// Example of replacing for tests: +// +// oldLoader := modular.AppConfigLoader +// defer func() { modular.AppConfigLoader = oldLoader }() +// modular.AppConfigLoader = func(app *StdApplication) error { +// // Custom test configuration loading +// return nil +// } var AppConfigLoader LoadAppConfigFunc = loadAppConfig -// ConfigProvider defines the interface for providing configuration objects +// ConfigProvider defines the interface for providing configuration objects. +// Configuration providers encapsulate configuration data and make it available +// to modules and the application framework. +// +// The framework supports multiple configuration sources (files, environment variables, +// command-line flags) and formats (JSON, YAML, TOML) through different providers. type ConfigProvider interface { - // GetConfig returns the configuration object + // GetConfig returns the configuration object. + // The returned value should be a pointer to a struct that represents + // the configuration schema. Modules typically type-assert this to + // their expected configuration type. + // + // Example: + // cfg := provider.GetConfig().(*MyModuleConfig) GetConfig() any } -// StdConfigProvider provides a standard implementation of ConfigProvider +// StdConfigProvider provides a standard implementation of ConfigProvider. +// It wraps a configuration struct and makes it available through the ConfigProvider interface. +// +// This is the most common way to create configuration providers for modules. +// Simply create your configuration struct and wrap it with NewStdConfigProvider. type StdConfigProvider struct { cfg any } -// GetConfig returns the configuration object +// GetConfig returns the configuration object. +// The returned value is the exact object that was passed to NewStdConfigProvider. func (s *StdConfigProvider) GetConfig() any { return s.cfg } -// NewStdConfigProvider creates a new standard configuration provider +// NewStdConfigProvider creates a new standard configuration provider. +// The cfg parameter should be a pointer to a struct that defines the +// configuration schema for your module. +// +// Example: +// +// type MyConfig struct { +// Host string `json:"host" default:"localhost"` +// Port int `json:"port" default:"8080"` +// } +// +// cfg := &MyConfig{} +// provider := modular.NewStdConfigProvider(cfg) func NewStdConfigProvider(cfg any) *StdConfigProvider { return &StdConfigProvider{cfg: cfg} } -// Config represents a configuration builder that can combine multiple feeders and structures +// Config represents a configuration builder that can combine multiple feeders and structures. +// It extends the golobby/config library with additional functionality for the modular framework. +// +// The Config builder allows you to: +// - Add multiple configuration sources (files, environment, etc.) +// - Combine configuration from different feeders +// - Apply configuration to multiple struct targets +// - Track which structs have been configured type Config struct { *config.Config + // StructKeys maps struct identifiers to their configuration objects. + // Used internally to track which configuration structures have been processed. StructKeys map[string]interface{} } -// NewConfig creates a new configuration builder +// NewConfig creates a new configuration builder. +// The returned Config can be used to set up complex configuration scenarios +// involving multiple sources and target structures. +// +// Example: +// +// cfg := modular.NewConfig() +// cfg.AddFeeder(modular.ConfigFeeders[0]) // Add file feeder +// cfg.AddStruct(&myConfig) // Add target struct +// err := cfg.Feed() // Load configuration func NewConfig() *Config { return &Config{ Config: config.New(), diff --git a/config_validation.go b/config_validation.go index 73346dde..1c2d434e 100644 --- a/config_validation.go +++ b/config_validation.go @@ -20,14 +20,53 @@ const ( tagDesc = "desc" // Used for generating sample config and documentation ) -// ConfigValidator is an interface for configuration validation +// ConfigValidator is an interface for configuration validation. +// Configuration structs can implement this interface to provide +// custom validation logic beyond the standard required field checking. +// +// The framework automatically calls Validate() on configuration objects +// that implement this interface during module initialization. +// +// Example implementation: +// +// type MyConfig struct { +// Host string `json:"host" required:"true"` +// Port int `json:"port" default:"8080" validate:"range:1024-65535"` +// } +// +// func (c *MyConfig) Validate() error { +// if c.Port < 1024 || c.Port > 65535 { +// return fmt.Errorf("port must be between 1024 and 65535") +// } +// return nil +// } type ConfigValidator interface { - // Validate validates the configuration and returns an error if invalid + // Validate validates the configuration and returns an error if invalid. + // This method is called automatically by the framework after configuration + // loading and default value processing. It should return a descriptive + // error if the configuration is invalid. Validate() error } -// ProcessConfigDefaults applies default values to a config struct based on struct tags -// It looks for `default:"value"` tags on struct fields and sets the field value if currently zero/empty +// ProcessConfigDefaults applies default values to a config struct based on struct tags. +// It looks for `default:"value"` tags on struct fields and sets the field value if currently zero/empty. +// +// Supported field types: +// - Basic types: string, int, float, bool +// - Slices: []string, []int, etc. +// - Pointers to basic types +// +// Example struct tags: +// +// type Config struct { +// Host string `default:"localhost"` +// Port int `default:"8080"` +// Debug bool `default:"false"` +// Features []string `default:"feature1,feature2"` +// } +// +// This function is automatically called by the configuration loading system +// before validation, but can also be called manually if needed. func ProcessConfigDefaults(cfg interface{}) error { if cfg == nil { return ErrConfigNil diff --git a/errors.go b/errors.go index 3a16ead7..c7f71041 100644 --- a/errors.go +++ b/errors.go @@ -4,15 +4,27 @@ import ( "errors" ) +// Common error definitions for the modular framework. +// These errors are organized into categories for different subsystems: +// +// Configuration errors: Issues with loading, parsing, or validating configuration +// Service registry errors: Problems with service registration and lookup +// Module lifecycle errors: Errors during module initialization, startup, or shutdown +// Dependency resolution errors: Issues with resolving module or service dependencies +// Tenant management errors: Problems with multi-tenant functionality +// +// All errors follow Go 1.13+ error wrapping conventions and can be used +// with errors.Is() and errors.As() for error handling and testing. + // Application errors var ( - // Configuration errors + // Configuration errors - issues with loading and managing configuration ErrConfigSectionNotFound = errors.New("config section not found") ErrApplicationNil = errors.New("application is nil") ErrConfigProviderNil = errors.New("failed to load app config: config provider is nil") ErrConfigSectionError = errors.New("failed to load app config: error triggered by section") - // Config validation errors + // Config validation errors - problems with configuration structure and values ErrConfigNil = errors.New("config is nil") ErrConfigNotPointer = errors.New("config must be a pointer") ErrConfigNotStruct = errors.New("config must be a struct") diff --git a/examples/instance-aware-db/main.go b/examples/instance-aware-db/main.go index d817862c..da9eaa96 100644 --- a/examples/instance-aware-db/main.go +++ b/examples/instance-aware-db/main.go @@ -13,9 +13,9 @@ import ( func main() { // This example demonstrates how to use instance-aware environment variable configuration // for multiple database connections - + fmt.Println("=== Instance-Aware Database Configuration Example ===") - + // Set up environment variables for multiple database connections // In a real application, these would be set externally envVars := map[string]string{ @@ -32,7 +32,7 @@ func main() { os.Setenv(key, value) fmt.Printf(" %s=%s\n", key, value) } - + // Clean up environment variables at the end defer func() { for key := range envVars { @@ -81,7 +81,7 @@ func main() { connections := dbManager.GetConnections() for _, connName := range connections { fmt.Printf(" - %s\n", connName) - + if db, exists := dbManager.GetConnection(connName); exists { if err := db.Ping(); err != nil { fmt.Printf(" ❌ Failed to ping %s: %v\n", connName, err) @@ -100,7 +100,7 @@ func main() { // Demonstrate using different connections fmt.Println("\nDemonstrating multiple database connections:") - + // Use primary connection if primaryDB, exists := dbManager.GetConnection("primary"); exists { fmt.Println("Using primary database...") @@ -111,7 +111,7 @@ func main() { } } - // Use secondary connection + // Use secondary connection if secondaryDB, exists := dbManager.GetConnection("secondary"); exists { fmt.Println("Using secondary database...") if _, err := secondaryDB.Exec("CREATE TABLE IF NOT EXISTS logs (id INTEGER PRIMARY KEY, message TEXT)"); err != nil { @@ -186,7 +186,7 @@ func setupDatabaseConnections(app modular.Application, dbModule *database.Module if prefixFunc != nil { feeder := modular.NewInstanceAwareEnvFeeder(prefixFunc) instanceConfigs := config.GetInstanceConfigs() - + // Feed each instance with environment variables for instanceKey, instanceConfig := range instanceConfigs { if err := feeder.FeedKey(instanceKey, instanceConfig); err != nil { @@ -204,4 +204,4 @@ func setupDatabaseConnections(app modular.Application, dbModule *database.Module } return nil -} \ No newline at end of file +} diff --git a/logger.go b/logger.go index 4ff80b3a..6d3525b9 100644 --- a/logger.go +++ b/logger.go @@ -1,9 +1,67 @@ package modular -// Logger defines the interface for application logging +// Logger defines the interface for application logging. +// The modular framework uses structured logging with key-value pairs +// to provide consistent, parseable log output across all modules. +// +// All framework operations (module initialization, service registration, +// dependency resolution, etc.) are logged using this interface, so +// implementing applications can control how framework logs appear. +// +// The Logger interface uses variadic arguments in key-value pairs: +// +// logger.Info("message", "key1", "value1", "key2", "value2") +// +// This approach is compatible with popular structured logging libraries +// like slog, logrus, zap, and others. +// +// Example implementation using Go's standard log/slog: +// +// type SlogLogger struct { +// logger *slog.Logger +// } +// +// func (l *SlogLogger) Info(msg string, args ...any) { +// l.logger.Info(msg, args...) +// } +// +// func (l *SlogLogger) Error(msg string, args ...any) { +// l.logger.Error(msg, args...) +// } +// +// func (l *SlogLogger) Warn(msg string, args ...any) { +// l.logger.Warn(msg, args...) +// } +// +// func (l *SlogLogger) Debug(msg string, args ...any) { +// l.logger.Debug(msg, args...) +// } type Logger interface { + // Info logs an informational message with optional key-value pairs. + // Used for normal application events like module startup, service registration, etc. + // + // Example: + // logger.Info("Module initialized", "module", "database", "version", "1.2.3") Info(msg string, args ...any) + + // Error logs an error message with optional key-value pairs. + // Used for errors that don't prevent application startup but should be noted. + // + // Example: + // logger.Error("Failed to connect to service", "service", "cache", "error", err) Error(msg string, args ...any) + + // Warn logs a warning message with optional key-value pairs. + // Used for conditions that are unusual but don't prevent normal operation. + // + // Example: + // logger.Warn("Service unavailable, using fallback", "service", "external-api") Warn(msg string, args ...any) + + // Debug logs a debug message with optional key-value pairs. + // Used for detailed diagnostic information, typically disabled in production. + // + // Example: + // logger.Debug("Dependency resolved", "from", "module1", "to", "module2") Debug(msg string, args ...any) } diff --git a/module.go b/module.go index a69470e7..506f4cd9 100644 --- a/module.go +++ b/module.go @@ -1,62 +1,257 @@ // Package modular provides a flexible, modular application framework for Go. // It supports configuration management, dependency injection, service registration, // and multi-tenant functionality. +// +// The modular framework allows you to build applications composed of independent +// modules that can declare dependencies, provide services, and be configured +// individually. Each module implements the Module interface and can optionally +// implement additional interfaces like Configurable, ServiceAware, Startable, etc. +// +// Basic usage: +// +// app := modular.NewStdApplication(configProvider, logger) +// app.RegisterModule(&MyModule{}) +// if err := app.Run(); err != nil { +// log.Fatal(err) +// } package modular import "context" -// Module represents a registrable component in the application +// Module represents a registrable component in the application. +// All modules must implement this interface to be managed by the application. +// +// A module is the basic building block of a modular application. It encapsulates +// a specific piece of functionality and can interact with other modules through +// the application's service registry and configuration system. type Module interface { - // Name returns the unique identifier for this module + // Name returns the unique identifier for this module. + // The name is used for dependency resolution and service registration. + // It must be unique within the application and should be descriptive + // of the module's purpose. + // + // Example: "database", "auth", "httpserver", "cache" Name() string - // Init Initialize the module with the application context + + // Init initializes the module with the application context. + // This method is called during application initialization after + // all modules have been registered and their configurations loaded. + // + // The Init method should: + // - Validate any required configuration + // - Initialize internal state + // - Register any services this module provides + // - Prepare for Start() to be called + // + // Init is called in dependency order - modules that depend on others + // are initialized after their dependencies. Init(app Application) error } -// Configurable is an interface for modules that can have configuration +// Configurable is an interface for modules that can have configuration. +// Modules implementing this interface can register configuration sections +// with the application, allowing them to receive typed configuration data. +// +// The configuration system supports multiple formats (JSON, YAML, TOML) +// and multiple sources (files, environment variables, etc.). type Configurable interface { - // RegisterConfig registers configuration requirements + // RegisterConfig registers configuration requirements with the application. + // This method is called during application initialization before Init(). + // + // Implementation should: + // - Define the configuration structure + // - Register the configuration section with app.RegisterConfigSection() + // - Set up any configuration validation rules + // + // Example: + // func (m *MyModule) RegisterConfig(app Application) error { + // cfg := &MyModuleConfig{} + // provider := modular.NewStdConfigProvider(cfg) + // app.RegisterConfigSection(m.Name(), provider) + // return nil + // } RegisterConfig(app Application) error } -// DependencyAware is an interface for modules that can have dependencies +// DependencyAware is an interface for modules that depend on other modules. +// The framework uses this information to determine initialization order, +// ensuring dependencies are initialized before dependent modules. +// +// Dependencies are resolved by module name and must be exact matches. +// Circular dependencies will cause initialization to fail. type DependencyAware interface { - // Dependencies returns names of other modules this module depends on + // Dependencies returns names of other modules this module depends on. + // The returned slice should contain the exact names returned by + // the Name() method of the dependency modules. + // + // Dependencies are initialized before this module during application startup. + // If any dependency is missing, application initialization will fail. + // + // Example: + // func (m *WebModule) Dependencies() []string { + // return []string{"database", "auth", "cache"} + // } Dependencies() []string } -// ServiceAware is an interface for modules that can provide or require services +// ServiceAware is an interface for modules that can provide or consume services. +// Services enable loose coupling between modules by providing a registry +// for sharing functionality without direct dependencies. +// +// Modules can both provide services for other modules to use and require +// services that other modules provide. The framework handles service +// injection automatically based on these declarations. type ServiceAware interface { - // ProvidesServices returns a list of services provided by this module + // ProvidesServices returns a list of services provided by this module. + // These services will be registered in the application's service registry + // after the module is initialized, making them available to other modules. + // + // Each ServiceProvider should specify: + // - Name: unique identifier for the service + // - Instance: the actual service implementation + // + // Example: + // func (m *DatabaseModule) ProvidesServices() []ServiceProvider { + // return []ServiceProvider{ + // {Name: "database", Instance: m.db}, + // {Name: "migrator", Instance: m.migrator}, + // } + // } ProvidesServices() []ServiceProvider - // RequiresServices returns a list of services required by this module + + // RequiresServices returns a list of services required by this module. + // These services must be provided by other modules or the application + // for this module to function correctly. + // + // Services can be matched by name or by interface. When using interface + // matching, the framework will find any service that implements the + // specified interface. + // + // Example: + // func (m *WebModule) RequiresServices() []ServiceDependency { + // return []ServiceDependency{ + // {Name: "database", Required: true}, + // {Name: "logger", SatisfiesInterface: reflect.TypeOf((*Logger)(nil)).Elem(), MatchByInterface: true}, + // } + // } RequiresServices() []ServiceDependency } -// Startable is an interface for modules that can be started +// Startable is an interface for modules that need to perform startup operations. +// Modules implementing this interface will have their Start method called +// after all modules have been initialized successfully. +// +// Start operations typically involve: +// - Starting background goroutines +// - Opening network listeners +// - Connecting to external services +// - Beginning periodic tasks type Startable interface { + // Start begins the module's runtime operations. + // This method is called after Init() and after all modules have been initialized. + // Start is called in dependency order - dependencies start before dependents. + // + // The provided context is the application's lifecycle context. When this + // context is cancelled, the module should stop its operations gracefully. + // + // Start should be non-blocking for short-running initialization, but may + // spawn goroutines for long-running operations. Use the provided context + // to handle graceful shutdown. + // + // Example: + // func (m *HTTPServerModule) Start(ctx context.Context) error { + // go func() { + // <-ctx.Done() + // m.server.Shutdown(context.Background()) + // }() + // return m.server.ListenAndServe() + // } Start(ctx context.Context) error } -// Stoppable is an interface for modules that can be stopped +// Stoppable is an interface for modules that need to perform cleanup operations. +// Modules implementing this interface will have their Stop method called +// during application shutdown, in reverse dependency order. +// +// Stop operations typically involve: +// - Gracefully shutting down background goroutines +// - Closing network connections +// - Flushing buffers and saving state +// - Releasing external resources type Stoppable interface { + // Stop performs graceful shutdown of the module. + // This method is called during application shutdown, in reverse dependency + // order (dependents stop before their dependencies). + // + // The provided context includes a timeout for the shutdown process. + // Modules should respect this timeout and return promptly when it expires. + // + // Stop should: + // - Stop accepting new work + // - Complete or cancel existing work + // - Close resources and connections + // - Return any critical errors that occurred during shutdown + // + // Example: + // func (m *DatabaseModule) Stop(ctx context.Context) error { + // return m.db.Close() + // } Stop(ctx context.Context) error } -// Constructable is an interface for modules that can be constructed with a constructor +// Constructable is an interface for modules that support constructor-based dependency injection. +// This is an advanced feature that allows modules to be reconstructed with their +// dependencies automatically injected as constructor parameters. +// +// This is useful when a module needs its dependencies available during construction +// rather than after initialization, or when using dependency injection frameworks. type Constructable interface { - // Constructor returns a function to construct this module + // Constructor returns a function to construct this module with dependency injection. + // The returned function should have the signature: + // func(app Application, services map[string]any) (Module, error) + // + // The services map contains all services that this module declared as requirements. + // The constructor can also accept individual service types as parameters, and + // the framework will automatically provide them based on type matching. + // + // Example: + // func (m *WebModule) Constructor() ModuleConstructor { + // return func(app Application, services map[string]any) (Module, error) { + // db := services["database"].(Database) + // return NewWebModule(db), nil + // } + // } Constructor() ModuleConstructor } -// ModuleConstructor is a function type that creates module instances with dependency injection +// ModuleConstructor is a function type that creates module instances with dependency injection. +// Constructor functions receive the application instance and a map of resolved services +// that the module declared as requirements. +// +// The constructor should: +// - Extract required services from the services map +// - Perform any type assertions needed +// - Create and return a new module instance +// - Return an error if construction fails +// +// Constructor functions enable advanced dependency injection patterns and can also +// accept typed parameters that the framework will resolve automatically. type ModuleConstructor func(app Application, services map[string]any) (Module, error) -// ModuleWithConstructor defines modules that support constructor-based dependency injection +// ModuleWithConstructor defines modules that support constructor-based dependency injection. +// This is a convenience interface that combines Module and Constructable. +// +// Modules implementing this interface will be reconstructed using their constructor +// after dependencies are resolved, allowing for cleaner dependency injection patterns. type ModuleWithConstructor interface { Module Constructable } -// ModuleRegistry represents a svcRegistry of modules +// ModuleRegistry represents a registry of modules keyed by their names. +// This is used internally by the application to manage registered modules +// and resolve dependencies between them. +// +// The registry ensures each module name is unique and provides efficient +// lookup during dependency resolution and lifecycle management. type ModuleRegistry map[string]Module diff --git a/modules/auth/module.go b/modules/auth/module.go index a6f8ed44..ada752e4 100644 --- a/modules/auth/module.go +++ b/modules/auth/module.go @@ -1,3 +1,24 @@ +// Package auth provides authentication and authorization functionality for modular applications. +// This module supports JWT tokens, session management, and OAuth2 flows. +// +// The auth module provides: +// - User authentication with configurable stores +// - JWT token generation and validation +// - Session management with configurable backends +// - OAuth2 integration support +// - Password hashing and validation +// +// Usage: +// +// app.RegisterModule(auth.NewModule()) +// +// The module registers an "auth" service that implements the AuthService interface, +// providing methods for user login, token validation, and session management. +// +// Configuration: +// +// The module requires an "auth" configuration section with JWT secrets, +// session settings, and OAuth2 configuration. package auth import ( @@ -8,35 +29,59 @@ import ( ) const ( - // ServiceName is the name used to register the auth service + // ServiceName is the name used to register the auth service. + // Other modules can reference this service by this name for dependency injection. ServiceName = "auth" ) -// Module implements the modular.Module interface for authentication +// Module implements the modular.Module interface for authentication. +// It provides comprehensive authentication and authorization functionality +// including JWT tokens, sessions, and OAuth2 support. +// +// The module is designed to work with pluggable stores for users and sessions, +// defaulting to in-memory implementations if external stores are not provided. type Module struct { config *Config service *Service logger modular.Logger } -// NewModule creates a new authentication module +// NewModule creates a new authentication module. +// The returned module must be registered with the application before use. +// +// Example: +// +// authModule := auth.NewModule() +// app.RegisterModule(authModule) func NewModule() modular.Module { return &Module{} } -// Name returns the module name +// Name returns the module name. +// This name is used for dependency resolution and service registration. func (m *Module) Name() string { return "auth" } -// RegisterConfig registers the module's configuration +// RegisterConfig registers the module's configuration requirements. +// This method sets up the configuration structure for the auth module, +// allowing the application to load authentication-related settings. +// +// The auth module expects configuration for: +// - JWT secret keys and token expiration +// - Session configuration (timeouts, secure flags) +// - OAuth2 provider settings +// - Password policy settings func (m *Module) RegisterConfig(app modular.Application) error { m.config = &Config{} app.RegisterConfigSection(m.Name(), modular.NewStdConfigProvider(m.config)) return nil } -// Init initializes the authentication module +// Init initializes the authentication module. +// This method validates the configuration and prepares the module for use. +// The actual service creation happens in the Constructor method to support +// dependency injection of user and session stores. func (m *Module) Init(app modular.Application) error { m.logger = app.Logger() @@ -49,24 +94,34 @@ func (m *Module) Init(app modular.Application) error { return nil } -// Start starts the authentication module +// Start starts the authentication module. +// Currently the auth module doesn't require any startup operations, +// but this method is available for future enhancements like background +// token cleanup or session maintenance tasks. func (m *Module) Start(ctx context.Context) error { m.logger.Info("Authentication module started", "module", m.Name()) return nil } -// Stop stops the authentication module +// Stop stops the authentication module. +// This method can be used for cleanup operations like closing database +// connections or stopping background tasks when they are added in the future. func (m *Module) Stop(ctx context.Context) error { m.logger.Info("Authentication module stopped", "module", m.Name()) return nil } -// Dependencies returns the module dependencies +// Dependencies returns the module dependencies. +// The auth module has no required module dependencies, making it suitable +// for use as a foundation module that other modules can depend on. func (m *Module) Dependencies() []string { return nil // No explicit module dependencies } -// ProvidesServices returns the services provided by this module +// ProvidesServices returns the services provided by this module. +// The auth module provides an authentication service that implements +// the AuthService interface, offering methods for user login, token +// validation, and session management. func (m *Module) ProvidesServices() []modular.ServiceProvider { return []modular.ServiceProvider{ { @@ -77,7 +132,14 @@ func (m *Module) ProvidesServices() []modular.ServiceProvider { } } -// RequiresServices returns the services required by this module +// RequiresServices returns the services required by this module. +// The auth module can optionally use external user and session stores. +// If these services are not provided, the module will fall back to +// in-memory implementations suitable for development and testing. +// +// Optional services: +// - user_store: Implementation of UserStore interface for persistent user data +// - session_store: Implementation of SessionStore interface for session persistence func (m *Module) RequiresServices() []modular.ServiceDependency { return []modular.ServiceDependency{ { @@ -91,7 +153,16 @@ func (m *Module) RequiresServices() []modular.ServiceDependency { } } -// Constructor provides dependency injection for the module +// Constructor provides dependency injection for the module. +// This method creates the authentication service with injected dependencies, +// using fallback implementations for optional services that aren't provided. +// +// The constructor pattern allows the module to be reconstructed with proper +// dependency injection after all required services have been resolved. +// +// Dependencies resolved: +// - user_store: External user storage (falls back to memory store) +// - session_store: External session storage (falls back to memory store) func (m *Module) Constructor() modular.ModuleConstructor { return func(app modular.Application, services map[string]any) (modular.Module, error) { // Get user store (use mock if not provided) diff --git a/modules/cache/config.go b/modules/cache/config.go index c8400f0b..14654ba7 100644 --- a/modules/cache/config.go +++ b/modules/cache/config.go @@ -1,24 +1,70 @@ package cache -// CacheConfig defines the configuration for the cache module +// CacheConfig defines the configuration for the cache module. +// This structure contains all the settings needed to configure both +// memory and Redis cache engines. +// +// Configuration can be provided through JSON, YAML, or environment variables. +// The struct tags define the mapping for each configuration source and +// validation rules. +// +// Example JSON configuration: +// +// { +// "engine": "redis", +// "defaultTTL": 600, +// "cleanupInterval": 300, +// "maxItems": 50000, +// "redisURL": "redis://localhost:6379", +// "redisPassword": "mypassword", +// "redisDB": 1 +// } +// +// Example environment variables: +// +// CACHE_ENGINE=memory +// CACHE_DEFAULT_TTL=300 +// CACHE_MAX_ITEMS=10000 type CacheConfig struct { - // Engine specifies the cache engine to use ("memory" or "redis") + // Engine specifies the cache engine to use. + // Supported values: "memory", "redis" + // Default: "memory" Engine string `json:"engine" yaml:"engine" env:"ENGINE" validate:"oneof=memory redis"` - // DefaultTTL is the default time-to-live for cache entries in seconds + // DefaultTTL is the default time-to-live for cache entries in seconds. + // Used when no explicit TTL is provided in cache operations. + // Must be at least 1 second. DefaultTTL int `json:"defaultTTL" yaml:"defaultTTL" env:"DEFAULT_TTL" validate:"min=1"` - // CleanupInterval is how often to clean up expired items (in seconds) + // CleanupInterval is how often to clean up expired items (in seconds). + // Only applicable to memory cache engine. + // Must be at least 1 second. CleanupInterval int `json:"cleanupInterval" yaml:"cleanupInterval" env:"CLEANUP_INTERVAL" validate:"min=1"` - // MaxItems is the maximum number of items to store in memory cache + // MaxItems is the maximum number of items to store in memory cache. + // When this limit is reached, least recently used items are evicted. + // Only applicable to memory cache engine. + // Must be at least 1. MaxItems int `json:"maxItems" yaml:"maxItems" env:"MAX_ITEMS" validate:"min=1"` - // Redis-specific configuration - RedisURL string `json:"redisURL" yaml:"redisURL" env:"REDIS_URL"` + // RedisURL is the connection URL for Redis server. + // Format: redis://[username:password@]host:port[/database] + // Only required when using Redis engine. + // Example: "redis://localhost:6379", "redis://user:pass@localhost:6379/1" + RedisURL string `json:"redisURL" yaml:"redisURL" env:"REDIS_URL"` + + // RedisPassword is the password for Redis authentication. + // Optional if Redis server doesn't require authentication. RedisPassword string `json:"redisPassword" yaml:"redisPassword" env:"REDIS_PASSWORD"` - RedisDB int `json:"redisDB" yaml:"redisDB" env:"REDIS_DB" validate:"min=0"` - // ConnectionMaxAge is the maximum age of a connection in seconds + // RedisDB is the Redis database number to use. + // Redis supports multiple databases (0-15 by default). + // Must be non-negative. + RedisDB int `json:"redisDB" yaml:"redisDB" env:"REDIS_DB" validate:"min=0"` + + // ConnectionMaxAge is the maximum age of a connection in seconds. + // Connections older than this will be closed and recreated. + // Helps prevent connection staleness in long-running applications. + // Must be at least 1 second. ConnectionMaxAge int `json:"connectionMaxAge" yaml:"connectionMaxAge" env:"CONNECTION_MAX_AGE" validate:"min=1"` } diff --git a/modules/cache/engine.go b/modules/cache/engine.go index 383703ae..6cd55f7e 100644 --- a/modules/cache/engine.go +++ b/modules/cache/engine.go @@ -5,32 +5,96 @@ import ( "time" ) -// CacheEngine defines the interface for cache engine implementations +// CacheEngine defines the interface for cache engine implementations. +// This interface abstracts the underlying storage mechanism, allowing +// the cache module to support multiple backends (memory, Redis, etc.) +// through a common API. +// +// All operations are context-aware to support cancellation and timeouts. +// Implementations should be thread-safe and handle concurrent access properly. +// +// Cache engines are responsible for: +// - Connection management to the underlying storage +// - Data serialization/deserialization +// - TTL handling and expiration +// - Error handling and recovery type CacheEngine interface { - // Connect establishes connection to the cache backend + // Connect establishes connection to the cache backend. + // This method is called during module startup and should prepare + // the engine for cache operations. For memory caches, this might + // initialize internal data structures. For network-based caches + // like Redis, this establishes the connection pool. + // + // The context can be used to handle connection timeouts. Connect(ctx context.Context) error - // Close closes the connection to the cache backend + // Close closes the connection to the cache backend. + // This method is called during module shutdown and should cleanup + // all resources, close network connections, and stop background + // processes. The method should be idempotent - safe to call multiple times. + // + // The context can be used to handle graceful shutdown timeouts. Close(ctx context.Context) error - // Get retrieves an item from the cache + // Get retrieves an item from the cache. + // Returns the cached value and a boolean indicating whether the key was found. + // If the key doesn't exist or has expired, returns (nil, false). + // + // The returned value should be the same type that was stored. + // The context can be used for operation timeouts. Get(ctx context.Context, key string) (interface{}, bool) - // Set stores an item in the cache with a TTL + // Set stores an item in the cache with a TTL. + // The value can be any serializable type. The TTL determines how long + // the item should remain in the cache before expiring. + // + // If TTL is 0 or negative, the item should use the default TTL or + // never expire, depending on the implementation. + // + // The context can be used for operation timeouts. Set(ctx context.Context, key string, value interface{}, ttl time.Duration) error - // Delete removes an item from the cache + // Delete removes an item from the cache. + // Should not return an error if the key doesn't exist. + // Only returns errors for actual operation failures. + // + // The context can be used for operation timeouts. Delete(ctx context.Context, key string) error - // Flush removes all items from the cache + // Flush removes all items from the cache. + // This operation should be atomic - either all items are removed + // or none are. Should be used with caution as it's irreversible. + // + // The context can be used for operation timeouts. Flush(ctx context.Context) error - // GetMulti retrieves multiple items from the cache + // GetMulti retrieves multiple items from the cache in a single operation. + // Returns a map containing only the keys that were found. + // Missing or expired keys are not included in the result. + // + // This operation should be more efficient than multiple Get calls + // for network-based caches. + // + // The context can be used for operation timeouts. GetMulti(ctx context.Context, keys []string) (map[string]interface{}, error) - // SetMulti stores multiple items in the cache with a TTL + // SetMulti stores multiple items in the cache with a TTL. + // All items use the same TTL value. This operation should be atomic + // where possible - either all items are stored or none are. + // + // This operation should be more efficient than multiple Set calls + // for network-based caches. + // + // The context can be used for operation timeouts. SetMulti(ctx context.Context, items map[string]interface{}, ttl time.Duration) error - // DeleteMulti removes multiple items from the cache + // DeleteMulti removes multiple items from the cache. + // Should not return an error for keys that don't exist. + // Only returns errors for actual operation failures. + // + // This operation should be more efficient than multiple Delete calls + // for network-based caches. + // + // The context can be used for operation timeouts. DeleteMulti(ctx context.Context, keys []string) error } diff --git a/modules/cache/module.go b/modules/cache/module.go index 609c38f9..d846e3dc 100644 --- a/modules/cache/module.go +++ b/modules/cache/module.go @@ -1,3 +1,66 @@ +// Package cache provides a flexible caching module for the modular framework. +// +// This module supports multiple cache backends including in-memory and Redis, +// with configurable TTL, cleanup intervals, and connection management. It provides +// a unified interface for caching operations across different storage engines. +// +// # Supported Cache Engines +// +// The cache module supports the following engines: +// - "memory": In-memory cache with LRU eviction and TTL support +// - "redis": Redis-based cache with connection pooling and persistence +// +// # Configuration +// +// The module can be configured through the CacheConfig structure: +// +// config := &CacheConfig{ +// Engine: "memory", // or "redis" +// DefaultTTL: 300, // 5 minutes default TTL +// CleanupInterval: 60, // cleanup every minute +// MaxItems: 10000, // max items for memory cache +// RedisURL: "redis://localhost:6379", // for Redis engine +// RedisPassword: "", // Redis password if required +// RedisDB: 0, // Redis database number +// ConnectionMaxAge: 60, // connection max age in seconds +// } +// +// # Service Registration +// +// The module registers itself as a service that can be injected into other modules: +// +// // Get the cache service +// cacheService := app.GetService("cache.provider").(*CacheModule) +// +// // Use the cache +// err := cacheService.Set(ctx, "key", "value", time.Minute*5) +// value, found := cacheService.Get(ctx, "key") +// +// # Usage Examples +// +// Basic caching operations: +// +// // Set a value with default TTL +// err := cache.Set(ctx, "user:123", userData, 0) +// +// // Set a value with custom TTL +// err := cache.Set(ctx, "session:abc", sessionData, time.Hour) +// +// // Get a value +// value, found := cache.Get(ctx, "user:123") +// if found { +// user := value.(UserData) +// // use user data +// } +// +// // Batch operations +// items := map[string]interface{}{ +// "key1": "value1", +// "key2": "value2", +// } +// err := cache.SetMulti(ctx, items, time.Minute*10) +// +// results, err := cache.GetMulti(ctx, []string{"key1", "key2"}) package cache import ( @@ -8,13 +71,26 @@ import ( "github.com/GoCodeAlone/modular" ) -// ModuleName is the name of this module +// ModuleName is the unique identifier for the cache module. const ModuleName = "cache" -// ServiceName is the name of the service provided by this module +// ServiceName is the name of the service provided by this module. +// Other modules can use this name to request the cache service through dependency injection. const ServiceName = "cache.provider" -// CacheModule represents the cache module +// CacheModule provides caching functionality for the modular framework. +// It supports multiple cache backends (memory and Redis) and provides a unified +// interface for caching operations including TTL management, batch operations, +// and automatic cleanup. +// +// The module implements the following interfaces: +// - modular.Module: Basic module lifecycle +// - modular.Configurable: Configuration management +// - modular.ServiceAware: Service dependency management +// - modular.Startable: Startup logic +// - modular.Stoppable: Shutdown logic +// +// Cache operations are thread-safe and support context cancellation. type CacheModule struct { name string config *CacheConfig @@ -22,19 +98,36 @@ type CacheModule struct { cacheEngine CacheEngine } -// NewModule creates a new instance of the cache module +// NewModule creates a new instance of the cache module. +// This is the primary constructor for the cache module and should be used +// when registering the module with the application. +// +// Example: +// +// app.RegisterModule(cache.NewModule()) func NewModule() modular.Module { return &CacheModule{ name: ModuleName, } } -// Name returns the name of the module +// Name returns the unique identifier for this module. +// This name is used for service registration, dependency resolution, +// and configuration section identification. func (m *CacheModule) Name() string { return m.name } -// RegisterConfig registers the module's configuration structure +// RegisterConfig registers the module's configuration structure. +// This method is called during application initialization to register +// the default configuration values for the cache module. +// +// Default configuration: +// - Engine: "memory" +// - DefaultTTL: 300 seconds (5 minutes) +// - CleanupInterval: 60 seconds (1 minute) +// - MaxItems: 10000 +// - Redis settings: empty/default values func (m *CacheModule) RegisterConfig(app modular.Application) error { // Register the configuration with default values defaultConfig := &CacheConfig{ @@ -52,7 +145,20 @@ func (m *CacheModule) RegisterConfig(app modular.Application) error { return nil } -// Init initializes the module +// Init initializes the cache module with the application context. +// This method is called after all modules have been registered and their +// configurations loaded. It sets up the cache engine based on the configuration. +// +// The initialization process: +// 1. Retrieves the module's configuration +// 2. Sets up logging +// 3. Initializes the appropriate cache engine (memory or Redis) +// 4. Logs the initialization status +// +// Supported cache engines: +// - "memory": In-memory cache with LRU eviction +// - "redis": Redis-based distributed cache +// - fallback: defaults to memory cache for unknown engines func (m *CacheModule) Init(app modular.Application) error { // Retrieve the registered config section for access cfg, err := app.GetConfigSection(m.name) @@ -80,7 +186,12 @@ func (m *CacheModule) Init(app modular.Application) error { return nil } -// Start performs startup logic for the module +// Start performs startup logic for the module. +// This method establishes connections to the cache backend and prepares +// the cache for operations. It's called after all modules have been initialized. +// +// For memory cache: No external connections are needed +// For Redis cache: Establishes connection pool to the Redis server func (m *CacheModule) Start(ctx context.Context) error { m.logger.Info("Starting cache module") err := m.cacheEngine.Connect(ctx) @@ -90,7 +201,14 @@ func (m *CacheModule) Start(ctx context.Context) error { return nil } -// Stop performs shutdown logic for the module +// Stop performs shutdown logic for the module. +// This method gracefully closes all connections and cleans up resources. +// It's called during application shutdown to ensure proper cleanup. +// +// The shutdown process: +// 1. Logs the shutdown initiation +// 2. Closes cache engine connections +// 3. Cleans up any background processes func (m *CacheModule) Stop(ctx context.Context) error { m.logger.Info("Stopping cache module") if err := m.cacheEngine.Close(ctx); err != nil { @@ -99,12 +217,17 @@ func (m *CacheModule) Stop(ctx context.Context) error { return nil } -// Dependencies returns the names of modules this module depends on +// Dependencies returns the names of modules this module depends on. +// The cache module has no dependencies and can be started independently. func (m *CacheModule) Dependencies() []string { return nil } -// ProvidesServices declares services provided by this module +// ProvidesServices declares services provided by this module. +// The cache module provides a cache service that can be injected into other modules. +// +// Provided services: +// - "cache.provider": The main cache service interface func (m *CacheModule) ProvidesServices() []modular.ServiceProvider { return []modular.ServiceProvider{ { @@ -115,24 +238,47 @@ func (m *CacheModule) ProvidesServices() []modular.ServiceProvider { } } -// RequiresServices declares services required by this module +// RequiresServices declares services required by this module. +// The cache module operates independently and requires no external services. func (m *CacheModule) RequiresServices() []modular.ServiceDependency { return nil } -// Constructor provides a dependency injection constructor for the module +// Constructor provides a dependency injection constructor for the module. +// This method is used by the dependency injection system to create +// the module instance with any required services. func (m *CacheModule) Constructor() modular.ModuleConstructor { return func(app modular.Application, services map[string]any) (modular.Module, error) { return m, nil } } -// Get retrieves a cached item by key +// Get retrieves a cached item by key. +// Returns the cached value and a boolean indicating whether the key was found. +// If the key doesn't exist or has expired, returns (nil, false). +// +// Example: +// +// value, found := cache.Get(ctx, "user:123") +// if found { +// user := value.(UserData) +// // process user data +// } func (m *CacheModule) Get(ctx context.Context, key string) (interface{}, bool) { return m.cacheEngine.Get(ctx, key) } -// Set stores an item in the cache with an optional TTL +// Set stores an item in the cache with an optional TTL. +// If ttl is 0, uses the default TTL from configuration. +// The value can be any serializable type. +// +// Example: +// +// // Use default TTL +// err := cache.Set(ctx, "user:123", userData, 0) +// +// // Use custom TTL +// err := cache.Set(ctx, "session:abc", sessionData, time.Hour) func (m *CacheModule) Set(ctx context.Context, key string, value interface{}, ttl time.Duration) error { if ttl == 0 { ttl = time.Duration(m.config.DefaultTTL) * time.Second @@ -143,7 +289,15 @@ func (m *CacheModule) Set(ctx context.Context, key string, value interface{}, tt return nil } -// Delete removes an item from the cache +// Delete removes an item from the cache. +// Returns an error if the deletion fails, but not if the key doesn't exist. +// +// Example: +// +// err := cache.Delete(ctx, "user:123") +// if err != nil { +// // handle deletion error +// } func (m *CacheModule) Delete(ctx context.Context, key string) error { if err := m.cacheEngine.Delete(ctx, key); err != nil { return fmt.Errorf("failed to delete cache item: %w", err) @@ -151,7 +305,16 @@ func (m *CacheModule) Delete(ctx context.Context, key string) error { return nil } -// Flush removes all items from the cache +// Flush removes all items from the cache. +// This operation is irreversible and should be used with caution. +// Useful for cache invalidation or testing scenarios. +// +// Example: +// +// err := cache.Flush(ctx) +// if err != nil { +// // handle flush error +// } func (m *CacheModule) Flush(ctx context.Context) error { if err := m.cacheEngine.Flush(ctx); err != nil { return fmt.Errorf("failed to flush cache: %w", err) @@ -159,7 +322,20 @@ func (m *CacheModule) Flush(ctx context.Context) error { return nil } -// GetMulti retrieves multiple items from the cache +// GetMulti retrieves multiple items from the cache in a single operation. +// Returns a map of key-value pairs for found items and an error if the operation fails. +// Missing keys are simply not included in the result map. +// +// Example: +// +// keys := []string{"user:123", "user:456", "user:789"} +// results, err := cache.GetMulti(ctx, keys) +// if err != nil { +// // handle error +// } +// for key, value := range results { +// // process found values +// } func (m *CacheModule) GetMulti(ctx context.Context, keys []string) (map[string]interface{}, error) { result, err := m.cacheEngine.GetMulti(ctx, keys) if err != nil { @@ -168,7 +344,18 @@ func (m *CacheModule) GetMulti(ctx context.Context, keys []string) (map[string]i return result, nil } -// SetMulti stores multiple items in the cache +// SetMulti stores multiple items in the cache in a single operation. +// All items will use the same TTL. If ttl is 0, uses the default TTL from configuration. +// This is more efficient than multiple Set calls for batch operations. +// +// Example: +// +// items := map[string]interface{}{ +// "user:123": userData1, +// "user:456": userData2, +// "session:abc": sessionData, +// } +// err := cache.SetMulti(ctx, items, time.Minute*30) func (m *CacheModule) SetMulti(ctx context.Context, items map[string]interface{}, ttl time.Duration) error { if ttl == 0 { ttl = time.Duration(m.config.DefaultTTL) * time.Second @@ -179,7 +366,17 @@ func (m *CacheModule) SetMulti(ctx context.Context, items map[string]interface{} return nil } -// DeleteMulti removes multiple items from the cache +// DeleteMulti removes multiple items from the cache in a single operation. +// This is more efficient than multiple Delete calls for batch operations. +// Does not return an error for keys that don't exist. +// +// Example: +// +// keys := []string{"user:123", "user:456", "expired:key"} +// err := cache.DeleteMulti(ctx, keys) +// if err != nil { +// // handle deletion error +// } func (m *CacheModule) DeleteMulti(ctx context.Context, keys []string) error { if err := m.cacheEngine.DeleteMulti(ctx, keys); err != nil { return fmt.Errorf("failed to delete multiple cache items: %w", err) diff --git a/modules/chimux/config.go b/modules/chimux/config.go index 324c7e89..a6ee9f9c 100644 --- a/modules/chimux/config.go +++ b/modules/chimux/config.go @@ -1,17 +1,91 @@ package chimux -// ChiMuxConfig holds the configuration for the chimux module +// ChiMuxConfig holds the configuration for the chimux module. +// This structure contains all the settings needed to configure CORS, +// request handling, and routing behavior for the Chi router. +// +// Configuration can be provided through JSON, YAML, or environment variables. +// The struct tags define the mapping for each configuration source and +// default values. +// +// Example YAML configuration: +// +// allowed_origins: +// - "https://example.com" +// - "https://app.example.com" +// allowed_methods: +// - "GET" +// - "POST" +// - "PUT" +// - "DELETE" +// allowed_headers: +// - "Origin" +// - "Accept" +// - "Content-Type" +// - "Authorization" +// allow_credentials: true +// max_age: 3600 +// timeout: 30000 +// basepath: "/api/v1" +// +// Example environment variables: +// +// CHIMUX_ALLOWED_ORIGINS=https://example.com,https://app.example.com +// CHIMUX_ALLOW_CREDENTIALS=true +// CHIMUX_BASE_PATH=/api/v1 type ChiMuxConfig struct { - AllowedOrigins []string `yaml:"allowed_origins" default:"[\"*\"]" desc:"List of allowed origins for CORS requests." env:"ALLOWED_ORIGINS"` // List of allowed origins for CORS requests. - AllowedMethods []string `yaml:"allowed_methods" default:"[\"GET\",\"POST\",\"PUT\",\"DELETE\",\"OPTIONS\"]" desc:"List of allowed HTTP methods." env:"ALLOWED_METHODS"` // List of allowed HTTP methods. - AllowedHeaders []string `yaml:"allowed_headers" default:"[\"Origin\",\"Accept\",\"Content-Type\",\"X-Requested-With\",\"Authorization\"]" desc:"List of allowed request headers." env:"ALLOWED_HEADERS"` // List of allowed request headers. - AllowCredentials bool `yaml:"allow_credentials" default:"false" desc:"Allow credentials in CORS requests." env:"ALLOW_CREDENTIALS"` // Allow credentials in CORS requests. - MaxAge int `yaml:"max_age" default:"300" desc:"Maximum age for CORS preflight cache in seconds." env:"MAX_AGE"` // Maximum age for CORS preflight cache in seconds. - Timeout int `yaml:"timeout" default:"60000" desc:"Default request timeout." env:"TIMEOUT"` // Default request timeout. - BasePath string `yaml:"basepath" desc:"A base path prefix for all routes registered through this module." env:"BASE_PATH"` // A base path prefix for all routes registered through this module. + // AllowedOrigins specifies the list of allowed origins for CORS requests. + // Use ["*"] to allow all origins, or specify exact origins for security. + // Multiple origins can be specified for multi-domain applications. + // Default: ["*"] + AllowedOrigins []string `yaml:"allowed_origins" default:"[\"*\"]" desc:"List of allowed origins for CORS requests." env:"ALLOWED_ORIGINS"` + + // AllowedMethods specifies the list of allowed HTTP methods for CORS requests. + // This controls which HTTP methods browsers are allowed to use in + // cross-origin requests. Common methods include GET, POST, PUT, DELETE, OPTIONS. + // Default: ["GET", "POST", "PUT", "DELETE", "OPTIONS"] + AllowedMethods []string `yaml:"allowed_methods" default:"[\"GET\",\"POST\",\"PUT\",\"DELETE\",\"OPTIONS\"]" desc:"List of allowed HTTP methods." env:"ALLOWED_METHODS"` + + // AllowedHeaders specifies the list of allowed request headers for CORS requests. + // This controls which headers browsers are allowed to send in cross-origin requests. + // Common headers include Origin, Accept, Content-Type, Authorization. + // Default: ["Origin", "Accept", "Content-Type", "X-Requested-With", "Authorization"] + AllowedHeaders []string `yaml:"allowed_headers" default:"[\"Origin\",\"Accept\",\"Content-Type\",\"X-Requested-With\",\"Authorization\"]" desc:"List of allowed request headers." env:"ALLOWED_HEADERS"` + + // AllowCredentials determines whether cookies, authorization headers, + // and TLS client certificates are allowed in CORS requests. + // Set to true when your API needs to handle authenticated cross-origin requests. + // Default: false + AllowCredentials bool `yaml:"allow_credentials" default:"false" desc:"Allow credentials in CORS requests." env:"ALLOW_CREDENTIALS"` + + // MaxAge specifies the maximum age for CORS preflight cache in seconds. + // This controls how long browsers can cache preflight request results, + // reducing the number of preflight requests for repeated cross-origin calls. + // Default: 300 (5 minutes) + MaxAge int `yaml:"max_age" default:"300" desc:"Maximum age for CORS preflight cache in seconds." env:"MAX_AGE"` + + // Timeout specifies the default request timeout in milliseconds. + // This sets a default timeout for request processing, though individual + // handlers may override this with their own timeout logic. + // Default: 60000 (60 seconds) + Timeout int `yaml:"timeout" default:"60000" desc:"Default request timeout." env:"TIMEOUT"` + + // BasePath specifies a base path prefix for all routes registered through this module. + // When set, all routes will be prefixed with this path. Useful for mounting + // the application under a sub-path or for API versioning. + // Example: "/api/v1" would make a route "/users" accessible as "/api/v1/users" + // Default: "" (no prefix) + BasePath string `yaml:"basepath" desc:"A base path prefix for all routes registered through this module." env:"BASE_PATH"` } -// Validate implements the modular.ConfigValidator interface +// Validate implements the modular.ConfigValidator interface. +// This method is called during configuration loading to ensure +// the configuration values are valid and consistent. +// +// Currently performs basic validation but can be extended to include: +// - URL validation for allowed origins +// - Timeout range validation +// - Base path format validation func (c *ChiMuxConfig) Validate() error { // Add custom validation logic here return nil diff --git a/modules/chimux/module.go b/modules/chimux/module.go index 31515438..334ce43e 100644 --- a/modules/chimux/module.go +++ b/modules/chimux/module.go @@ -1,3 +1,85 @@ +// Package chimux provides a Chi-based HTTP router module for the modular framework. +// +// This module wraps the popular Go Chi router and integrates it with the modular +// framework's service system, providing HTTP routing, middleware management, CORS +// support, and tenant-aware configuration. +// +// # Features +// +// The chimux module offers the following capabilities: +// - HTTP routing with pattern matching and parameter extraction +// - Middleware chain management with automatic service discovery +// - CORS configuration with per-tenant customization +// - Base path support for sub-application mounting +// - Tenant-aware configuration for multi-tenant applications +// - Service registration for dependency injection +// +// # Requirements +// +// The chimux module requires a TenantApplication to operate. It will return an +// error if initialized with a regular Application instance. +// +// # Configuration +// +// The module can be configured through the ChiMuxConfig structure: +// +// config := &ChiMuxConfig{ +// AllowedOrigins: []string{"https://example.com", "https://app.example.com"}, +// AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, +// AllowedHeaders: []string{"Origin", "Accept", "Content-Type", "Authorization"}, +// AllowCredentials: true, +// MaxAge: 3600, +// Timeout: 30000, +// BasePath: "/api/v1", +// } +// +// # Service Registration +// +// The module registers multiple services for different use cases: +// - "chimux.router": The full ChiMuxModule instance +// - "router": BasicRouter interface for simple routing needs +// - "chi.router": Direct access to the underlying Chi router +// +// # Usage Examples +// +// Basic routing: +// +// router := app.GetService("router").(chimux.BasicRouter) +// router.Get("/users", getUsersHandler) +// router.Post("/users", createUserHandler) +// router.Get("/users/{id}", getUserHandler) +// +// Advanced routing with Chi features: +// +// chiRouter := app.GetService("chi.router").(chi.Router) +// chiRouter.Route("/api", func(r chi.Router) { +// r.Use(authMiddleware) +// r.Get("/protected", protectedHandler) +// }) +// +// Middleware integration: +// +// // Modules implementing MiddlewareProvider are automatically discovered +// type MyModule struct{} +// +// func (m *MyModule) ProvideMiddleware() []chimux.Middleware { +// return []chimux.Middleware{ +// myCustomMiddleware, +// loggingMiddleware, +// } +// } +// +// # Tenant Support +// +// The module supports tenant-specific configurations: +// +// // Different tenants can have different CORS settings +// tenant1Config := &ChiMuxConfig{ +// AllowedOrigins: []string{"https://tenant1.example.com"}, +// } +// tenant2Config := &ChiMuxConfig{ +// AllowedOrigins: []string{"https://tenant2.example.com"}, +// } package chimux import ( @@ -14,19 +96,37 @@ import ( "github.com/go-chi/chi/v5/middleware" ) -// ModuleName is the name of this module +// ModuleName is the unique identifier for the chimux module. const ModuleName = "chimux" -// ServiceName is the name of the service provided by this module (the chi router) +// ServiceName is the name of the primary service provided by this module. +// Use this to request the chimux router service through dependency injection. const ServiceName = "chimux.router" -// Error definitions +// Error definitions for the chimux module. var ( - // ErrRequiresTenantApplication is returned when the module is initialized with a non-tenant application + // ErrRequiresTenantApplication is returned when the module is initialized + // with a non-tenant application. The chimux module requires tenant support + // for proper multi-tenant routing and configuration. ErrRequiresTenantApplication = errors.New("chimux module requires a TenantApplication") ) -// ChiMuxModule represents the chimux module +// ChiMuxModule provides HTTP routing functionality using the Chi router library. +// It integrates Chi with the modular framework's service system and provides +// tenant-aware configuration, middleware management, and CORS support. +// +// The module implements the following interfaces: +// - modular.Module: Basic module lifecycle +// - modular.Configurable: Configuration management +// - modular.ServiceAware: Service dependency management +// - modular.Startable: Startup logic +// - modular.Stoppable: Shutdown logic +// - modular.TenantAwareModule: Tenant lifecycle management +// - BasicRouter: Basic HTTP routing +// - Router: Extended Chi router functionality +// - ChiRouterService: Direct Chi router access +// +// The router is thread-safe and supports concurrent request handling. type ChiMuxModule struct { name string config *ChiMuxConfig @@ -36,7 +136,13 @@ type ChiMuxModule struct { logger modular.Logger } -// NewChiMuxModule creates a new instance of the chimux module +// NewChiMuxModule creates a new instance of the chimux module. +// This is the primary constructor for the chimux module and should be used +// when registering the module with the application. +// +// Example: +// +// app.RegisterModule(chimux.NewChiMuxModule()) func NewChiMuxModule() modular.Module { return &ChiMuxModule{ name: ModuleName, @@ -44,12 +150,24 @@ func NewChiMuxModule() modular.Module { } } -// Name returns the name of the module +// Name returns the unique identifier for this module. +// This name is used for service registration, dependency resolution, +// and configuration section identification. func (m *ChiMuxModule) Name() string { return m.name } -// RegisterConfig registers the module's configuration structure +// RegisterConfig registers the module's configuration structure. +// This method is called during application initialization to register +// the default configuration values for the chimux module. +// +// Default configuration: +// - AllowedOrigins: ["*"] (all origins allowed) +// - AllowedMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"] +// - AllowedHeaders: ["Origin", "Accept", "Content-Type", "X-Requested-With", "Authorization"] +// - AllowCredentials: false +// - MaxAge: 300 seconds (5 minutes) +// - Timeout: 60000 milliseconds (60 seconds) func (m *ChiMuxModule) RegisterConfig(app modular.Application) error { // Register the configuration with default values defaultConfig := &ChiMuxConfig{ @@ -66,7 +184,22 @@ func (m *ChiMuxModule) RegisterConfig(app modular.Application) error { return nil } -// Init initializes the module +// Init initializes the chimux module with the application context. +// This method is called after all modules have been registered and their +// configurations loaded. It sets up the Chi router, applies middleware, +// and configures CORS settings. +// +// The initialization process: +// 1. Validates that the application supports tenants +// 2. Loads the module configuration +// 3. Creates and configures the Chi router +// 4. Sets up default middleware (RequestID, RealIP, Logger, Recoverer) +// 5. Applies CORS middleware based on configuration +// 6. Discovers and applies middleware from other modules +// +// Requirements: +// - Must be used with a TenantApplication +// - Configuration must be properly loaded func (m *ChiMuxModule) Init(app modular.Application) error { if err := m.initApplication(app); err != nil { return err @@ -176,7 +309,15 @@ func (m *ChiMuxModule) setupMiddleware(app modular.Application) error { return nil } -// Start performs startup logic for the module +// Start performs startup logic for the module. +// This method loads tenant-specific configurations that may have been +// registered after module initialization. It's called after all modules +// have been initialized and are ready to start. +// +// The startup process: +// 1. Loads configurations for all registered tenants +// 2. Applies tenant-specific CORS and routing settings +// 3. Prepares the router for incoming requests func (m *ChiMuxModule) Start(context.Context) error { m.logger.Info("Starting chimux module") @@ -186,18 +327,31 @@ func (m *ChiMuxModule) Start(context.Context) error { return nil } -// Stop performs shutdown logic for the module +// Stop performs shutdown logic for the module. +// This method gracefully shuts down the router and cleans up resources. +// Note that the HTTP server itself is typically managed by a separate +// HTTP server module. func (m *ChiMuxModule) Stop(context.Context) error { m.logger.Info("Stopping chimux module") return nil } -// Dependencies returns the names of modules this module depends on +// Dependencies returns the names of modules this module depends on. +// The chimux module has no hard dependencies and can be started independently. +// However, it will automatically discover and integrate with modules that +// implement MiddlewareProvider. func (m *ChiMuxModule) Dependencies() []string { return nil } -// ProvidesServices declares services provided by this module +// ProvidesServices declares services provided by this module. +// The chimux module provides multiple service interfaces to accommodate +// different usage patterns and integration needs. +// +// Provided services: +// - "chimux.router": The full ChiMuxModule instance +// - "router": BasicRouter interface for simple routing needs +// - "chi.router": Direct access to the underlying Chi router func (m *ChiMuxModule) ProvidesServices() []modular.ServiceProvider { return []modular.ServiceProvider{ { @@ -218,7 +372,10 @@ func (m *ChiMuxModule) ProvidesServices() []modular.ServiceProvider { } } -// RequiresServices declares services required by this module +// RequiresServices declares services required by this module. +// The chimux module optionally depends on middleware providers. +// It will automatically discover and integrate with any modules +// that implement the MiddlewareProvider interface. func (m *ChiMuxModule) RequiresServices() []modular.ServiceDependency { return []modular.ServiceDependency{ { @@ -230,14 +387,21 @@ func (m *ChiMuxModule) RequiresServices() []modular.ServiceDependency { } } -// Constructor provides a dependency injection constructor for the module +// Constructor provides a dependency injection constructor for the module. +// This method is used by the dependency injection system to create +// the module instance with any required services. func (m *ChiMuxModule) Constructor() modular.ModuleConstructor { return func(app modular.Application, services map[string]any) (modular.Module, error) { return m, nil } } -// OnTenantRegistered is called when a new tenant is registered +// OnTenantRegistered is called when a new tenant is registered. +// This method is part of the TenantAwareModule interface and allows +// the chimux module to prepare tenant-specific configurations. +// +// The actual configuration loading is deferred to avoid deadlocks +// during the tenant registration process. func (m *ChiMuxModule) OnTenantRegistered(tenantID modular.TenantID) { m.logger.Info("Tenant registered in chimux module", "tenantID", tenantID) @@ -246,14 +410,19 @@ func (m *ChiMuxModule) OnTenantRegistered(tenantID modular.TenantID) { m.tenantConfigs[tenantID] = nil } -// OnTenantRemoved is called when a tenant is removed +// OnTenantRemoved is called when a tenant is removed. +// This method cleans up any tenant-specific configurations and resources. func (m *ChiMuxModule) OnTenantRemoved(tenantID modular.TenantID) { m.logger.Info("Tenant removed from chimux module", "tenantID", tenantID) delete(m.tenantConfigs, tenantID) } -// GetTenantConfig retrieves the loaded configuration for a specific tenant -// Returns the base config if no specific tenant config is found. +// GetTenantConfig retrieves the loaded configuration for a specific tenant. +// Returns the tenant-specific configuration if available, or the base +// configuration as a fallback. +// +// This method is useful for modules that need to access tenant-specific +// router configurations at runtime. func (m *ChiMuxModule) GetTenantConfig(tenantID modular.TenantID) *ChiMuxConfig { if cfg, ok := m.tenantConfigs[tenantID]; ok { return cfg diff --git a/modules/chimux/router.go b/modules/chimux/router.go index 6f44ac61..4e2c37db 100644 --- a/modules/chimux/router.go +++ b/modules/chimux/router.go @@ -6,50 +6,116 @@ import ( "github.com/go-chi/chi/v5" ) -// ChiRouterService defines the interface for working with the Chi router +// ChiRouterService defines the interface for working with the Chi router. +// This interface provides direct access to the underlying Chi router instance +// for modules that need advanced Chi-specific functionality. type ChiRouterService interface { - // Direct access to the underlying chi router + // ChiRouter returns the underlying chi.Router instance. + // Use this when you need access to Chi's advanced features like + // Route, Group, or other Chi-specific methods. ChiRouter() chi.Router } -// Middleware is an alias for the chi middleware handler function +// Middleware is an alias for the Chi middleware handler function. +// This type represents a middleware function that can be applied to routes. type Middleware func(http.Handler) http.Handler -// MiddlewareProvider defines a service that provides middleware for the chimux router +// MiddlewareProvider defines a service that provides middleware for the chimux router. +// Modules implementing this interface will have their middleware automatically +// discovered and applied to the router during initialization. +// +// Example implementation: +// +// type AuthModule struct{} +// +// func (a *AuthModule) ProvideMiddleware() []chimux.Middleware { +// return []chimux.Middleware{ +// authenticationMiddleware, +// authorizationMiddleware, +// } +// } type MiddlewareProvider interface { + // ProvideMiddleware returns a slice of middleware functions that should + // be applied to the router. The middleware will be applied in the order + // returned by this method. ProvideMiddleware() []Middleware } -// BasicRouter defines the essential router interface that most modules need -// This interface avoids the Route/Group methods that are problematic for interface abstraction +// BasicRouter defines the essential router interface that most modules need. +// This interface provides access to HTTP method handlers and basic routing +// functionality without exposing Chi-specific methods that can be problematic +// for interface abstraction. +// +// Use this interface when you need simple routing functionality and don't +// require Chi's advanced features like Route groups or sub-routers. type BasicRouter interface { - // HTTP method handlers + // HTTP method handlers for registering route handlers + + // Get registers a GET handler for the specified pattern. + // The pattern supports Chi's URL parameter syntax: "/users/{id}" Get(pattern string, handler http.HandlerFunc) + + // Post registers a POST handler for the specified pattern. Post(pattern string, handler http.HandlerFunc) + + // Put registers a PUT handler for the specified pattern. Put(pattern string, handler http.HandlerFunc) + + // Delete registers a DELETE handler for the specified pattern. Delete(pattern string, handler http.HandlerFunc) + + // Patch registers a PATCH handler for the specified pattern. Patch(pattern string, handler http.HandlerFunc) + + // Head registers a HEAD handler for the specified pattern. Head(pattern string, handler http.HandlerFunc) + + // Options registers an OPTIONS handler for the specified pattern. Options(pattern string, handler http.HandlerFunc) - // Generic handlers + // Generic handlers for registering any HTTP handler + + // Handle registers a generic HTTP handler for the specified pattern. + // Use this when you need to handle multiple HTTP methods in one handler + // or when working with existing http.Handler implementations. Handle(pattern string, handler http.Handler) + + // HandleFunc registers a generic HTTP handler function for the specified pattern. HandleFunc(pattern string, handler http.HandlerFunc) - // Mounting and middleware + // Mounting and middleware support + + // Mount attaches another http.Handler at the specified pattern. + // This is useful for mounting sub-applications or third-party handlers. + // The mounted handler will receive requests with the mount pattern stripped. Mount(pattern string, handler http.Handler) + + // Use appends one or more middleware functions to the middleware chain. + // Middleware is applied in the order it's added and affects all routes + // registered after the middleware is added. Use(middlewares ...func(http.Handler) http.Handler) - // HTTP handler + // HTTP handler interface + + // ServeHTTP implements the http.Handler interface, allowing the router + // to be used directly as an HTTP handler or mounted in other routers. ServeHTTP(w http.ResponseWriter, r *http.Request) } -// Router extends BasicRouter with Chi's actual interface -// This allows modules that need Route/Group to access them directly +// Router extends BasicRouter with Chi's full router interface. +// This interface provides access to all Chi router functionality including +// Route groups, sub-routers, and advanced routing features. +// +// Use this interface when you need Chi's advanced features like: +// - Route groups with shared middleware +// - Sub-routers with isolated middleware stacks +// - Advanced routing patterns and matching type Router interface { BasicRouter - chi.Router // Embed Chi's actual Router interface + chi.Router // Embed Chi's actual Router interface for full functionality } -// RouterService is an alias for BasicRouter for modules that don't need Route/Group +// RouterService is an alias for BasicRouter. +// This provides a convenient service name for dependency injection +// when modules only need basic routing functionality. type RouterService = BasicRouter diff --git a/modules/database/module.go b/modules/database/module.go index 3fd0a11c..db4bd10c 100644 --- a/modules/database/module.go +++ b/modules/database/module.go @@ -1,3 +1,26 @@ +// Package database provides database connectivity and management for modular applications. +// This module supports multiple database connections with different drivers and provides +// a unified interface for database operations. +// +// The database module features: +// - Multiple database connections with configurable drivers (MySQL, PostgreSQL, SQLite, etc.) +// - Connection pooling and health monitoring +// - Default connection selection for simplified usage +// - Database service abstraction for testing and mocking +// - Instance-aware configuration for environment-specific settings +// +// Usage: +// +// app.RegisterModule(database.NewModule()) +// +// The module registers database services that provide access to sql.DB instances +// and higher-level database operations. Other modules can depend on these services +// for database access. +// +// Configuration: +// +// The module requires a "database" configuration section with connection details +// for each database instance, including driver, DSN, and connection pool settings. package database import ( @@ -14,10 +37,13 @@ var ( ErrNoDefaultService = errors.New("no default database service available") ) -// Module name constant +// Module name constant for service registration and dependency resolution. const Name = "database" -// lazyDefaultService is a wrapper that lazily resolves the default database service +// lazyDefaultService is a wrapper that lazily resolves the default database service. +// This wrapper allows the database service to be registered before the actual +// database connections are established, supporting proper dependency injection +// while maintaining lazy initialization of database resources. type lazyDefaultService struct { module *Module } @@ -183,7 +209,15 @@ func (l *lazyDefaultService) Begin() (*sql.Tx, error) { return tx, nil } -// Module represents the database module +// Module represents the database module and implements the modular.Module interface. +// It manages multiple database connections and provides services for database access. +// +// The module supports: +// - Multiple named database connections +// - Automatic connection health monitoring +// - Default connection selection +// - Service abstraction for easier testing +// - Instance-aware configuration type Module struct { config *Config connections map[string]*sql.DB @@ -196,7 +230,13 @@ var ( ErrMissingDSN = errors.New("database connection missing DSN") ) -// NewModule creates a new database module +// NewModule creates a new database module instance. +// The returned module must be registered with the application before use. +// +// Example: +// +// dbModule := database.NewModule() +// app.RegisterModule(dbModule) func NewModule() *Module { return &Module{ connections: make(map[string]*sql.DB), @@ -204,12 +244,23 @@ func NewModule() *Module { } } -// Name returns the name of the module +// Name returns the name of the module. +// This name is used for dependency resolution and configuration section lookup. func (m *Module) Name() string { return Name } -// RegisterConfig registers the module's configuration structure +// RegisterConfig registers the module's configuration structure. +// The database module uses instance-aware configuration to support +// environment-specific database connection settings. +// +// Configuration structure: +// - Default: name of the default connection to use +// - Connections: map of connection names to their configurations +// +// Environment variables: +// +// DB__DRIVER, DB__DSN, etc. func (m *Module) RegisterConfig(app modular.Application) error { // Register the configuration with default values defaultConfig := &Config{ @@ -227,7 +278,15 @@ func (m *Module) RegisterConfig(app modular.Application) error { return nil } -// Init initializes the database module +// Init initializes the database module and establishes database connections. +// This method loads the configuration, validates connection settings, +// and establishes connections to all configured databases. +// +// Initialization process: +// 1. Load configuration from the "database" section +// 2. Validate connection configurations +// 3. Create database services for each connection +// 4. Test initial connectivity func (m *Module) Init(app modular.Application) error { // Get the configuration provider, err := app.GetConfigSection(m.Name()) @@ -251,7 +310,9 @@ func (m *Module) Init(app modular.Application) error { return nil } -// Start starts the database module +// Start starts the database module and verifies connectivity. +// This method performs health checks on all database connections +// to ensure they are ready for use by other modules. func (m *Module) Start(ctx context.Context) error { // Test connections to make sure they're still alive for name, db := range m.connections { @@ -262,7 +323,9 @@ func (m *Module) Start(ctx context.Context) error { return nil } -// Stop stops the database module +// Stop stops the database module and closes all connections. +// This method gracefully closes all database connections and services, +// ensuring proper cleanup during application shutdown. func (m *Module) Stop(ctx context.Context) error { // Close all database services for name, service := range m.services { @@ -277,12 +340,19 @@ func (m *Module) Stop(ctx context.Context) error { return nil } -// Dependencies returns the names of modules this module depends on +// Dependencies returns the names of modules this module depends on. +// The database module has no dependencies, making it suitable as a +// foundation module that other modules can depend on. func (m *Module) Dependencies() []string { return nil // No dependencies } -// ProvidesServices declares services provided by this module +// ProvidesServices declares services provided by this module. +// The database module provides: +// - database.manager: Module instance for direct database management +// - database.service: Default database service for convenient access +// +// Other modules can depend on these services to access database functionality. func (m *Module) ProvidesServices() []modular.ServiceProvider { providers := []modular.ServiceProvider{ { @@ -300,12 +370,22 @@ func (m *Module) ProvidesServices() []modular.ServiceProvider { return providers } -// RequiresServices declares services required by this module +// RequiresServices declares services required by this module. +// The database module is self-contained and doesn't require +// services from other modules. func (m *Module) RequiresServices() []modular.ServiceDependency { return nil // No required services } -// GetConnection returns a database connection by name +// GetConnection returns a database connection by name. +// This method allows access to specific named database connections +// that were configured in the module's configuration. +// +// Example: +// +// if db, exists := dbModule.GetConnection("primary"); exists { +// // Use the primary database connection +// } func (m *Module) GetConnection(name string) (*sql.DB, bool) { if db, exists := m.connections[name]; exists { return db, true @@ -313,7 +393,12 @@ func (m *Module) GetConnection(name string) (*sql.DB, bool) { return nil, false } -// GetDefaultConnection returns the default database connection +// GetDefaultConnection returns the default database connection. +// The default connection is determined by the "default" field in the +// configuration. If no default is configured or the named connection +// doesn't exist, this method will return any available connection. +// +// Returns nil if no connections are available. func (m *Module) GetDefaultConnection() *sql.DB { if m.config == nil || m.config.Default == "" { return nil @@ -331,7 +416,8 @@ func (m *Module) GetDefaultConnection() *sql.DB { return nil } -// GetConnections returns a list of all available connection names +// GetConnections returns a list of all available connection names. +// This is useful for discovery and diagnostic purposes. func (m *Module) GetConnections() []string { connections := make([]string, 0, len(m.connections)) for name := range m.connections { @@ -340,7 +426,9 @@ func (m *Module) GetConnections() []string { return connections } -// GetDefaultService returns the default database service +// GetDefaultService returns the default database service. +// Similar to GetDefaultConnection, but returns a DatabaseService +// interface that provides additional functionality beyond the raw sql.DB. func (m *Module) GetDefaultService() DatabaseService { if m.config == nil || m.config.Default == "" { return nil @@ -358,7 +446,9 @@ func (m *Module) GetDefaultService() DatabaseService { return nil } -// GetService returns a database service by name +// GetService returns a database service by name. +// Database services provide a higher-level interface than raw database +// connections, including connection management and additional utilities. func (m *Module) GetService(name string) (DatabaseService, bool) { if service, exists := m.services[name]; exists { return service, true @@ -366,7 +456,9 @@ func (m *Module) GetService(name string) (DatabaseService, bool) { return nil, false } -// initializeConnections initializes the database connections based on the module's configuration +// initializeConnections initializes database connections based on the module's configuration. +// This method processes each configured connection, creates database services, +// and establishes initial connectivity to validate the configuration. func (m *Module) initializeConnections() error { // Initialize database connections if len(m.config.Connections) > 0 { diff --git a/modules/eventbus/config.go b/modules/eventbus/config.go index b8e5083b..b1eff72e 100644 --- a/modules/eventbus/config.go +++ b/modules/eventbus/config.go @@ -1,27 +1,82 @@ package eventbus -// EventBusConfig defines the configuration for the event bus module +// EventBusConfig defines the configuration for the event bus module. +// This structure contains all the settings needed to configure event processing, +// worker pools, event retention, and external broker connections. +// +// Configuration can be provided through JSON, YAML, or environment variables. +// The struct tags define the mapping for each configuration source and +// validation rules. +// +// Example YAML configuration: +// +// engine: "memory" +// maxEventQueueSize: 2000 +// defaultEventBufferSize: 20 +// workerCount: 10 +// eventTTL: 7200 +// retentionDays: 14 +// externalBrokerURL: "redis://localhost:6379" +// externalBrokerUser: "eventbus_user" +// externalBrokerPassword: "secure_password" +// +// Example environment variables: +// +// EVENTBUS_ENGINE=memory +// EVENTBUS_MAX_EVENT_QUEUE_SIZE=1000 +// EVENTBUS_WORKER_COUNT=5 type EventBusConfig struct { - // Engine specifies the event bus engine to use ("memory", "redis", "kafka", etc.) + // Engine specifies the event bus engine to use. + // Supported values: "memory", "redis", "kafka" + // Default: "memory" Engine string `json:"engine" yaml:"engine" validate:"oneof=memory redis kafka" env:"ENGINE"` - // MaxEventQueueSize is the maximum number of events to queue per topic + // MaxEventQueueSize is the maximum number of events to queue per topic. + // When this limit is reached, new events may be dropped or publishers + // may be blocked, depending on the engine implementation. + // Must be at least 1. MaxEventQueueSize int `json:"maxEventQueueSize" yaml:"maxEventQueueSize" validate:"min=1" env:"MAX_EVENT_QUEUE_SIZE"` - // DefaultEventBufferSize is the default buffer size for subscription channels + // DefaultEventBufferSize is the default buffer size for subscription channels. + // This affects how many events can be buffered for each subscription before + // blocking. Larger buffers can improve performance but use more memory. + // Must be at least 1. DefaultEventBufferSize int `json:"defaultEventBufferSize" yaml:"defaultEventBufferSize" validate:"min=1" env:"DEFAULT_EVENT_BUFFER_SIZE"` - // WorkerCount is the number of worker goroutines for async event processing + // WorkerCount is the number of worker goroutines for async event processing. + // These workers process events from asynchronous subscriptions. More workers + // can increase throughput but also increase resource usage. + // Must be at least 1. WorkerCount int `json:"workerCount" yaml:"workerCount" validate:"min=1" env:"WORKER_COUNT"` - // EventTTL is the time to live for events in seconds + // EventTTL is the time to live for events in seconds. + // Events older than this value may be automatically removed from queues + // or marked as expired. Used for event cleanup and storage management. + // Must be at least 1. EventTTL int `json:"eventTTL" yaml:"eventTTL" validate:"min=1" env:"EVENT_TTL"` - // RetentionDays is how many days to retain event history + // RetentionDays is how many days to retain event history. + // This affects event storage and cleanup policies. Longer retention + // allows for event replay and debugging but requires more storage. + // Must be at least 1. RetentionDays int `json:"retentionDays" yaml:"retentionDays" validate:"min=1" env:"RETENTION_DAYS"` - // External broker configuration - ExternalBrokerURL string `json:"externalBrokerURL" yaml:"externalBrokerURL" env:"EXTERNAL_BROKER_URL"` - ExternalBrokerUser string `json:"externalBrokerUser" yaml:"externalBrokerUser" env:"EXTERNAL_BROKER_USER"` + // ExternalBrokerURL is the connection URL for external message brokers. + // Used when the engine is set to "redis" or "kafka". The format depends + // on the specific broker type. + // Examples: + // Redis: "redis://localhost:6379" or "redis://user:pass@host:port/db" + // Kafka: "kafka://localhost:9092" or "kafka://broker1:9092,broker2:9092" + ExternalBrokerURL string `json:"externalBrokerURL" yaml:"externalBrokerURL" env:"EXTERNAL_BROKER_URL"` + + // ExternalBrokerUser is the username for external broker authentication. + // Used when the external broker requires authentication. + // Leave empty if the broker doesn't require authentication. + ExternalBrokerUser string `json:"externalBrokerUser" yaml:"externalBrokerUser" env:"EXTERNAL_BROKER_USER"` + + // ExternalBrokerPassword is the password for external broker authentication. + // Used when the external broker requires authentication. + // Leave empty if the broker doesn't require authentication. + // This should be kept secure and may be provided via environment variables. ExternalBrokerPassword string `json:"externalBrokerPassword" yaml:"externalBrokerPassword" env:"EXTERNAL_BROKER_PASSWORD"` } diff --git a/modules/eventbus/eventbus.go b/modules/eventbus/eventbus.go index d29d00a3..53439db5 100644 --- a/modules/eventbus/eventbus.go +++ b/modules/eventbus/eventbus.go @@ -5,68 +5,139 @@ import ( "time" ) -// Event represents a message in the event bus +// Event represents a message in the event bus. +// Events are the core data structure used for communication between +// publishers and subscribers. They contain the message data along with +// metadata for tracking and processing. type Event struct { - // Topic is the channel or subject of the event + // Topic is the channel or subject of the event. + // Topics are used for routing events to the appropriate subscribers. + // Topic names can use hierarchical patterns like "user.created" or "order.payment.failed". Topic string `json:"topic"` - // Payload is the data associated with the event + // Payload is the data associated with the event. + // This can be any serializable data structure that represents + // the event's information. The payload type should be consistent + // for events within the same topic. Payload interface{} `json:"payload"` - // Metadata contains additional information about the event + // Metadata contains additional information about the event. + // This can include source information, correlation IDs, version numbers, + // or any other contextual data that doesn't belong in the main payload. + // Optional field that can be nil if no metadata is needed. Metadata map[string]interface{} `json:"metadata,omitempty"` - // CreatedAt is when the event was created + // CreatedAt is when the event was created. + // This timestamp is set automatically when the event is published + // and can be used for event ordering, TTL calculations, and debugging. CreatedAt time.Time `json:"createdAt"` - // ProcessingStarted is when the event processing started + // ProcessingStarted is when the event processing started. + // This field is set when an event handler begins processing the event. + // Used for performance monitoring and timeout detection. ProcessingStarted *time.Time `json:"processingStarted,omitempty"` - // ProcessingCompleted is when the event processing completed + // ProcessingCompleted is when the event processing completed. + // This field is set when an event handler finishes processing the event, + // whether successfully or with an error. Used for performance monitoring + // and event lifecycle tracking. ProcessingCompleted *time.Time `json:"processingCompleted,omitempty"` } -// EventHandler is a function that handles an event +// EventHandler is a function that handles an event. +// Event handlers are called when an event matching their subscription +// topic is published. Handlers should be idempotent when possible and +// handle errors gracefully. +// +// The context can be used for cancellation, timeouts, and passing +// request-scoped values. Handlers should respect context cancellation +// and return promptly when the context is cancelled. +// +// Example handler: +// +// func userCreatedHandler(ctx context.Context, event Event) error { +// user := event.Payload.(UserData) +// return sendWelcomeEmail(ctx, user.Email) +// } type EventHandler func(ctx context.Context, event Event) error -// Subscription represents a subscription to a topic +// Subscription represents a subscription to a topic. +// Subscriptions are created when a handler is registered for a topic +// and provide methods for managing the subscription lifecycle. type Subscription interface { - // Topic returns the topic being subscribed to + // Topic returns the topic being subscribed to. + // This may include wildcard patterns depending on the engine implementation. Topic() string - // ID returns the unique identifier for this subscription + // ID returns the unique identifier for this subscription. + // Each subscription gets a unique ID that can be used for tracking, + // logging, and debugging purposes. ID() string - // IsAsync returns true if this is an asynchronous subscription + // IsAsync returns true if this is an asynchronous subscription. + // Asynchronous subscriptions process events in background workers, + // while synchronous subscriptions process events immediately. IsAsync() bool - // Cancel cancels the subscription + // Cancel cancels the subscription. + // After calling Cancel, the subscription will no longer receive events. + // This is equivalent to calling Unsubscribe on the event bus. + // The method is idempotent and safe to call multiple times. Cancel() error } -// EventBus defines the interface for an event bus implementation +// EventBus defines the interface for an event bus implementation. +// This interface abstracts the underlying messaging mechanism, allowing +// the eventbus module to support multiple backends (memory, Redis, Kafka) +// through a common API. +// +// All operations are context-aware to support cancellation and timeouts. +// Implementations should be thread-safe and handle concurrent access properly. type EventBus interface { - // Start initializes the event bus + // Start initializes the event bus. + // This method is called during module startup and should prepare + // the event bus for publishing and subscribing operations. + // For memory buses, this might initialize internal data structures. + // For network-based buses, this establishes connections. Start(ctx context.Context) error - // Stop shuts down the event bus + // Stop shuts down the event bus. + // This method is called during module shutdown and should cleanup + // all resources, close connections, and stop background processes. + // It should ensure all in-flight events are processed before returning. Stop(ctx context.Context) error - // Publish sends an event to the specified topic + // Publish sends an event to the specified topic. + // The event will be delivered to all active subscribers of the topic. + // The method should handle event queuing, topic routing, and delivery + // according to the engine's semantics. Publish(ctx context.Context, event Event) error - // Subscribe registers a handler for a topic with synchronous processing + // Subscribe registers a handler for a topic with synchronous processing. + // Events matching the topic will be delivered immediately to the handler + // in the same goroutine that published them. The publisher will wait + // for the handler to complete before continuing. Subscribe(ctx context.Context, topic string, handler EventHandler) (Subscription, error) - // SubscribeAsync registers a handler for a topic with asynchronous processing + // SubscribeAsync registers a handler for a topic with asynchronous processing. + // Events matching the topic will be queued for processing by worker goroutines. + // The publisher can continue immediately without waiting for processing. + // This is preferred for heavy operations or non-critical event handling. SubscribeAsync(ctx context.Context, topic string, handler EventHandler) (Subscription, error) - // Unsubscribe removes a subscription + // Unsubscribe removes a subscription. + // After unsubscribing, the subscription will no longer receive events. + // This method should be idempotent and not return errors for + // subscriptions that are already cancelled. Unsubscribe(ctx context.Context, subscription Subscription) error - // Topics returns a list of all active topics + // Topics returns a list of all active topics. + // This includes only topics that currently have at least one subscriber. + // Useful for monitoring, debugging, and administrative interfaces. Topics() []string - // SubscriberCount returns the number of subscribers for a topic + // SubscriberCount returns the number of subscribers for a topic. + // This includes both synchronous and asynchronous subscriptions. + // Returns 0 if the topic has no subscribers or doesn't exist. SubscriberCount(topic string) int } diff --git a/modules/eventbus/module.go b/modules/eventbus/module.go index 3ec4c590..b68851af 100644 --- a/modules/eventbus/module.go +++ b/modules/eventbus/module.go @@ -1,3 +1,113 @@ +// Package eventbus provides a flexible event-driven messaging system for the modular framework. +// +// This module enables decoupled communication between application components through +// an event bus pattern. It supports both synchronous and asynchronous event processing, +// multiple event bus engines, and configurable event handling strategies. +// +// # Features +// +// The eventbus module offers the following capabilities: +// - Topic-based event publishing and subscription +// - Synchronous and asynchronous event processing +// - Multiple engine support (memory, Redis, Kafka) +// - Configurable worker pools for async processing +// - Event metadata and lifecycle tracking +// - Subscription management with unique identifiers +// - Event TTL and retention policies +// +// # Configuration +// +// The module can be configured through the EventBusConfig structure: +// +// config := &EventBusConfig{ +// Engine: "memory", // or "redis", "kafka" +// MaxEventQueueSize: 1000, // events per topic queue +// DefaultEventBufferSize: 10, // subscription channel buffer +// WorkerCount: 5, // async processing workers +// EventTTL: 3600, // event time-to-live in seconds +// RetentionDays: 7, // event history retention +// ExternalBrokerURL: "", // for external brokers +// ExternalBrokerUser: "", // broker authentication +// ExternalBrokerPassword: "", // broker password +// } +// +// # Service Registration +// +// The module registers itself as a service for dependency injection: +// +// // Get the event bus service +// eventBus := app.GetService("eventbus.provider").(*EventBusModule) +// +// // Publish an event +// err := eventBus.Publish(ctx, "user.created", userData) +// +// // Subscribe to events +// subscription, err := eventBus.Subscribe(ctx, "user.*", userEventHandler) +// +// # Usage Examples +// +// Basic event publishing: +// +// // Publish a simple event +// err := eventBus.Publish(ctx, "order.placed", orderData) +// +// // Publish with custom metadata +// event := Event{ +// Topic: "payment.processed", +// Payload: paymentData, +// Metadata: map[string]interface{}{ +// "source": "payment-service", +// "version": "1.2.0", +// }, +// } +// err := eventBus.Publish(ctx, event.Topic, event.Payload) +// +// Event subscription patterns: +// +// // Synchronous subscription +// subscription, err := eventBus.Subscribe(ctx, "user.updated", func(ctx context.Context, event Event) error { +// user := event.Payload.(UserData) +// return updateUserCache(user) +// }) +// +// // Asynchronous subscription for heavy processing +// asyncSub, err := eventBus.SubscribeAsync(ctx, "image.uploaded", func(ctx context.Context, event Event) error { +// imageData := event.Payload.(ImageData) +// return processImageThumbnails(imageData) +// }) +// +// // Wildcard subscriptions +// allOrdersSub, err := eventBus.Subscribe(ctx, "order.*", orderEventHandler) +// +// Subscription management: +// +// // Check subscription details +// fmt.Printf("Subscribed to: %s (ID: %s, Async: %v)", +// subscription.Topic(), subscription.ID(), subscription.IsAsync()) +// +// // Cancel specific subscriptions +// err := eventBus.Unsubscribe(ctx, subscription) +// +// // Or cancel through the subscription itself +// err := subscription.Cancel() +// +// # Event Processing Patterns +// +// The module supports different event processing patterns: +// +// **Synchronous Processing**: Events are processed immediately in the same goroutine +// that published them. Best for lightweight operations and when ordering is important. +// +// **Asynchronous Processing**: Events are queued and processed by worker goroutines. +// Best for heavy operations, external API calls, or when you don't want to block +// the publisher. +// +// # Engine Support +// +// Currently supported engines: +// - **memory**: In-process event bus using Go channels +// - **redis**: Distributed event bus using Redis pub/sub (planned) +// - **kafka**: Enterprise event bus using Apache Kafka (planned) package eventbus import ( @@ -8,13 +118,26 @@ import ( "github.com/GoCodeAlone/modular" ) -// ModuleName is the name of this module +// ModuleName is the unique identifier for the eventbus module. const ModuleName = "eventbus" -// ServiceName is the name of the service provided by this module +// ServiceName is the name of the service provided by this module. +// Other modules can use this name to request the event bus service through dependency injection. const ServiceName = "eventbus.provider" -// EventBusModule represents the event bus module +// EventBusModule provides event-driven messaging capabilities for the modular framework. +// It implements a publish-subscribe pattern with support for multiple event bus engines, +// asynchronous processing, and flexible subscription management. +// +// The module implements the following interfaces: +// - modular.Module: Basic module lifecycle +// - modular.Configurable: Configuration management +// - modular.ServiceAware: Service dependency management +// - modular.Startable: Startup logic +// - modular.Stoppable: Shutdown logic +// - EventBus: Event publishing and subscription interface +// +// Event processing is thread-safe and supports concurrent publishers and subscribers. type EventBusModule struct { name string config *EventBusConfig @@ -24,19 +147,38 @@ type EventBusModule struct { isStarted bool } -// NewModule creates a new instance of the event bus module +// NewModule creates a new instance of the event bus module. +// This is the primary constructor for the eventbus module and should be used +// when registering the module with the application. +// +// Example: +// +// app.RegisterModule(eventbus.NewModule()) func NewModule() modular.Module { return &EventBusModule{ name: ModuleName, } } -// Name returns the name of the module +// Name returns the unique identifier for this module. +// This name is used for service registration, dependency resolution, +// and configuration section identification. func (m *EventBusModule) Name() string { return m.name } -// RegisterConfig registers the module's configuration structure +// RegisterConfig registers the module's configuration structure. +// This method is called during application initialization to register +// the default configuration values for the eventbus module. +// +// Default configuration: +// - Engine: "memory" +// - MaxEventQueueSize: 1000 events per topic +// - DefaultEventBufferSize: 10 events per subscription channel +// - WorkerCount: 5 async processing workers +// - EventTTL: 3600 seconds (1 hour) +// - RetentionDays: 7 days for event history +// - ExternalBroker settings: empty (not used for memory engine) func (m *EventBusModule) RegisterConfig(app modular.Application) error { // Register the configuration with default values defaultConfig := &EventBusConfig{ @@ -55,7 +197,19 @@ func (m *EventBusModule) RegisterConfig(app modular.Application) error { return nil } -// Init initializes the module +// Init initializes the eventbus module with the application context. +// This method is called after all modules have been registered and their +// configurations loaded. It sets up the event bus engine based on configuration. +// +// The initialization process: +// 1. Retrieves the module's configuration +// 2. Sets up logging +// 3. Initializes the appropriate event bus engine +// 4. Prepares the event bus for startup +// +// Supported engines: +// - "memory": In-process event bus using Go channels +// - fallback: defaults to memory engine for unknown engines func (m *EventBusModule) Init(app modular.Application) error { // Retrieve the registered config section for access cfg, err := app.GetConfigSection(m.name) @@ -80,7 +234,17 @@ func (m *EventBusModule) Init(app modular.Application) error { return nil } -// Start performs startup logic for the module +// Start performs startup logic for the module. +// This method starts the event bus engine and begins processing events. +// It's called after all modules have been initialized and are ready to start. +// +// The startup process: +// 1. Checks if already started (idempotent) +// 2. Starts the underlying event bus engine +// 3. Initializes worker pools for async processing +// 4. Prepares topic management and subscription tracking +// +// This method is thread-safe and can be called multiple times safely. func (m *EventBusModule) Start(ctx context.Context) error { m.logger.Info("Starting event bus module") @@ -102,7 +266,19 @@ func (m *EventBusModule) Start(ctx context.Context) error { return nil } -// Stop performs shutdown logic for the module +// Stop performs shutdown logic for the module. +// This method gracefully shuts down the event bus, ensuring all in-flight +// events are processed and all subscriptions are properly cleaned up. +// +// The shutdown process: +// 1. Checks if already stopped (idempotent) +// 2. Stops accepting new events +// 3. Waits for in-flight events to complete +// 4. Cancels all active subscriptions +// 5. Shuts down worker pools +// 6. Closes the underlying event bus engine +// +// This method is thread-safe and can be called multiple times safely. func (m *EventBusModule) Stop(ctx context.Context) error { m.logger.Info("Stopping event bus module") @@ -124,12 +300,18 @@ func (m *EventBusModule) Stop(ctx context.Context) error { return nil } -// Dependencies returns the names of modules this module depends on +// Dependencies returns the names of modules this module depends on. +// The eventbus module operates independently and has no dependencies. func (m *EventBusModule) Dependencies() []string { return nil } -// ProvidesServices declares services provided by this module +// ProvidesServices declares services provided by this module. +// The eventbus module provides an event bus service that can be injected +// into other modules for event-driven communication. +// +// Provided services: +// - "eventbus.provider": The main event bus service interface func (m *EventBusModule) ProvidesServices() []modular.ServiceProvider { return []modular.ServiceProvider{ { @@ -140,19 +322,32 @@ func (m *EventBusModule) ProvidesServices() []modular.ServiceProvider { } } -// RequiresServices declares services required by this module +// RequiresServices declares services required by this module. +// The eventbus module operates independently and requires no external services. func (m *EventBusModule) RequiresServices() []modular.ServiceDependency { return nil } -// Constructor provides a dependency injection constructor for the module +// Constructor provides a dependency injection constructor for the module. +// This method is used by the dependency injection system to create +// the module instance with any required services. func (m *EventBusModule) Constructor() modular.ModuleConstructor { return func(app modular.Application, services map[string]any) (modular.Module, error) { return m, nil } } -// Publish publishes an event to the event bus +// Publish publishes an event to the event bus. +// Creates an Event struct with the provided topic and payload, then +// sends it through the event bus for processing by subscribers. +// +// The event will be delivered to all active subscribers of the topic. +// Topic patterns and wildcards may be supported depending on the engine. +// +// Example: +// +// err := eventBus.Publish(ctx, "user.created", userData) +// err := eventBus.Publish(ctx, "order.payment.failed", paymentData) func (m *EventBusModule) Publish(ctx context.Context, topic string, payload interface{}) error { event := Event{ Topic: topic, @@ -161,27 +356,84 @@ func (m *EventBusModule) Publish(ctx context.Context, topic string, payload inte return m.eventbus.Publish(ctx, event) } -// Subscribe subscribes to a topic on the event bus +// Subscribe subscribes to a topic on the event bus with synchronous processing. +// The provided handler will be called immediately when an event is published +// to the specified topic. The handler blocks the event delivery until it completes. +// +// Use synchronous subscriptions for: +// - Lightweight event processing +// - When event ordering is important +// - Critical event handlers that must complete before continuing +// +// Example: +// +// subscription, err := eventBus.Subscribe(ctx, "user.login", func(ctx context.Context, event Event) error { +// user := event.Payload.(UserData) +// return updateLastLoginTime(user.ID) +// }) func (m *EventBusModule) Subscribe(ctx context.Context, topic string, handler EventHandler) (Subscription, error) { return m.eventbus.Subscribe(ctx, topic, handler) } -// SubscribeAsync subscribes to a topic with asynchronous event handling +// SubscribeAsync subscribes to a topic with asynchronous event processing. +// The provided handler will be queued for processing by worker goroutines, +// allowing the event publisher to continue without waiting for processing. +// +// Use asynchronous subscriptions for: +// - Heavy processing operations +// - External API calls +// - Non-critical event handlers +// - When you want to avoid blocking publishers +// +// Example: +// +// subscription, err := eventBus.SubscribeAsync(ctx, "image.uploaded", func(ctx context.Context, event Event) error { +// imageData := event.Payload.(ImageData) +// return generateThumbnails(imageData) +// }) func (m *EventBusModule) SubscribeAsync(ctx context.Context, topic string, handler EventHandler) (Subscription, error) { return m.eventbus.SubscribeAsync(ctx, topic, handler) } -// Unsubscribe cancels a subscription +// Unsubscribe cancels a subscription and stops receiving events. +// The subscription will be removed from the event bus and no longer +// receive events for its topic. +// +// This method is idempotent - calling it multiple times on the same +// subscription is safe and will not cause errors. +// +// Example: +// +// err := eventBus.Unsubscribe(ctx, subscription) func (m *EventBusModule) Unsubscribe(ctx context.Context, subscription Subscription) error { return m.eventbus.Unsubscribe(ctx, subscription) } -// Topics returns a list of all active topics +// Topics returns a list of all active topics that have subscribers. +// This can be useful for debugging, monitoring, or building administrative +// interfaces that show current event bus activity. +// +// Example: +// +// activeTopics := eventBus.Topics() +// for _, topic := range activeTopics { +// count := eventBus.SubscriberCount(topic) +// fmt.Printf("Topic: %s, Subscribers: %d\n", topic, count) +// } func (m *EventBusModule) Topics() []string { return m.eventbus.Topics() } -// SubscriberCount returns the number of subscribers for a topic +// SubscriberCount returns the number of active subscribers for a topic. +// This includes both synchronous and asynchronous subscriptions. +// Returns 0 if the topic has no subscribers. +// +// Example: +// +// count := eventBus.SubscriberCount("user.created") +// if count == 0 { +// log.Warn("No subscribers for user creation events") +// } func (m *EventBusModule) SubscriberCount(topic string) int { return m.eventbus.SubscriberCount(topic) } diff --git a/modules/httpclient/config.go b/modules/httpclient/config.go index a309e60a..3db169d5 100644 --- a/modules/httpclient/config.go +++ b/modules/httpclient/config.go @@ -7,51 +7,120 @@ import ( ) // Config defines the configuration for the HTTP client module. +// This structure contains all the settings needed to configure HTTP client +// behavior, connection pooling, timeouts, and logging. +// +// Configuration can be provided through JSON, YAML, or environment variables. +// The struct tags define the mapping for each configuration source. +// +// Example YAML configuration: +// +// max_idle_conns: 200 +// max_idle_conns_per_host: 20 +// idle_conn_timeout: 120 +// request_timeout: 60 +// tls_timeout: 15 +// disable_compression: false +// disable_keep_alives: false +// verbose: true +// verbose_options: +// log_headers: true +// log_body: true +// max_body_log_size: 1024 +// log_to_file: true +// log_file_path: "/var/log/httpclient" +// +// Example environment variables: +// +// HTTPCLIENT_MAX_IDLE_CONNS=200 +// HTTPCLIENT_REQUEST_TIMEOUT=60 +// HTTPCLIENT_VERBOSE=true type Config struct { // MaxIdleConns controls the maximum number of idle (keep-alive) connections across all hosts. + // This setting affects the total connection pool size and memory usage. + // Higher values allow more concurrent connections but use more memory. + // Default: 100 MaxIdleConns int `yaml:"max_idle_conns" json:"max_idle_conns" env:"MAX_IDLE_CONNS"` // MaxIdleConnsPerHost controls the maximum idle (keep-alive) connections to keep per-host. + // This prevents a single host from monopolizing the connection pool. + // Should be tuned based on expected traffic patterns to specific hosts. + // Default: 10 MaxIdleConnsPerHost int `yaml:"max_idle_conns_per_host" json:"max_idle_conns_per_host" env:"MAX_IDLE_CONNS_PER_HOST"` - // IdleConnTimeout is the maximum amount of time an idle connection will remain idle before - // closing itself, in seconds. + // IdleConnTimeout is the maximum amount of time an idle connection will remain idle + // before closing itself, in seconds. This helps prevent stale connections and + // reduces server-side resource usage. + // Default: 90 seconds IdleConnTimeout int `yaml:"idle_conn_timeout" json:"idle_conn_timeout" env:"IDLE_CONN_TIMEOUT"` // RequestTimeout is the maximum time for a request to complete, in seconds. + // This includes connection time, any redirects, and reading the response body. + // Use WithTimeout() method for per-request timeout overrides. + // Default: 30 seconds RequestTimeout int `yaml:"request_timeout" json:"request_timeout" env:"REQUEST_TIMEOUT"` // TLSTimeout is the maximum time waiting for TLS handshake, in seconds. + // This only affects HTTPS connections and should be set based on expected + // network latency and certificate chain complexity. + // Default: 10 seconds TLSTimeout int `yaml:"tls_timeout" json:"tls_timeout" env:"TLS_TIMEOUT"` // DisableCompression disables decompressing response bodies. + // When false (default), the client automatically handles gzip compression. + // Set to true if you need to handle compression manually or want raw responses. + // Default: false (compression enabled) DisableCompression bool `yaml:"disable_compression" json:"disable_compression" env:"DISABLE_COMPRESSION"` // DisableKeepAlives disables HTTP keep-alive and will only use connections for a single request. + // This can be useful for debugging or when connecting to servers that don't handle + // keep-alives properly, but significantly impacts performance. + // Default: false (keep-alives enabled) DisableKeepAlives bool `yaml:"disable_keep_alives" json:"disable_keep_alives" env:"DISABLE_KEEP_ALIVES"` // Verbose enables detailed logging of HTTP requests and responses. + // When enabled, logs include request/response headers, bodies, timing information, + // and error details. Very useful for debugging but can impact performance. + // Default: false Verbose bool `yaml:"verbose" json:"verbose" env:"VERBOSE"` // VerboseOptions configures the behavior when Verbose is enabled. + // This allows fine-grained control over what gets logged and where. VerboseOptions *VerboseOptions `yaml:"verbose_options" json:"verbose_options" env:"VERBOSE_OPTIONS"` } // VerboseOptions configures the behavior of verbose logging. +// These options provide fine-grained control over HTTP request/response logging +// to balance debugging needs with performance and security considerations. type VerboseOptions struct { // LogHeaders enables logging of request and response headers. + // This includes all HTTP headers sent and received, which can contain + // sensitive information like authorization tokens. + // Default: false LogHeaders bool `yaml:"log_headers" json:"log_headers" env:"LOG_HEADERS"` // LogBody enables logging of request and response bodies. + // This can generate large amounts of log data and may contain sensitive + // information. Consider using MaxBodyLogSize to limit logged content. + // Default: false LogBody bool `yaml:"log_body" json:"log_body" env:"LOG_BODY"` // MaxBodyLogSize limits the size of logged request and response bodies. + // Bodies larger than this size will be truncated in logs. Set to 0 for no limit. + // Helps prevent log spam from large file uploads or downloads. + // Default: 0 (no limit) MaxBodyLogSize int `yaml:"max_body_log_size" json:"max_body_log_size" env:"MAX_BODY_LOG_SIZE"` - // LogToFile enables logging to files instead of just the logger. + // LogToFile enables logging to files instead of just the application logger. + // When enabled, HTTP logs are written to separate files for easier analysis. + // Requires LogFilePath to be set. + // Default: false LogToFile bool `yaml:"log_to_file" json:"log_to_file" env:"LOG_TO_FILE"` // LogFilePath is the directory where log files will be written. + // Log files are organized by date and include request/response details. + // The directory must be writable by the application. + // Default: "" (current directory) LogFilePath string `yaml:"log_file_path" json:"log_file_path" env:"LOG_FILE_PATH"` } diff --git a/modules/httpclient/module.go b/modules/httpclient/module.go index b5ac0171..7bc58f9c 100644 --- a/modules/httpclient/module.go +++ b/modules/httpclient/module.go @@ -1,4 +1,118 @@ // Package httpclient provides a configurable HTTP client module for the modular framework. +// +// This module offers a production-ready HTTP client with comprehensive configuration +// options, request/response logging, connection pooling, timeout management, and +// request modification capabilities. It's designed for reliable HTTP communication +// in microservices and web applications. +// +// # Features +// +// The httpclient module provides the following capabilities: +// - Configurable connection pooling and keep-alive settings +// - Request and response timeout management +// - TLS handshake timeout configuration +// - Comprehensive request/response logging with file output +// - Request modification pipeline for adding headers, authentication, etc. +// - Performance-optimized transport settings +// - Support for compression and keep-alive control +// - Service interface for dependency injection +// +// # Configuration +// +// The module can be configured through the Config structure: +// +// config := &Config{ +// MaxIdleConns: 100, // total idle connections +// MaxIdleConnsPerHost: 10, // idle connections per host +// IdleConnTimeout: 90, // idle connection timeout (seconds) +// RequestTimeout: 30, // request timeout (seconds) +// TLSTimeout: 10, // TLS handshake timeout (seconds) +// DisableCompression: false, // enable gzip compression +// DisableKeepAlives: false, // enable connection reuse +// Verbose: true, // enable request/response logging +// VerboseOptions: &VerboseOptions{ +// LogToFile: true, +// LogFilePath: "/var/log/httpclient", +// }, +// } +// +// # Service Registration +// +// The module registers itself as a service for dependency injection: +// +// // Get the HTTP client service +// client := app.GetService("httpclient").(httpclient.ClientService) +// +// // Use the client +// resp, err := client.Client().Get("https://api.example.com/users") +// +// // Create a client with custom timeout +// timeoutClient := client.WithTimeout(60) +// resp, err := timeoutClient.Post("https://api.example.com/upload", "application/json", data) +// +// # Usage Examples +// +// Basic HTTP requests: +// +// // GET request +// resp, err := client.Client().Get("https://api.example.com/health") +// if err != nil { +// return err +// } +// defer resp.Body.Close() +// +// // POST request with JSON +// jsonData := bytes.NewBuffer([]byte(`{"name": "test"}`)) +// resp, err := client.Client().Post( +// "https://api.example.com/users", +// "application/json", +// jsonData, +// ) +// +// Request modification for authentication: +// +// // Set up request modifier for API key authentication +// modifier := func(req *http.Request) *http.Request { +// req.Header.Set("Authorization", "Bearer "+apiToken) +// req.Header.Set("User-Agent", "MyApp/1.0") +// return req +// } +// client.SetRequestModifier(modifier) +// +// // All subsequent requests will include the headers +// resp, err := client.Client().Get("https://api.example.com/protected") +// +// Custom timeout scenarios: +// +// // Short timeout for health checks +// healthClient := client.WithTimeout(5) +// resp, err := healthClient.Get("https://service.example.com/health") +// +// // Long timeout for file uploads +// uploadClient := client.WithTimeout(300) +// resp, err := uploadClient.Post("https://api.example.com/upload", contentType, fileData) +// +// # Logging and Debugging +// +// When verbose logging is enabled, the module logs detailed request and response +// information including headers, bodies, and timing data. This is invaluable for +// debugging API integrations and monitoring HTTP performance. +// +// Log output includes: +// - Request method, URL, and headers +// - Request body (configurable) +// - Response status, headers, and body +// - Request duration and timing breakdown +// - Error details and retry information +// +// # Performance Considerations +// +// The module is optimized for production use with: +// - Connection pooling to reduce connection overhead +// - Keep-alive connections for better performance +// - Configurable timeouts to prevent resource leaks +// - Optional compression to reduce bandwidth usage +// - Efficient request modification pipeline package httpclient import ( @@ -13,13 +127,24 @@ import ( "github.com/GoCodeAlone/modular" ) -// ModuleName is the name of this module +// ModuleName is the unique identifier for the httpclient module. const ModuleName = "httpclient" -// ServiceName is the name of the service provided by this module +// ServiceName is the name of the service provided by this module. +// Other modules can use this name to request the HTTP client service through dependency injection. const ServiceName = "httpclient" // HTTPClientModule implements a configurable HTTP client module. +// It provides a production-ready HTTP client with comprehensive configuration +// options, logging capabilities, and request modification features. +// +// The module implements the following interfaces: +// - modular.Module: Basic module lifecycle +// - modular.Configurable: Configuration management +// - modular.ServiceAware: Service dependency management +// - ClientService: HTTP client service interface +// +// The HTTP client is thread-safe and can be used concurrently from multiple goroutines. type HTTPClientModule struct { config *Config app modular.Application @@ -37,18 +162,38 @@ var ( ) // NewHTTPClientModule creates a new instance of the HTTP client module. +// This is the primary constructor for the httpclient module and should be used +// when registering the module with the application. +// +// Example: +// +// app.RegisterModule(httpclient.NewHTTPClientModule()) func NewHTTPClientModule() modular.Module { return &HTTPClientModule{ modifier: func(r *http.Request) *http.Request { return r }, // Default no-op modifier } } -// Name returns the name of the module. +// Name returns the unique identifier for this module. +// This name is used for service registration, dependency resolution, +// and configuration section identification. func (m *HTTPClientModule) Name() string { return ModuleName } -// RegisterConfig registers the module's configuration. +// RegisterConfig registers the module's configuration structure. +// This method is called during application initialization to register +// the default configuration values for the httpclient module. +// +// Default configuration: +// - MaxIdleConns: 100 (total idle connections) +// - MaxIdleConnsPerHost: 10 (idle connections per host) +// - IdleConnTimeout: 90 seconds +// - RequestTimeout: 30 seconds +// - TLSTimeout: 10 seconds +// - DisableCompression: false (compression enabled) +// - DisableKeepAlives: false (keep-alives enabled) +// - Verbose: false (logging disabled) func (m *HTTPClientModule) RegisterConfig(app modular.Application) error { // Register the configuration with default values defaultConfig := &Config{ @@ -66,7 +211,23 @@ func (m *HTTPClientModule) RegisterConfig(app modular.Application) error { return nil } -// Init initializes the module with the provided application. +// Init initializes the httpclient module with the application context. +// This method is called after all modules have been registered and their +// configurations loaded. It sets up the HTTP client, transport, and logging. +// +// The initialization process: +// 1. Retrieves the module's configuration +// 2. Sets up logging +// 3. Creates and configures the HTTP transport with connection pooling +// 4. Sets up request/response logging if verbose mode is enabled +// 5. Creates the HTTP client with configured transport and middleware +// 6. Initializes request modification pipeline +// +// Transport configuration includes: +// - Connection pooling settings for optimal performance +// - Timeout configurations for reliability +// - Compression and keep-alive settings +// - TLS handshake timeout for secure connections func (m *HTTPClientModule) Init(app modular.Application) error { m.app = app m.logger = app.Logger() diff --git a/modules/httpclient/service.go b/modules/httpclient/service.go index 151d355e..201fa4f5 100644 --- a/modules/httpclient/service.go +++ b/modules/httpclient/service.go @@ -5,18 +5,95 @@ import ( ) // ClientService defines the interface for the HTTP client service. -// Any module that needs to make HTTP requests can use this service. +// This interface provides access to configured HTTP clients and request +// modification capabilities. Any module that needs to make HTTP requests +// can use this service through dependency injection. +// +// The service provides multiple ways to access HTTP clients: +// - Default client with module configuration +// - Timeout-specific clients for different use cases +// - Request modification pipeline for common headers/auth +// +// Example usage: +// +// // Basic usage +// client := httpClientService.Client() +// resp, err := client.Get("https://api.example.com/data") +// +// // Custom timeout +// shortTimeoutClient := httpClientService.WithTimeout(5) +// resp, err := shortTimeoutClient.Get("https://api.example.com/health") +// +// // Request modification +// modifier := httpClientService.RequestModifier() +// req, _ := http.NewRequest("GET", "https://api.example.com/data", nil) +// modifiedReq := modifier(req) type ClientService interface { - // Client returns the configured http.Client instance + // Client returns the configured http.Client instance. + // This client uses the module's configuration for timeouts, connection + // pooling, compression, and other transport settings. The client is + // thread-safe and can be used concurrently. + // + // The returned client includes any configured request modification + // pipeline and verbose logging if enabled. Client() *http.Client - // RequestModifier returns a modifier function that can modify a request before it's sent + // RequestModifier returns a modifier function that can modify a request before it's sent. + // This function applies any configured request modifications such as + // authentication headers, user agents, or custom headers. + // + // The modifier can be used manually when creating custom requests: + // req, _ := http.NewRequest("POST", url, body) + // req = modifier(req) + // resp, err := client.Do(req) RequestModifier() RequestModifierFunc - // WithTimeout creates a new client with the specified timeout in seconds + // WithTimeout creates a new client with the specified timeout in seconds. + // This is useful for creating clients with different timeout requirements + // without affecting the default client configuration. + // + // The new client inherits all other configuration from the module + // (connection pooling, compression, etc.) but uses the specified timeout. + // + // Common timeout scenarios: + // - Health checks: 5-10 seconds + // - API calls: 30-60 seconds + // - File uploads: 300+ seconds WithTimeout(timeoutSeconds int) *http.Client } // RequestModifierFunc is a function type that can be used to modify an HTTP request // before it is sent by the client. +// +// Request modifiers are useful for: +// - Adding authentication headers (Bearer tokens, API keys) +// - Setting common headers (User-Agent, Content-Type) +// - Adding request tracking (correlation IDs, request IDs) +// - Request logging and debugging +// - Request validation and sanitization +// +// Example modifier implementations: +// +// // API key authentication +// func apiKeyModifier(apiKey string) RequestModifierFunc { +// return func(req *http.Request) *http.Request { +// req.Header.Set("Authorization", "Bearer "+apiKey) +// return req +// } +// } +// +// // Request tracing +// func tracingModifier(req *http.Request) *http.Request { +// req.Header.Set("X-Request-ID", generateRequestID()) +// req.Header.Set("X-Trace-ID", getTraceID(req.Context())) +// return req +// } +// +// // User agent setting +// func userAgentModifier(userAgent string) RequestModifierFunc { +// return func(req *http.Request) *http.Request { +// req.Header.Set("User-Agent", userAgent) +// return req +// } +// } type RequestModifierFunc func(*http.Request) *http.Request diff --git a/modules/httpserver/module.go b/modules/httpserver/module.go index cd731efc..20cda441 100644 --- a/modules/httpserver/module.go +++ b/modules/httpserver/module.go @@ -1,4 +1,27 @@ // Package httpserver provides an HTTP server module for the modular framework. +// This module offers a complete HTTP server implementation with support for +// TLS, automatic certificate management, graceful shutdown, and middleware integration. +// +// The httpserver module features: +// - HTTP and HTTPS server support +// - Automatic TLS certificate generation and management +// - Configurable timeouts and limits +// - Graceful shutdown handling +// - Handler registration and middleware support +// - Health check endpoints +// - Integration with Let's Encrypt for automatic certificates +// +// Usage: +// +// app.RegisterModule(httpserver.NewModule()) +// +// The module registers an HTTP server service that can be used by other modules +// to register handlers, middleware, or access the underlying server instance. +// +// Configuration: +// +// The module requires an "httpserver" configuration section with server +// settings including address, ports, TLS configuration, and timeout values. package httpserver import ( @@ -22,19 +45,28 @@ import ( "github.com/GoCodeAlone/modular" ) -// ModuleName is the name of this module +// ModuleName is the name of this module for registration and dependency resolution. const ModuleName = "httpserver" -// Error definitions +// Error definitions for HTTP server operations. var ( - // ErrServerNotStarted is returned when attempting to stop a server that hasn't been started + // ErrServerNotStarted is returned when attempting to stop a server that hasn't been started. ErrServerNotStarted = errors.New("server not started") - // ErrNoHandler is returned when no HTTP handler is available + // ErrNoHandler is returned when no HTTP handler is available for the server. ErrNoHandler = errors.New("no HTTP handler available") ) -// HTTPServerModule represents the HTTP server module +// HTTPServerModule represents the HTTP server module and implements the modular.Module interface. +// It provides a complete HTTP server implementation with TLS support, graceful shutdown, +// and integration with the modular framework's configuration and service systems. +// +// The module manages: +// - HTTP server lifecycle (start, stop, graceful shutdown) +// - TLS certificate management and automatic generation +// - Request routing and handler registration +// - Server configuration and health monitoring +// - Integration with certificate services for automatic HTTPS type HTTPServerModule struct { config *HTTPServerConfig server *http.Server @@ -48,17 +80,32 @@ type HTTPServerModule struct { // Make sure the HTTPServerModule implements the Module interface var _ modular.Module = (*HTTPServerModule)(nil) -// NewHTTPServerModule creates a new instance of the HTTP server module +// NewHTTPServerModule creates a new instance of the HTTP server module. +// The returned module must be registered with the application before use. +// +// Example: +// +// httpModule := httpserver.NewHTTPServerModule() +// app.RegisterModule(httpModule) func NewHTTPServerModule() modular.Module { return &HTTPServerModule{} } -// Name returns the name of the module +// Name returns the name of the module. +// This name is used for dependency resolution and configuration section lookup. func (m *HTTPServerModule) Name() string { return ModuleName } -// RegisterConfig registers the module's configuration structure +// RegisterConfig registers the module's configuration structure. +// The HTTP server module supports comprehensive configuration including: +// - Server address and port settings +// - Timeout configurations (read, write, idle, shutdown) +// - TLS settings and certificate paths +// - Security headers and CORS configuration +// +// Default values are provided for common use cases, but can be +// overridden through configuration files or environment variables. func (m *HTTPServerModule) RegisterConfig(app modular.Application) error { // Register the configuration with default values defaultConfig := &HTTPServerConfig{ @@ -74,7 +121,16 @@ func (m *HTTPServerModule) RegisterConfig(app modular.Application) error { return nil } -// Init initializes the module with the provided application +// Init initializes the module with the provided application. +// This method loads the configuration, sets up the logger, and prepares +// the HTTP server for startup. It also attempts to resolve optional +// services like certificate management. +// +// Initialization process: +// 1. Load HTTP server configuration +// 2. Set up logging +// 3. Resolve optional certificate service for TLS +// 4. Prepare server instance (actual startup happens in Start) func (m *HTTPServerModule) Init(app modular.Application) error { m.app = app m.logger = app.Logger() @@ -113,7 +169,19 @@ func (m *HTTPServerModule) Constructor() modular.ModuleConstructor { } } -// Start starts the HTTP server +// Start starts the HTTP server and begins accepting connections. +// This method configures the server with the loaded configuration, +// sets up TLS if enabled, and starts listening for HTTP requests. +// +// The server startup process: +// 1. Validate that a handler has been registered +// 2. Create http.Server instance with configured timeouts +// 3. Set up TLS certificates if HTTPS is enabled +// 4. Start the server in a goroutine +// 5. Handle graceful shutdown on context cancellation +// +// The server will continue running until the context is cancelled +// or Stop() is called explicitly. func (m *HTTPServerModule) Start(ctx context.Context) error { if m.handler == nil { return ErrNoHandler @@ -248,7 +316,18 @@ func (m *HTTPServerModule) Start(ctx context.Context) error { return nil } -// Stop stops the HTTP server +// Stop stops the HTTP server gracefully. +// This method initiates a graceful shutdown of the HTTP server, +// allowing existing connections to finish processing before closing. +// +// The shutdown process: +// 1. Check if server is running +// 2. Create shutdown context with configured timeout +// 3. Call server.Shutdown() to stop accepting new connections +// 4. Wait for existing connections to complete or timeout +// 5. Mark server as stopped +// +// If the shutdown timeout is exceeded, the server will be forcefully closed. func (m *HTTPServerModule) Stop(ctx context.Context) error { if m.server == nil || !m.started { return ErrServerNotStarted diff --git a/modules/jsonschema/module.go b/modules/jsonschema/module.go index 0d2f35a8..4e14c241 100644 --- a/modules/jsonschema/module.go +++ b/modules/jsonschema/module.go @@ -1,38 +1,215 @@ +// Package jsonschema provides JSON Schema validation capabilities for the modular framework. +// +// This module integrates JSON Schema validation into the modular framework, allowing +// applications to validate JSON data against predefined schemas. It supports schema +// compilation from files or URLs and provides multiple validation methods for different +// data sources. +// +// # Features +// +// The jsonschema module provides the following capabilities: +// - JSON Schema compilation from files, URLs, or embedded schemas +// - Validation of JSON data from multiple sources (bytes, readers, interfaces) +// - Error reporting with detailed validation failure information +// - Service interface for dependency injection +// - Support for JSON Schema draft versions through underlying library +// - Thread-safe schema compilation and validation +// +// # Schema Sources +// +// Schemas can be loaded from various sources: +// - Local files: "/path/to/schema.json" +// - URLs: "https://example.com/api/schema.json" +// - Embedded schemas: "embedded://user-schema" +// - Schema registry: "registry://user/v1" +// +// # Service Registration +// +// The module registers a JSON schema service for dependency injection: +// +// // Get the JSON schema service +// schemaService := app.GetService("jsonschema.service").(jsonschema.JSONSchemaService) +// +// // Compile a schema +// schema, err := schemaService.CompileSchema("/path/to/user-schema.json") +// +// // Validate JSON data +// err = schemaService.ValidateBytes(schema, jsonData) +// +// # Usage Examples +// +// Schema compilation and basic validation: +// +// // Compile a schema from file +// schema, err := schemaService.CompileSchema("./schemas/user.json") +// if err != nil { +// return fmt.Errorf("failed to compile schema: %w", err) +// } +// +// // Validate JSON bytes +// jsonData := []byte(`{"name": "John", "age": 30}`) +// err = schemaService.ValidateBytes(schema, jsonData) +// if err != nil { +// return fmt.Errorf("validation failed: %w", err) +// } +// +// Validating different data sources: +// +// // Validate from HTTP request body +// err = schemaService.ValidateReader(schema, request.Body) +// +// // Validate Go structs/interfaces +// userData := map[string]interface{}{ +// "name": "Alice", +// "age": 25, +// "email": "alice@example.com", +// } +// err = schemaService.ValidateInterface(schema, userData) +// +// HTTP API validation example: +// +// func validateUserHandler(schemaService jsonschema.JSONSchemaService) http.HandlerFunc { +// // Compile schema once at startup +// userSchema, err := schemaService.CompileSchema("./schemas/user.json") +// if err != nil { +// log.Fatal("Failed to compile user schema:", err) +// } +// +// return func(w http.ResponseWriter, r *http.Request) { +// // Validate request body against schema +// if err := schemaService.ValidateReader(userSchema, r.Body); err != nil { +// http.Error(w, "Invalid request: "+err.Error(), http.StatusBadRequest) +// return +// } +// +// // Process valid request... +// } +// } +// +// Configuration validation: +// +// // Validate application configuration +// configSchema, err := schemaService.CompileSchema("./schemas/config.json") +// if err != nil { +// return err +// } +// +// configData := map[string]interface{}{ +// "database": map[string]interface{}{ +// "host": "localhost", +// "port": 5432, +// }, +// "logging": map[string]interface{}{ +// "level": "info", +// }, +// } +// +// if err := schemaService.ValidateInterface(configSchema, configData); err != nil { +// return fmt.Errorf("invalid configuration: %w", err) +// } +// +// # Schema Definition Examples +// +// User schema example (user.json): +// +// { +// "$schema": "https://json-schema.org/draft/2020-12/schema", +// "type": "object", +// "properties": { +// "name": { +// "type": "string", +// "minLength": 1, +// "maxLength": 100 +// }, +// "age": { +// "type": "integer", +// "minimum": 0, +// "maximum": 150 +// }, +// "email": { +// "type": "string", +// "format": "email" +// } +// }, +// "required": ["name", "age"], +// "additionalProperties": false +// } +// +// # Error Handling +// +// The module provides detailed error information for validation failures, +// including the specific path and reason for each validation error. This +// helps in providing meaningful feedback to users and debugging schema issues. package jsonschema import ( "github.com/GoCodeAlone/modular" ) +// Name is the unique identifier for the jsonschema module. const Name = "modular.jsonschema" +// Module provides JSON Schema validation capabilities for the modular framework. +// It integrates JSON Schema validation into the service system and provides +// a simple interface for schema compilation and data validation. +// +// The module implements the following interfaces: +// - modular.Module: Basic module lifecycle +// - modular.ServiceAware: Service dependency management +// +// The module is stateless and thread-safe, making it suitable for +// concurrent validation operations in web applications and services. type Module struct { schemaService JSONSchemaService } +// NewModule creates a new instance of the JSON schema module. +// This is the primary constructor for the jsonschema module and should be used +// when registering the module with the application. +// +// The module is stateless and creates a new schema service instance +// with a configured JSON schema compiler. +// +// Example: +// +// app.RegisterModule(jsonschema.NewModule()) func NewModule() *Module { return &Module{ schemaService: NewJSONSchemaService(), } } +// Name returns the unique identifier for this module. +// This name is used for service registration and dependency resolution. func (m *Module) Name() string { return Name } +// Init initializes the JSON schema module. +// The module requires no initialization and is ready to use immediately. +// This method is called during application startup but performs no operations. func (m *Module) Init(app modular.Application) error { return nil } +// ProvidesServices declares services provided by this module. +// The jsonschema module provides a schema validation service that can be +// injected into other modules for JSON data validation. +// +// Provided services: +// - "jsonschema.service": The JSONSchemaService interface for validation operations func (m *Module) ProvidesServices() []modular.ServiceProvider { return []modular.ServiceProvider{ { - Name: "jsonschema.service", - Instance: m.schemaService, + Name: "jsonschema.service", + Description: "JSON Schema validation service for data validation", + Instance: m.schemaService, }, } } +// RequiresServices declares services required by this module. +// The jsonschema module operates independently and requires no external services. func (m *Module) RequiresServices() []modular.ServiceDependency { return nil } diff --git a/modules/jsonschema/service.go b/modules/jsonschema/service.go index f16a7adf..d9cfb9f2 100644 --- a/modules/jsonschema/service.go +++ b/modules/jsonschema/service.go @@ -8,37 +8,135 @@ import ( "github.com/santhosh-tekuri/jsonschema/v6" ) -// Schema represents a compiled JSON schema +// Schema represents a compiled JSON schema. +// A compiled schema can be used multiple times for validation operations +// and is thread-safe for concurrent use. Schemas should be compiled once +// and reused for performance. type Schema interface { - // Validate validates the given value against the JSON schema + // Validate validates the given value against the JSON schema. + // The value can be any Go data structure that represents JSON data. + // Returns an error if validation fails, with details about the failure. + // + // Example: + // data := map[string]interface{}{"name": "John", "age": 30} + // err := schema.Validate(data) + // if err != nil { + // // Handle validation error + // } Validate(value interface{}) error } -// JSONSchemaService defines the operations that can be performed with JSON schemas +// JSONSchemaService defines the operations that can be performed with JSON schemas. +// This service provides methods for compiling schemas from various sources and +// validating JSON data in different formats. +// +// The service is thread-safe and can be used concurrently from multiple goroutines. +// Schemas should be compiled once and cached for reuse to avoid performance overhead. +// +// Example usage: +// +// // Compile schema once +// schema, err := service.CompileSchema("./user-schema.json") +// if err != nil { +// return err +// } +// +// // Use for multiple validations +// err = service.ValidateBytes(schema, jsonData1) +// err = service.ValidateBytes(schema, jsonData2) type JSONSchemaService interface { - // CompileSchema compiles a JSON schema from a file path or URL + // CompileSchema compiles a JSON schema from a file path or URL. + // The source can be a local file path, HTTP/HTTPS URL, or other URI + // supported by the underlying JSON schema library. + // + // Supported source formats: + // - Local files: "/path/to/schema.json", "./schemas/user.json" + // - HTTP URLs: "https://example.com/schemas/user.json" + // - HTTPS URLs: "https://schemas.org/draft/2020-12/schema" + // + // The compiled schema is cached and can be reused for multiple validations. + // Compilation is relatively expensive, so schemas should be compiled once + // at application startup when possible. + // + // Example: + // schema, err := service.CompileSchema("./schemas/user.json") + // if err != nil { + // return fmt.Errorf("failed to compile schema: %w", err) + // } CompileSchema(source string) (Schema, error) - // ValidateBytes validates raw JSON data against a compiled schema + // ValidateBytes validates raw JSON data against a compiled schema. + // The data parameter should contain valid JSON bytes. The method + // unmarshals the JSON and validates it against the schema. + // + // This is useful when you have JSON data as a byte slice, such as + // from HTTP request bodies, file contents, or network messages. + // + // Example: + // jsonData := []byte(`{"name": "John", "age": 30}`) + // err := service.ValidateBytes(schema, jsonData) + // if err != nil { + // return fmt.Errorf("validation failed: %w", err) + // } ValidateBytes(schema Schema, data []byte) error - // ValidateReader validates JSON from an io.Reader against a compiled schema + // ValidateReader validates JSON from an io.Reader against a compiled schema. + // This method reads JSON data from the reader, unmarshals it, and validates + // against the schema. The reader is consumed entirely during validation. + // + // This is useful for validating streaming JSON data, HTTP request bodies, + // or large JSON files without loading everything into memory first. + // + // Example: + // file, err := os.Open("data.json") + // if err != nil { + // return err + // } + // defer file.Close() + // + // err = service.ValidateReader(schema, file) + // if err != nil { + // return fmt.Errorf("file validation failed: %w", err) + // } ValidateReader(schema Schema, reader io.Reader) error - // ValidateInterface validates a Go interface{} against a compiled schema + // ValidateInterface validates a Go interface{} against a compiled schema. + // The data parameter should be a Go data structure that represents JSON data, + // such as maps, slices, and primitive types returned by json.Unmarshal. + // + // This is useful when you already have unmarshaled JSON data or when + // working with Go structs that need validation against a schema. + // + // Example: + // userData := map[string]interface{}{ + // "name": "Alice", + // "age": 25, + // "email": "alice@example.com", + // } + // err := service.ValidateInterface(schema, userData) + // if err != nil { + // return fmt.Errorf("user data invalid: %w", err) + // } ValidateInterface(schema Schema, data interface{}) error } -// schemaServiceImpl is the concrete implementation of JSONSchemaService +// schemaServiceImpl is the concrete implementation of JSONSchemaService. +// It uses the santhosh-tekuri/jsonschema library for JSON schema compilation +// and validation. The implementation is thread-safe and can handle concurrent +// schema compilation and validation operations. type schemaServiceImpl struct { compiler *jsonschema.Compiler } -// schemaWrapper wraps the jsonschema.Schema to implement our Schema interface +// schemaWrapper wraps the jsonschema.Schema to implement our Schema interface. +// This wrapper provides a consistent interface while hiding the underlying +// implementation details from consumers of the service. type schemaWrapper struct { schema *jsonschema.Schema } +// Validate validates the given value against the JSON schema. +// Returns a wrapped error with additional context if validation fails. func (s *schemaWrapper) Validate(value interface{}) error { if err := s.schema.Validate(value); err != nil { return fmt.Errorf("schema validation failed: %w", err) @@ -46,13 +144,21 @@ func (s *schemaWrapper) Validate(value interface{}) error { return nil } -// NewJSONSchemaService creates a new JSON schema service +// NewJSONSchemaService creates a new JSON schema service. +// The service is initialized with a fresh compiler instance and is ready +// to compile schemas and perform validations immediately. +// +// The service uses sensible defaults and supports JSON Schema draft versions +// as configured by the underlying jsonschema library. func NewJSONSchemaService() JSONSchemaService { return &schemaServiceImpl{ compiler: jsonschema.NewCompiler(), } } +// CompileSchema compiles a JSON schema from the specified source. +// The source can be a file path, URL, or other URI supported by the compiler. +// Returns a Schema interface that can be used for validation operations. func (s *schemaServiceImpl) CompileSchema(source string) (Schema, error) { schema, err := s.compiler.Compile(source) if err != nil { @@ -61,6 +167,9 @@ func (s *schemaServiceImpl) CompileSchema(source string) (Schema, error) { return &schemaWrapper{schema: schema}, nil } +// ValidateBytes validates raw JSON data against a compiled schema. +// The method unmarshals the JSON data and then validates it against the schema. +// Returns an error if either unmarshaling or validation fails. func (s *schemaServiceImpl) ValidateBytes(schema Schema, data []byte) error { var v interface{} if err := json.Unmarshal(data, &v); err != nil { @@ -72,6 +181,9 @@ func (s *schemaServiceImpl) ValidateBytes(schema Schema, data []byte) error { return nil } +// ValidateReader validates JSON from an io.Reader against a compiled schema. +// The method reads and unmarshals JSON from the reader, then validates it. +// The reader is consumed entirely during the operation. func (s *schemaServiceImpl) ValidateReader(schema Schema, reader io.Reader) error { v, err := jsonschema.UnmarshalJSON(reader) if err != nil { @@ -83,6 +195,9 @@ func (s *schemaServiceImpl) ValidateReader(schema Schema, reader io.Reader) erro return nil } +// ValidateInterface validates a Go interface{} against a compiled schema. +// The data should be a structure that represents JSON data (maps, slices, primitives). +// This is the most direct validation method when you already have unmarshaled data. func (s *schemaServiceImpl) ValidateInterface(schema Schema, data interface{}) error { if err := schema.Validate(data); err != nil { return fmt.Errorf("interface validation failed: %w", err) diff --git a/modules/letsencrypt/module.go b/modules/letsencrypt/module.go index d6c9968b..8c1e3274 100644 --- a/modules/letsencrypt/module.go +++ b/modules/letsencrypt/module.go @@ -1,5 +1,125 @@ // Package letsencrypt provides a module for automatic SSL certificate generation // via Let's Encrypt for the modular framework. +// +// This module integrates Let's Encrypt ACME protocol support into the modular framework, +// enabling automatic SSL/TLS certificate provisioning, renewal, and management. It supports +// multiple challenge types and DNS providers for flexible certificate acquisition. +// +// # Features +// +// The letsencrypt module provides the following capabilities: +// - Automatic SSL certificate acquisition from Let's Encrypt +// - Support for HTTP-01 and DNS-01 challenge types +// - Multiple DNS provider integrations (Cloudflare, Route53, DigitalOcean, etc.) +// - Automatic certificate renewal before expiration +// - Certificate storage and management +// - Staging and production environment support +// - Service interface for integration with HTTP servers +// +// # Challenge Types +// +// The module supports two ACME challenge types: +// - HTTP-01: Domain validation via HTTP endpoints +// - DNS-01: Domain validation via DNS TXT records +// +// # Supported DNS Providers +// +// When using DNS-01 challenges, the following providers are supported: +// - Cloudflare +// - AWS Route53 +// - DigitalOcean +// - Google Cloud DNS +// - Azure DNS +// - Namecheap +// +// # Configuration +// +// The module can be configured through the LetsEncryptConfig structure: +// +// config := &LetsEncryptConfig{ +// Email: "admin@example.com", +// Domains: []string{"example.com", "www.example.com"}, +// ChallengeType: "http-01", // or "dns-01" +// DNSProvider: "cloudflare", // for DNS challenges +// CADirectory: CAProduction, // or CAStaging for testing +// CertificatePath: "/etc/ssl/certs", // certificate storage path +// KeyPath: "/etc/ssl/private", // private key storage path +// DNSProviderConfig: &CloudflareConfig{ +// APIToken: "your-cloudflare-token", +// }, +// } +// +// # Service Registration +// +// The module registers itself as a certificate service: +// +// // Get the certificate service +// certService := app.GetService("letsencrypt.certificates").(letsencrypt.CertificateService) +// +// // Get a certificate for a domain +// cert, err := certService.GetCertificate("example.com") +// +// // Configure TLS with automatic certificates +// tlsConfig := &tls.Config{ +// GetCertificate: certService.GetCertificate, +// } +// +// # Usage Examples +// +// Basic HTTP server with automatic HTTPS: +// +// // Configure Let's Encrypt module +// config := &LetsEncryptConfig{ +// Email: "admin@example.com", +// Domains: []string{"example.com"}, +// ChallengeType: "http-01", +// CADirectory: CAProduction, +// } +// +// // Get certificate service +// certService := app.GetService("letsencrypt.certificates").(CertificateService) +// +// // Create TLS config with automatic certificates +// tlsConfig := &tls.Config{ +// GetCertificate: certService.GetCertificate, +// } +// +// // Start HTTPS server +// server := &http.Server{ +// Addr: ":443", +// TLSConfig: tlsConfig, +// Handler: httpHandler, +// } +// server.ListenAndServeTLS("", "") +// +// DNS challenge with Cloudflare: +// +// config := &LetsEncryptConfig{ +// Email: "admin@example.com", +// Domains: []string{"example.com", "*.example.com"}, +// ChallengeType: "dns-01", +// DNSProvider: "cloudflare", +// DNSProviderConfig: &CloudflareConfig{ +// APIToken: os.Getenv("CLOUDFLARE_API_TOKEN"), +// }, +// } +// +// # Certificate Management +// +// The module automatically handles: +// - Certificate acquisition on first request +// - Certificate renewal (default: 30 days before expiration) +// - Certificate storage and loading +// - OCSP stapling support +// - Certificate chain validation +// +// # Security Considerations +// +// - Use staging environment for testing to avoid rate limits +// - Store API credentials securely (environment variables, secrets) +// - Ensure proper file permissions for certificate storage +// - Monitor certificate expiration and renewal logs +// - Use strong private keys (RSA 2048+ or ECDSA P-256+) package letsencrypt import ( @@ -29,16 +149,33 @@ import ( // Constants for Let's Encrypt URLs const ( - // CAStaging is the URL for Let's Encrypt's staging environment + // CAStaging is the URL for Let's Encrypt's staging environment. + // Use this for testing to avoid hitting production rate limits. + // Certificates from staging are not trusted by browsers. CAStaging = "https://acme-staging-v02.api.letsencrypt.org/directory" - // CAProduction is the URL for Let's Encrypt's production environment + + // CAProduction is the URL for Let's Encrypt's production environment. + // Use this for production deployments. Has strict rate limits. + // Certificates from production are trusted by all major browsers. CAProduction = "https://acme-v02.api.letsencrypt.org/directory" ) -// ModuleName is the name of this module +// ModuleName is the unique identifier for the letsencrypt module. const ModuleName = "letsencrypt" -// LetsEncryptModule represents the Let's Encrypt module +// LetsEncryptModule provides automatic SSL certificate management using Let's Encrypt. +// It handles certificate acquisition, renewal, and storage with support for multiple +// challenge types and DNS providers. +// +// The module implements the following interfaces: +// - modular.Module: Basic module lifecycle +// - modular.Configurable: Configuration management +// - modular.ServiceAware: Service dependency management +// - modular.Startable: Startup logic +// - modular.Stoppable: Shutdown logic +// - CertificateService: Certificate management interface +// +// Certificate operations are thread-safe and support concurrent requests. type LetsEncryptModule struct { config *LetsEncryptConfig client *lego.Client @@ -47,7 +184,7 @@ type LetsEncryptModule struct { certMutex sync.RWMutex shutdownChan chan struct{} renewalTicker *time.Ticker - rootCAs *x509.CertPool // Changed from *tls.CertPool to *x509.CertPool + rootCAs *x509.CertPool // Certificate authority root certificates } // User implements the ACME User interface for Let's Encrypt diff --git a/modules/reverseproxy/module.go b/modules/reverseproxy/module.go index 16075ba3..be0d737a 100644 --- a/modules/reverseproxy/module.go +++ b/modules/reverseproxy/module.go @@ -23,6 +23,24 @@ import ( // ReverseProxyModule provides a modular reverse proxy implementation with support for // multiple backends, composite routes that combine responses from different backends, // and tenant-specific routing configurations. +// +// The module implements the following interfaces: +// - modular.Module: Basic module lifecycle +// - modular.Configurable: Configuration management +// - modular.ServiceAware: Service dependency management +// - modular.TenantAwareModule: Tenant lifecycle management +// - modular.Startable: Startup logic +// - modular.Stoppable: Shutdown logic +// +// Key features include: +// - Multi-backend proxy routing with health checks +// - Composite responses combining multiple backend calls +// - Circuit breakers for fault tolerance +// - Response caching for performance optimization +// - Tenant-aware routing and configuration +// - Request/response transformation pipelines +// - Comprehensive metrics collection +// - Path-based and header-based routing rules type ReverseProxyModule struct { config *ReverseProxyConfig router routerService @@ -46,8 +64,19 @@ type ReverseProxyModule struct { } // NewModule creates a new ReverseProxyModule with default settings. -// It initializes the HTTP client with optimized connection pooling and timeouts, -// and prepares the internal data structures needed for routing. +// This is the primary constructor for the reverseproxy module and should be used +// when registering the module with the application. +// +// The module initializes with: +// - Optimized HTTP client with connection pooling +// - Circuit breakers for each backend +// - Response caching infrastructure +// - Metrics collection (if enabled) +// - Thread-safe data structures for concurrent access +// +// Example: +// +// app.RegisterModule(reverseproxy.NewModule()) func NewModule() *ReverseProxyModule { // We'll initialize with a nil client and create it later // either in Constructor (if httpclient service is available) diff --git a/modules/scheduler/module.go b/modules/scheduler/module.go index 7ca030c7..50cdfe71 100644 --- a/modules/scheduler/module.go +++ b/modules/scheduler/module.go @@ -1,3 +1,59 @@ +// Package scheduler provides job scheduling and task execution capabilities for the modular framework. +// +// This module implements a flexible job scheduler that supports both immediate and scheduled +// job execution, configurable worker pools, job persistence, and comprehensive job lifecycle +// management. It's designed for reliable background task processing in web applications and services. +// +// # Features +// +// The scheduler module provides the following capabilities: +// - Immediate and scheduled job execution +// - Configurable worker pools for concurrent processing +// - Job persistence with multiple storage backends +// - Job status tracking and lifecycle management +// - Automatic job cleanup and retention policies +// - Service interface for dependency injection +// - Thread-safe operations for concurrent access +// +// # Service Registration +// +// The module registers a scheduler service for dependency injection: +// +// // Get the scheduler service +// scheduler := app.GetService("scheduler.provider").(*SchedulerModule) +// +// // Schedule immediate job +// job := scheduler.ScheduleJob("process-data", processDataFunc, time.Now()) +// +// // Schedule delayed job +// futureTime := time.Now().Add(time.Hour) +// job := scheduler.ScheduleJob("cleanup", cleanupFunc, futureTime) +// +// # Usage Examples +// +// Basic job scheduling: +// +// // Define a job function +// emailJob := func(ctx context.Context) error { +// return sendEmail("user@example.com", "Welcome!") +// } +// +// // Schedule immediate execution +// job := scheduler.ScheduleJob("send-welcome-email", emailJob, time.Now()) +// +// // Schedule for later +// scheduledTime := time.Now().Add(time.Minute * 30) +// job := scheduler.ScheduleJob("send-reminder", reminderJob, scheduledTime) +// +// Job with custom options: +// +// // Create scheduler with custom options +// customScheduler := NewScheduler( +// jobStore, +// WithWorkerCount(10), +// WithQueueSize(500), +// WithCheckInterval(time.Second * 5), +// ) package scheduler import ( @@ -9,13 +65,25 @@ import ( "github.com/GoCodeAlone/modular" ) -// ModuleName is the name of this module +// ModuleName is the unique identifier for the scheduler module. const ModuleName = "scheduler" -// ServiceName is the name of the service provided by this module +// ServiceName is the name of the service provided by this module. +// Other modules can use this name to request the scheduler service through dependency injection. const ServiceName = "scheduler.provider" -// SchedulerModule represents the scheduler module +// SchedulerModule provides job scheduling and task execution capabilities. +// It manages a pool of worker goroutines that execute scheduled jobs and +// provides persistence and lifecycle management for jobs. +// +// The module implements the following interfaces: +// - modular.Module: Basic module lifecycle +// - modular.Configurable: Configuration management +// - modular.ServiceAware: Service dependency management +// - modular.Startable: Startup logic +// - modular.Stoppable: Shutdown logic +// +// Job execution is thread-safe and supports concurrent job processing. type SchedulerModule struct { name string config *SchedulerConfig @@ -26,19 +94,37 @@ type SchedulerModule struct { schedulerLock sync.Mutex } -// NewModule creates a new instance of the scheduler module +// NewModule creates a new instance of the scheduler module. +// This is the primary constructor for the scheduler module and should be used +// when registering the module with the application. +// +// Example: +// +// app.RegisterModule(scheduler.NewModule()) func NewModule() modular.Module { return &SchedulerModule{ name: ModuleName, } } -// Name returns the name of the module +// Name returns the unique identifier for this module. +// This name is used for service registration, dependency resolution, +// and configuration section identification. func (m *SchedulerModule) Name() string { return m.name } -// RegisterConfig registers the module's configuration structure +// RegisterConfig registers the module's configuration structure. +// This method is called during application initialization to register +// the default configuration values for the scheduler module. +// +// Default configuration: +// - WorkerCount: 5 worker goroutines +// - QueueSize: 100 job queue capacity +// - ShutdownTimeout: 30 seconds for graceful shutdown +// - StorageType: "memory" storage backend +// - CheckInterval: 1 second for job polling +// - RetentionDays: 7 days for completed job retention func (m *SchedulerModule) RegisterConfig(app modular.Application) error { // Register the configuration with default values defaultConfig := &SchedulerConfig{ diff --git a/service.go b/service.go index dde35db7..7ff9490e 100644 --- a/service.go +++ b/service.go @@ -2,21 +2,79 @@ package modular import "reflect" -// ServiceRegistry allows registration and retrieval of services +// ServiceRegistry allows registration and retrieval of services by name. +// Services are stored as interface{} values and must be type-asserted +// when retrieved. The registry supports both concrete types and interfaces. +// +// Services enable loose coupling between modules by providing a shared +// registry where modules can publish functionality for others to consume. type ServiceRegistry map[string]any -// ServiceProvider defines a service with metadata +// ServiceProvider defines a service offered by a module. +// Services are registered in the application's service registry and can +// be consumed by other modules that declare them as dependencies. +// +// A service provider encapsulates: +// - Name: unique identifier for service lookup +// - Description: human-readable description for documentation +// - Instance: the actual service implementation (interface{}) type ServiceProvider struct { - Name string + // Name is the unique identifier for this service. + // Other modules reference this service by this exact name. + // Should be descriptive and follow naming conventions like "database", "logger", "cache". + Name string + + // Description provides human-readable documentation for this service. + // Used for debugging and documentation purposes. + // Example: "PostgreSQL database connection pool" Description string - Instance any + + // Instance is the actual service implementation. + // Can be any type - struct, interface implementation, function, etc. + // Consuming modules are responsible for type assertion. + Instance any } -// ServiceDependency defines a dependency on a service +// ServiceDependency defines a requirement for a service from another module. +// Dependencies can be matched either by exact name or by interface type. +// The framework handles dependency resolution and injection automatically. +// +// There are two main patterns for service dependencies: +// +// 1. Name-based lookup: +// ServiceDependency{Name: "database", Required: true} +// +// 2. Interface-based lookup: +// ServiceDependency{ +// Name: "logger", +// MatchByInterface: true, +// SatisfiesInterface: reflect.TypeOf((*Logger)(nil)).Elem(), +// Required: true, +// } type ServiceDependency struct { - Name string // Service name to lookup (can be empty for interface-based lookup) - Required bool // If true, application fails to start if service is missing - Type reflect.Type // Concrete type (if known) - SatisfiesInterface reflect.Type // Interface type (if known) - MatchByInterface bool // If true, find first service that satisfies interface type + // Name is the service identifier to lookup. + // For interface-based matching, this is used as the key in the + // injected services map but may not correspond to a registered service name. + Name string + + // Required indicates whether the application should fail to start + // if this service is not available. Optional services (Required: false) + // will be silently ignored if not found. + Required bool + + // Type specifies the concrete type expected for this service. + // Used for additional type checking during dependency resolution. + // Optional - if nil, no concrete type checking is performed. + Type reflect.Type + + // SatisfiesInterface specifies an interface that the service must implement. + // Used with MatchByInterface to find services by interface rather than name. + // Obtain with: reflect.TypeOf((*InterfaceName)(nil)).Elem() + SatisfiesInterface reflect.Type + + // MatchByInterface enables interface-based service lookup. + // When true, the framework will search for any service that implements + // SatisfiesInterface rather than looking up by exact name. + // Useful for loose coupling where modules depend on interfaces rather than specific implementations. + MatchByInterface bool } diff --git a/tenant.go b/tenant.go index f55e4722..ea67a555 100644 --- a/tenant.go +++ b/tenant.go @@ -1,21 +1,66 @@ // Package modular provides tenant functionality for multi-tenant applications. // This file contains tenant-related types and interfaces. +// +// The tenant functionality enables a single application instance to serve +// multiple isolated tenants, each with their own configuration, data, and +// potentially customized behavior. +// +// Key concepts: +// - TenantID: unique identifier for each tenant +// - TenantContext: context that carries tenant information through the call chain +// - TenantService: manages tenant registration and configuration +// - TenantAwareModule: modules that can adapt their behavior per tenant +// +// Example multi-tenant application setup: +// +// // Create tenant service +// tenantSvc := modular.NewStandardTenantService(logger) +// +// // Register tenant service +// app.RegisterService("tenantService", tenantSvc) +// +// // Register tenant-aware modules +// app.RegisterModule(&MyTenantAwareModule{}) +// +// // Register tenants with specific configurations +// tenantSvc.RegisterTenant("tenant-1", map[string]ConfigProvider{ +// "database": modular.NewStdConfigProvider(&DatabaseConfig{Host: "tenant1-db"}), +// }) package modular import ( "context" ) -// TenantID represents a unique tenant identifier +// TenantID represents a unique tenant identifier. +// Tenant IDs should be stable, unique strings that identify tenants +// throughout the application lifecycle. Common patterns include: +// - Customer IDs: "customer-12345" +// - Domain names: "example.com" +// - UUIDs: "550e8400-e29b-41d4-a716-446655440000" type TenantID string -// TenantContext is a context for tenant-aware operations +// TenantContext is a context for tenant-aware operations. +// It extends the standard Go context.Context interface to carry tenant +// identification through the call chain, enabling tenant-specific behavior +// in modules and services. +// +// TenantContext should be used whenever performing operations that need +// to be tenant-specific, such as database queries, configuration lookups, +// or service calls. type TenantContext struct { context.Context tenantID TenantID } -// NewTenantContext creates a new context with tenant information +// NewTenantContext creates a new context with tenant information. +// The returned context carries the tenant ID and can be used throughout +// the application to identify which tenant an operation belongs to. +// +// Example: +// +// tenantCtx := modular.NewTenantContext(ctx, "customer-123") +// result, err := tenantAwareService.DoSomething(tenantCtx, data) func NewTenantContext(ctx context.Context, tenantID TenantID) *TenantContext { return &TenantContext{ Context: ctx, @@ -23,12 +68,24 @@ func NewTenantContext(ctx context.Context, tenantID TenantID) *TenantContext { } } -// GetTenantID returns the tenant ID from the context +// GetTenantID returns the tenant ID from the context. +// This allows modules and services to determine which tenant +// the current operation is for. func (tc *TenantContext) GetTenantID() TenantID { return tc.tenantID } -// GetTenantIDFromContext attempts to extract tenant ID from a context +// GetTenantIDFromContext attempts to extract tenant ID from a context. +// Returns the tenant ID and true if the context is a TenantContext, +// or empty string and false if it's not a tenant-aware context. +// +// This is useful for functions that may or may not receive a tenant context: +// +// if tenantID, ok := modular.GetTenantIDFromContext(ctx); ok { +// // Handle tenant-specific logic +// } else { +// // Handle default/non-tenant logic +// } func GetTenantIDFromContext(ctx context.Context) (TenantID, bool) { if tc, ok := ctx.(*TenantContext); ok { return tc.GetTenantID(), true @@ -36,29 +93,123 @@ func GetTenantIDFromContext(ctx context.Context) (TenantID, bool) { return "", false } -// TenantService provides tenant management functionality +// TenantService provides tenant management functionality. +// The tenant service is responsible for: +// - Managing tenant registration and lifecycle +// - Providing tenant-specific configuration +// - Notifying modules about tenant events +// - Coordinating tenant-aware operations +// +// Applications that need multi-tenant functionality should register +// a TenantService implementation as a service named "tenantService". type TenantService interface { - // GetTenantConfig returns tenant-specific config for the given tenant and section + // GetTenantConfig returns tenant-specific configuration for the given tenant and section. + // This method looks up configuration that has been specifically registered for + // the tenant, falling back to default configuration if tenant-specific config + // is not available. + // + // The section parameter identifies which configuration section to retrieve + // (e.g., "database", "cache", "api"). + // + // Example: + // cfg, err := tenantSvc.GetTenantConfig("tenant-123", "database") + // if err != nil { + // return err + // } + // dbConfig := cfg.GetConfig().(*DatabaseConfig) GetTenantConfig(tenantID TenantID, section string) (ConfigProvider, error) - // GetTenants returns all tenant IDs + // GetTenants returns all registered tenant IDs. + // This is useful for operations that need to iterate over all tenants, + // such as maintenance tasks, reporting, or health checks. + // + // Example: + // for _, tenantID := range tenantSvc.GetTenants() { + // // Perform operation for each tenant + // err := performMaintenanceForTenant(tenantID) + // } GetTenants() []TenantID - // RegisterTenant registers a new tenant with optional initial configs + // RegisterTenant registers a new tenant with optional initial configurations. + // The configs map provides tenant-specific configuration for different sections. + // If a section is not provided in the configs map, the tenant will use the + // default application configuration for that section. + // + // Example: + // tenantConfigs := map[string]ConfigProvider{ + // "database": modular.NewStdConfigProvider(&DatabaseConfig{ + // Host: "tenant-specific-db.example.com", + // }), + // "cache": modular.NewStdConfigProvider(&CacheConfig{ + // Prefix: "tenant-123:", + // }), + // } + // err := tenantSvc.RegisterTenant("tenant-123", tenantConfigs) RegisterTenant(tenantID TenantID, configs map[string]ConfigProvider) error - // RegisterTenantAwareModule registers a module that wants to be notified about tenant lifecycle events + // RegisterTenantAwareModule registers a module that wants to be notified about tenant lifecycle events. + // Modules implementing the TenantAwareModule interface can register to receive + // notifications when tenants are added or removed, allowing them to perform + // tenant-specific initialization or cleanup. + // + // This is typically called automatically by the application framework during + // module initialization, but can also be called directly if needed. + // + // Example: + // module := &MyTenantAwareModule{} + // err := tenantSvc.RegisterTenantAwareModule(module) RegisterTenantAwareModule(module TenantAwareModule) error } // TenantAwareModule is an optional interface that modules can implement -// to receive notifications about tenant lifecycle events +// to receive notifications about tenant lifecycle events. +// +// Modules implementing this interface will be automatically registered +// with the tenant service during application initialization, and will +// receive callbacks when tenants are added or removed. +// +// This enables modules to: +// - Initialize tenant-specific resources when tenants are added +// - Clean up tenant-specific resources when tenants are removed +// - Maintain tenant-specific caches or connections +// - Perform tenant-specific migrations or setup +// +// Example implementation: +// +// type MyModule struct { +// tenantConnections map[TenantID]*Connection +// } +// +// func (m *MyModule) OnTenantRegistered(tenantID TenantID) { +// // Initialize tenant-specific resources +// conn := createConnectionForTenant(tenantID) +// m.tenantConnections[tenantID] = conn +// } +// +// func (m *MyModule) OnTenantRemoved(tenantID TenantID) { +// // Clean up tenant-specific resources +// if conn, ok := m.tenantConnections[tenantID]; ok { +// conn.Close() +// delete(m.tenantConnections, tenantID) +// } +// } type TenantAwareModule interface { Module - // OnTenantRegistered is called when a new tenant is registered + // OnTenantRegistered is called when a new tenant is registered. + // This method should be used to initialize any tenant-specific resources, + // such as database connections, caches, or configuration. + // + // The method should be non-blocking and handle errors gracefully. + // If initialization fails, the module should log the error but not + // prevent the tenant registration from completing. OnTenantRegistered(tenantID TenantID) - // OnTenantRemoved is called when a tenant is removed + // OnTenantRemoved is called when a tenant is removed. + // This method should be used to clean up any tenant-specific resources + // to prevent memory leaks or resource exhaustion. + // + // The method should be non-blocking and handle cleanup failures gracefully. + // Even if cleanup fails, the tenant removal should proceed. OnTenantRemoved(tenantID TenantID) }