modular

package module
v1.4.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 16, 2025 License: MIT Imports: 18 Imported by: 10

README ¶

modular

Modular Go

GitHub License Go Reference CodeQL Dependabot Updates CI Modules CI Examples CI Go Report Card codecov

Overview

Modular is a package that provides a structured way to create modular applications in Go. It allows you to build applications as collections of modules that can be easily added, removed, or replaced. Key features include:

  • Module lifecycle management: Initialize, start, and gracefully stop modules
  • Dependency management: Automatically resolve and order module dependencies
  • Service registry: Register and retrieve application services
  • Configuration management: Handle configuration for modules and services
  • Configuration validation: Validate configurations with defaults, required fields, and custom logic
  • Sample config generation: Generate sample configuration files in various formats
  • Dependency injection: Inject required services into modules
  • Multi-tenancy support: Build applications that serve multiple tenants with isolated configurations

🧩 Available Modules

Modular comes with a rich ecosystem of pre-built modules that you can easily integrate into your applications:

Module Description Configuration Documentation
auth Authentication and authorization with JWT, sessions, password hashing, and OAuth2/OIDC support Yes Documentation
cache Multi-backend caching with Redis and in-memory support Yes Documentation
chimux Chi router integration with middleware support Yes Documentation
database Database connectivity and SQL operations with multiple driver support Yes Documentation
eventbus Asynchronous event handling and pub/sub messaging Yes Documentation
httpclient Configurable HTTP client with connection pooling, timeouts, and verbose logging Yes Documentation
httpserver HTTP/HTTPS server with TLS support, graceful shutdown, and configurable timeouts Yes Documentation
jsonschema JSON Schema validation services No Documentation
letsencrypt SSL/TLS certificate automation with Let's Encrypt Yes Documentation
reverseproxy Reverse proxy with load balancing, circuit breaker, and health monitoring Yes Documentation
scheduler Job scheduling with cron expressions and worker pools Yes Documentation

Each module is designed to be:

  • Plug-and-play: Easy integration with minimal configuration
  • Configurable: Extensive configuration options via YAML, environment variables, or code
  • Production-ready: Built with best practices, proper error handling, and comprehensive testing
  • Well-documented: Complete documentation with examples and API references

📖 For detailed information about each module, see the modules directory or click on the individual module links above.

Examples

The examples/ directory contains complete, working examples that demonstrate how to use Modular with different patterns and module combinations:

Example Description Features
basic-app Simple modular application HTTP server, routing, configuration
reverse-proxy HTTP reverse proxy server Load balancing, backend routing, CORS
http-client HTTP client with proxy backend HTTP client integration, request routing
advanced-logging Advanced HTTP client logging Verbose logging, file output, request/response inspection
Quick Start with Examples

Each example is a complete, standalone application that you can run immediately:

cd examples/basic-app
GOWORK=off go build
./basic-app

Visit the examples directory for detailed documentation, configuration guides, and step-by-step instructions for each example.

Learning Path

Installation

go get github.com/CrisisTextLine/modular

Usage

Basic Application
package main

import (
    "github.com/CrisisTextLine/modular"
    "log/slog"
    "os"
)

func main() {
    // Create a logger
    logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
    
    // Create config provider with application configuration
    config := &AppConfig{
        Name: "MyApp",
        Version: "1.0.0",
    }
    configProvider := modular.NewStdConfigProvider(config)
    
    // Create the application
    app := modular.NewStdApplication(configProvider, logger)
    
    // Register modules
    app.RegisterModule(NewDatabaseModule())
    app.RegisterModule(NewAPIModule())
    
    // Run the application (this will block until the application is terminated)
    if err := app.Run(); err != nil {
        logger.Error("Application error", "error", err)
        os.Exit(1)
    }
}
Creating a Module
type DatabaseModule struct {
    db     *sql.DB
    config *DatabaseConfig
}

func NewDatabaseModule() modular.Module {
    return &DatabaseModule{}
}

// RegisterConfig registers the module's configuration
func (m *DatabaseModule) RegisterConfig(app modular.Application) error {
    m.config = &DatabaseConfig{
        DSN: "postgres://user:password@localhost:5432/dbname",
    }
    app.RegisterConfigSection("database", modular.NewStdConfigProvider(m.config))
    return nil
}

// Name returns the module's unique name
func (m *DatabaseModule) Name() string {
    return "database"
}

// Dependencies returns other modules this module depends on
func (m *DatabaseModule) Dependencies() []string {
    return []string{} // No dependencies
}

// Init initializes the module
func (m *DatabaseModule) Init(app modular.Application) error {
    // Initialize database connection
    db, err := sql.Open("postgres", m.config.DSN)
    if (err != nil) {
        return err
    }
    m.db = db
    return nil
}

// ProvidesServices returns services provided by this module
func (m *DatabaseModule) ProvidesServices() []modular.ServiceProvider {
    return []modular.ServiceProvider{
        {
            Name:        "database",
            Description: "Database connection",
            Instance:    m.db,
        },
    }
}

// RequiresServices returns services required by this module
func (m *DatabaseModule) RequiresServices() []modular.ServiceDependency {
    return []modular.ServiceDependency{} // No required services
}

// Start starts the module
func (m *DatabaseModule) Start(ctx context.Context) error {
    return nil // Database is already connected
}

// Stop stops the module
func (m *DatabaseModule) Stop(ctx context.Context) error {
    return m.db.Close()
}
Service Dependencies
// A module that depends on another service
func (m *APIModule) RequiresServices() []modular.ServiceDependency {
    return []modular.ServiceDependency{
        {
            Name:     "database",
            Required: true,  // Application won't start if this service is missing
        },
        {
            Name:     "cache",
            Required: false, // Optional dependency
        },
    }
}

// Using constructor injection
func (m *APIModule) Constructor() modular.ModuleConstructor {
    return func(app modular.Application, services map[string]any) (modular.Module, error) {
        // Services that were requested in RequiresServices() are available here
        db := services["database"].(*sql.DB)
        
        // Create a new module instance with injected services
        return &APIModule{
            db: db,
        }, nil
    }
}
Interface-Based Service Matching

Modular supports finding and injecting services based on interface compatibility, regardless of the service name:

// Define an interface that services should implement
type LoggerService interface {
    Log(level string, message string)
}

// Require a service that implements a specific interface
func (m *MyModule) RequiresServices() []modular.ServiceDependency {
    return []modular.ServiceDependency{
        {
            Name:               "logger", // The name you'll use to access it in the Constructor
            Required:           true,
            MatchByInterface:   true, // Enable interface-based matching
            SatisfiesInterface: reflect.TypeOf((*LoggerService)(nil)).Elem(),
        },
    }
}

// Constructor will receive any service implementing LoggerService
func (m *MyModule) Constructor() modular.ModuleConstructor {
    return func(app modular.Application, services map[string]any) (modular.Module, error) {
        // This will work even if the actual registered service name is different
        logger := services["logger"].(LoggerService)
        return &MyModule{logger: logger}, nil
    }
}

See DOCUMENTATION.md for more advanced details about service dependencies and interface matching.

Logger Management

The framework provides methods to get and set the application logger, allowing for dynamic logger configuration at runtime:

// Get the current logger
currentLogger := app.Logger()

// Switch to a different logger (e.g., for different log levels or output destinations)
newLogger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
    Level: slog.LevelDebug,
}))
app.SetLogger(newLogger)

// The new logger is now used by the application and all modules
app.Logger().Info("Logger has been switched to JSON format with debug level")

This is useful for scenarios such as:

  • Dynamic log level changes: Switch between debug and production logging based on runtime conditions
  • Configuration-driven logging: Update logger configuration based on config file changes
  • Environment-specific loggers: Use different loggers for development vs production
  • Log rotation: Switch to new log files without restarting the application

Example: Dynamic log level switching

// Switch to debug logging when needed
debugLogger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
    Level: slog.LevelDebug,
}))
app.SetLogger(debugLogger)

// Later, switch back to info level
infoLogger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
    Level: slog.LevelInfo,
}))
app.SetLogger(infoLogger)
Configuration Management
// Define your configuration struct
type AppConfig struct {
    Name    string `json:"name" yaml:"name" default:"DefaultApp" desc:"Application name"`
    Version string `json:"version" yaml:"version" required:"true" desc:"Application version"`
    Debug   bool   `json:"debug" yaml:"debug" default:"false" desc:"Enable debug mode"`
}

// Implement ConfigValidator interface for custom validation
func (c *AppConfig) Validate() error {
    // Custom validation logic
    if c.Version == "0.0.0" {
        return fmt.Errorf("invalid version: %s", c.Version)
    }
    return nil
}
Configuration Validation and Default Values

Modular now includes powerful configuration validation features:

Default Values with Struct Tags
// Define struct with default values
type ServerConfig struct {
    Host        string `yaml:"host" default:"localhost" desc:"Server host"`
    Port        int    `yaml:"port" default:"8080" desc:"Server port"`
    ReadTimeout int    `yaml:"readTimeout" default:"30" desc:"Read timeout in seconds"`
    Debug       bool   `yaml:"debug" default:"false" desc:"Enable debug mode"`
}

Default values are automatically applied to fields that have zero/empty values when configurations are loaded.

Required Field Validation
type DatabaseConfig struct {
    Host     string `yaml:"host" default:"localhost" desc:"Database host"`
    Port     int    `yaml:"port" default:"5432" desc:"Database port"`
    Name     string `yaml:"name" required:"true" desc:"Database name"` // Must be provided
    User     string `yaml:"user" default:"postgres" desc:"Database user"`
    Password string `yaml:"password" required:"true" desc:"Database password"` // Must be provided
}

Required fields are validated during configuration loading, and appropriate errors are returned if they're missing.

Custom Validation Logic
// Implement the ConfigValidator interface
func (c *AppConfig) Validate() error {
    // Validate environment is one of the expected values
    validEnvs := map[string]bool{"dev": true, "test": true, "prod": true}
    if !validEnvs[c.Environment] {
        return fmt.Errorf("%w: environment must be one of [dev, test, prod]", modular.ErrConfigValidationFailed)
    }
    
    // Additional custom validation
    if c.Server.Port < 1024 || c.Server.Port > 65535 {
        return fmt.Errorf("%w: server port must be between 1024 and 65535", modular.ErrConfigValidationFailed)
    }
    
    return nil
}
Generating Sample Configuration Files
// Generate a sample configuration file
cfg := &AppConfig{}
err := modular.SaveSampleConfig(cfg, "yaml", "config-sample.yaml")
if err != nil {
    log.Fatalf("Error generating sample config: %v", err)
}

Sample configurations can be generated in YAML, JSON, or TOML formats, with all default values pre-populated.

Command-Line Integration
func main() {
    // Generate sample config file if requested
    if len(os.Args) > 1 && os.Args[1] == "--generate-config" {
        format := "yaml"
        if len(os.Args) > 2 {
            format = os.Args[2]
        }
        outputFile := "config-sample." + format
        if len(os.Args) > 3 {
            outputFile = os.Args[3]
        }
        
        cfg := &AppConfig{}
        if err := modular.SaveSampleConfig(cfg, format, outputFile); err != nil {
            fmt.Printf("Error generating sample config: %v\n", err)
            os.Exit(1)  // Error condition should exit with non-zero code
        }
        fmt.Printf("Sample config generated at %s\n", outputFile)
        os.Exit(0)
    }
    
    // Continue with normal application startup...
}

Multi-Tenant Support

Modular provides built-in support for multi-tenant applications through:

Tenant Contexts
// Creating a tenant context
tenantID := modular.TenantID("tenant1")
ctx := modular.NewTenantContext(context.Background(), tenantID)

// Using tenant context with the application
tenantCtx, err := app.WithTenant(tenantID)
if err != nil {
    log.Fatal("Failed to create tenant context:", err)
}

// Extract tenant ID from a context
if id, ok := modular.GetTenantIDFromContext(ctx); ok {
    fmt.Println("Current tenant:", id)
}
Tenant-Aware Configuration
// Register a tenant service in your module
func (m *MultiTenantModule) ProvidesServices() []modular.ServiceProvider {
    return []modular.ServiceProvider{
        {
            Name:        "tenantService",
            Description: "Tenant management service",
            Instance:    modular.NewStandardTenantService(m.logger),
        },
        {
            Name:        "tenantConfigLoader",
            Description: "Tenant configuration loader",
            Instance:    modular.DefaultTenantConfigLoader("./configs/tenants"),
        },
    }
}

// Create tenant-aware configuration
func (m *MultiTenantModule) RegisterConfig(app *modular.Application) {
    // Default config
    defaultConfig := &MyConfig{
        Setting: "default",
    }
    
    // Get tenant service (must be provided by another module)
    var tenantService modular.TenantService
    app.GetService("tenantService", &tenantService)
    
    // Create tenant-aware config provider
    tenantAwareConfig := modular.NewTenantAwareConfig(
        modular.NewStdConfigProvider(defaultConfig),
        tenantService,
        "mymodule",
    )
    
    app.RegisterConfigSection("mymodule", tenantAwareConfig)
}

// Using tenant-aware configs in your code
func (m *MultiTenantModule) ProcessRequestWithTenant(ctx context.Context) {
    // Get config specific to the tenant in the context
    config, ok := m.config.(*modular.TenantAwareConfig)
    if !ok {
        // Handle non-tenant-aware config
        return
    }
    
    // Get tenant-specific configuration
    myConfig := config.GetConfigWithContext(ctx).(*MyConfig)
    
    // Use tenant-specific settings
    fmt.Println("Tenant setting:", myConfig.Setting)
}
Tenant-Aware Modules
// Implement the TenantAwareModule interface
type MultiTenantModule struct {
    modular.Module
    tenantData map[modular.TenantID]*TenantData
}

func (m *MultiTenantModule) OnTenantRegistered(tenantID modular.TenantID) {
    // Initialize resources for this tenant
    m.tenantData[tenantID] = &TenantData{
        initialized: true,
    }
}

func (m *MultiTenantModule) OnTenantRemoved(tenantID modular.TenantID) {
    // Clean up tenant resources
    delete(m.tenantData, tenantID)
}
Loading Tenant Configurations
// Set up a file-based tenant config loader
configLoader := modular.NewFileBasedTenantConfigLoader(modular.TenantConfigParams{
    ConfigNameRegex: regexp.MustCompile("^tenant-[\\w-]+\\.(json|yaml)$"),
    ConfigDir:       "./configs/tenants",
    ConfigFeeders:   []modular.Feeder{},
})

// Register the loader as a service
app.RegisterService("tenantConfigLoader", configLoader)

Key Interfaces

Module

The core interface that all modules must implement:

type Module interface {
    RegisterConfig(app *Application)
    Init(app *Application) error
    Start(ctx context.Context) error
    Stop(ctx context.Context) error
    Name() string
    Dependencies() []string
    ProvidesServices() []ServiceProvider
    RequiresServices() []ServiceDependency
}
TenantAwareModule

Interface for modules that need to respond to tenant lifecycle events:

type TenantAwareModule interface {
    Module
    OnTenantRegistered(tenantID TenantID)
    OnTenantRemoved(tenantID TenantID)
}
TenantService

Interface for managing tenants:

type TenantService interface {
    GetTenantConfig(tenantID TenantID, section string) (ConfigProvider, error)
    GetTenants() []TenantID
    RegisterTenant(tenantID TenantID, configs map[string]ConfigProvider) error
}
ConfigProvider

Interface for configuration providers:

type ConfigProvider interface {
    GetConfig() any
}
ConfigValidator

Interface for implementing custom configuration validation logic:

type ConfigValidator interface {
    Validate() error
}

CLI Tool

Modular comes with a command-line tool (modcli) to help you create new modules and configurations.

Installation

You can install the CLI tool using one of the following methods:

go install github.com/CrisisTextLine/modular/cmd/modcli@latest

This will download, build, and install the latest version of the CLI tool directly to your GOPATH's bin directory, which should be in your PATH.

Download pre-built binaries

Download the latest release from the GitHub Releases page and add it to your PATH.

Build from source
# Clone the repository
git clone https://github.com/CrisisTextLine/modular.git
cd modular/cmd/modcli

# Build the CLI tool
go build -o modcli

# Move to a directory in your PATH
mv modcli /usr/local/bin/  # Linux/macOS
# or add the current directory to your PATH
Usage

Generate a new module:

modcli generate module --name MyFeature

Generate a configuration:

modcli generate config --name Server

For more details on available commands:

modcli --help

Each command includes interactive prompts to guide you through the process of creating modules or configurations with the features you need.

📚 Additional Resources

Having Issues?

If you're experiencing problems with module interfaces (e.g., "Module does not implement Startable"), check out the debugging section which includes diagnostic tools like:

// Debug module interface implementations
modular.DebugModuleInterfaces(app, "your-module-name")

// Check all modules at once
modular.DebugAllModuleInterfaces(app)

License

MIT License

Documentation ¶

Overview ¶

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 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 provides tenant-aware functionality for multi-tenant applications. This file contains the core tenant service implementation.

Index ¶

Constants ¶

This section is empty.

Variables ¶

View Source
var (
	// 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 - 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")
	ErrConfigRequiredFieldMissing = errors.New("required field is missing")
	ErrConfigValidationFailed     = errors.New("config validation failed")
	ErrUnsupportedTypeForDefault  = errors.New("unsupported type for default value")
	ErrDefaultValueParseError     = errors.New("failed to parse default value")
	ErrDefaultValueOverflowsInt   = errors.New("default value overflows int")
	ErrDefaultValueOverflowsUint  = errors.New("default value overflows uint")
	ErrDefaultValueOverflowsFloat = errors.New("default value overflows float")
	ErrInvalidFieldKind           = errors.New("invalid field kind")
	ErrIncompatibleFieldKind      = errors.New("incompatible field kind")
	ErrUnexpectedFieldKind        = errors.New("unexpected field kind")
	ErrUnsupportedFormatType      = errors.New("unsupported format type")
	ErrConfigFeederError          = errors.New("config feeder error")
	ErrConfigSetupError           = errors.New("config setup error")
	ErrConfigNilPointer           = errors.New("config is nil pointer")
	ErrFieldCannotBeSet           = errors.New("field cannot be set")

	// Service registry errors
	ErrServiceAlreadyRegistered = errors.New("service already registered")
	ErrServiceNotFound          = errors.New("service not found")

	// Service injection errors
	ErrTargetNotPointer      = errors.New("target must be a non-nil pointer")
	ErrTargetValueInvalid    = errors.New("target value is invalid")
	ErrServiceIncompatible   = errors.New("service cannot be assigned to target")
	ErrServiceNil            = errors.New("service is nil")
	ErrServiceWrongType      = errors.New("service doesn't satisfy required type")
	ErrServiceWrongInterface = errors.New("service doesn't satisfy required interface")

	// Dependency resolution errors
	ErrCircularDependency      = errors.New("circular dependency detected")
	ErrModuleDependencyMissing = errors.New("module depends on non-existent module")
	ErrRequiredServiceNotFound = errors.New("required service not found for module")

	// Constructor errors
	ErrConstructorNotFunction              = errors.New("constructor must be a function")
	ErrConstructorInvalidReturnCount       = errors.New("constructor must return exactly two values (Module, error)")
	ErrConstructorInvalidReturnType        = errors.New("constructor must return a Module as first value")
	ErrConstructorParameterServiceNotFound = errors.New("no service found for constructor parameter")
	ErrInvalidInterfaceConfiguration       = errors.New("invalid interface configuration for required service")
	ErrInterfaceConfigurationNil           = errors.New("SatisfiesInterface is nil")
	ErrInterfaceConfigurationNotInterface  = errors.New("SatisfiesInterface is not an interface type")
	ErrServiceInterfaceIncompatible        = errors.New("service does not implement required interface")

	// Tenant errors
	ErrAppContextNotInitialized        = errors.New("application context not initialized")
	ErrTenantNotFound                  = errors.New("tenant not found")
	ErrTenantConfigNotFound            = errors.New("tenant config section not found")
	ErrTenantConfigProviderNil         = errors.New("tenant config provider is nil")
	ErrTenantConfigValueNil            = errors.New("tenant config value is nil")
	ErrTenantRegisterNilConfig         = errors.New("cannot register nil config for tenant")
	ErrMockTenantConfigsNotInitialized = errors.New("mock tenant configs not initialized")
	ErrConfigSectionNotFoundForTenant  = errors.New("config section not found for tenant")

	// Test-specific errors
	ErrSetupFailed   = errors.New("setup error")
	ErrFeedFailed    = errors.New("feed error")
	ErrFeedKeyFailed = errors.New("feedKey error")

	// Tenant config errors
	ErrConfigCastFailed      = errors.New("failed to cast config to expected type")
	ErrOriginalOrLoadedNil   = errors.New("original or loaded config is nil")
	ErrDestinationNotPointer = errors.New("destination must be a pointer")
	ErrCannotCopyMapToStruct = errors.New("cannot copy from map to non-struct")
	ErrUnsupportedSourceType = errors.New("unsupported source type")

	// Additional tenant config errors
	ErrTenantSectionConfigNil     = errors.New("tenant section config is nil after feeding")
	ErrCreatedNilProvider         = errors.New("created nil provider for tenant section")
	ErrIncompatibleFieldTypes     = errors.New("incompatible types for field assignment")
	ErrIncompatibleInterfaceValue = errors.New("incompatible interface value for field")
)

Application errors

View Source
var ConfigFeeders = []Feeder{
	feeders.NewEnvFeeder(),
}

ConfigFeeders provides a default set of configuration feeders for common use cases

View Source
var (
	ErrUnsupportedExtension = errors.New("unsupported file extension")
)

Static errors for better error handling

Functions ¶

func CompareModuleInstances ¶

func CompareModuleInstances(original, current Module, moduleName string)

CompareModuleInstances compares two module instances to see if they're the same

func DebugAllModuleInterfaces ¶

func DebugAllModuleInterfaces(app Application)

DebugAllModuleInterfaces debugs all registered modules

func DebugModuleInterfaces ¶

func DebugModuleInterfaces(app Application, moduleName string)

DebugModuleInterfaces helps debug interface implementation issues during module lifecycle

func GenerateSampleConfig ¶

func GenerateSampleConfig(cfg interface{}, format string) ([]byte, error)

GenerateSampleConfig generates a sample configuration for a config struct The format parameter can be "yaml", "json", or "toml"

func LoadTenantConfigs ¶

func LoadTenantConfigs(app Application, tenantService TenantService, params TenantConfigParams) error

LoadTenantConfigs scans the given directory for config files. Each file should be named with the tenant ID (e.g. "tenant123.json"). For each file, it unmarshals the configuration and registers it with the provided TenantService for the given section. The configNameRegex is a regex pattern for the config file names (e.g. "^tenant[0-9]+\\.json$").

func ProcessConfigDefaults ¶

func ProcessConfigDefaults(cfg interface{}) 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.

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 SaveSampleConfig ¶

func SaveSampleConfig(cfg interface{}, format, filePath string) error

SaveSampleConfig generates and saves a sample configuration file

func ValidateConfig ¶

func ValidateConfig(cfg interface{}) error

ValidateConfig validates a configuration using the following steps: 1. Processes default values 2. Validates required fields 3. If the config implements ConfigValidator, calls its Validate method

func ValidateConfigRequired ¶

func ValidateConfigRequired(cfg interface{}) error

ValidateConfigRequired checks all struct fields with `required:"true"` tag and verifies they are not zero/empty values

Types ¶

type AppRegistry ¶

type AppRegistry interface {
	// 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
}

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 Application ¶

type Application interface {
	// 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 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.
	// 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.
	// 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.
	// Returns a map of section names to their configuration providers.
	// Useful for debugging and introspection.
	ConfigSections() map[string]ConfigProvider

	// 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 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 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 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 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 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.
	// 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.
	// 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.
	// Should be called before module registration to ensure
	// all framework operations use the new logger.
	SetLogger(logger Logger)

	// SetVerboseConfig enables or disables verbose configuration debugging.
	// When enabled, DEBUG level logging will be performed during configuration
	// processing to show which config is being processed, which key is being
	// evaluated, and which attribute or key is being searched for.
	SetVerboseConfig(enabled bool)

	// IsVerboseConfig returns whether verbose configuration debugging is enabled.
	IsVerboseConfig() bool
}

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)
}

func NewStdApplication ¶

func NewStdApplication(cp ConfigProvider, logger Logger) Application

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)
}

type ComplexFeeder ¶

type ComplexFeeder interface {
	Feeder
	FeedKey(string, interface{}) error
}

ComplexFeeder extends the basic Feeder interface with additional functionality for complex configuration scenarios

type Config ¶

type Config struct {
	// Feeders contains all the registered configuration feeders
	Feeders []Feeder
	// StructKeys maps struct identifiers to their configuration objects.
	// Used internally to track which configuration structures have been processed.
	StructKeys map[string]interface{}
	// VerboseDebug enables detailed logging during configuration processing
	VerboseDebug bool
	// Logger is used for verbose debug logging
	Logger Logger
	// FieldTracker tracks which fields are populated by which feeders
	FieldTracker FieldTracker
}

Config represents a configuration builder that can combine multiple feeders and structures. It provides functionality for the modular framework to coordinate configuration loading.

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
  • Enable verbose debugging for configuration processing
  • Track field-level population details

func NewConfig ¶

func NewConfig() *Config

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 (*Config) AddFeeder ¶

func (c *Config) AddFeeder(feeder Feeder) *Config

AddFeeder adds a configuration feeder to support verbose debugging and field tracking

func (*Config) AddStructKey ¶

func (c *Config) AddStructKey(key string, target interface{}) *Config

AddStructKey adds a structure with a key to the configuration

func (*Config) Feed ¶

func (c *Config) Feed() error

Feed with validation applies defaults and validates configs after feeding

func (*Config) SetFieldTracker ¶

func (c *Config) SetFieldTracker(tracker FieldTracker) *Config

SetFieldTracker sets the field tracker for capturing field population details

func (*Config) SetVerboseDebug ¶

func (c *Config) SetVerboseDebug(enabled bool, logger Logger) *Config

SetVerboseDebug enables or disables verbose debug logging

type ConfigProvider ¶

type ConfigProvider interface {
	// 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
}

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 ConfigSetup ¶

type ConfigSetup interface {
	Setup() error
}

ConfigSetup is an interface that configs can implement to perform additional setup after being populated by feeders

type ConfigValidator ¶

type ConfigValidator interface {
	// 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
}

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 Configurable ¶

type Configurable interface {
	// 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
}

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 Constructable ¶

type Constructable interface {
	// 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
}

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 DefaultFieldTracker ¶

type DefaultFieldTracker struct {
	FieldPopulations []FieldPopulation
	// contains filtered or unexported fields
}

DefaultFieldTracker is a basic implementation of FieldTracker

func NewDefaultFieldTracker ¶

func NewDefaultFieldTracker() *DefaultFieldTracker

NewDefaultFieldTracker creates a new default field tracker

func (*DefaultFieldTracker) GetFieldPopulation ¶

func (t *DefaultFieldTracker) GetFieldPopulation(fieldPath string) *FieldPopulation

GetFieldPopulation returns the population info for a specific field path

func (*DefaultFieldTracker) GetPopulationsByFeeder ¶

func (t *DefaultFieldTracker) GetPopulationsByFeeder(feederType string) []FieldPopulation

GetPopulationsByFeeder returns all field populations by a specific feeder type

func (*DefaultFieldTracker) GetPopulationsBySource ¶

func (t *DefaultFieldTracker) GetPopulationsBySource(sourceType string) []FieldPopulation

GetPopulationsBySource returns all field populations by a specific source type

func (*DefaultFieldTracker) RecordFieldPopulation ¶

func (t *DefaultFieldTracker) RecordFieldPopulation(fp FieldPopulation)

RecordFieldPopulation records a field population event

func (*DefaultFieldTracker) SetLogger ¶

func (t *DefaultFieldTracker) SetLogger(logger Logger)

SetLogger sets the logger for the tracker

type DependencyAware ¶

type DependencyAware interface {
	// 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
}

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 Feeder ¶

type Feeder interface {
	// Feed gets a struct and feeds it using configuration data.
	Feed(structure interface{}) error
}

Feeder defines the interface for configuration feeders that provide configuration data.

type FieldPopulation ¶

type FieldPopulation struct {
	FieldPath   string      // Full path to the field (e.g., "Connections.primary.DSN")
	FieldName   string      // Name of the field
	FieldType   string      // Type of the field
	FeederType  string      // Type of feeder that populated it
	SourceType  string      // Type of source (env, yaml, etc.)
	SourceKey   string      // Source key that was used (e.g., "DB_PRIMARY_DSN")
	Value       interface{} // Value that was set
	InstanceKey string      // Instance key for instance-aware fields
	SearchKeys  []string    // All keys that were searched for this field
	FoundKey    string      // The key that was actually found
}

FieldPopulation represents a single field population event

type FieldTracker ¶

type FieldTracker interface {
	// RecordFieldPopulation records that a field was populated by a feeder
	RecordFieldPopulation(fp FieldPopulation)

	// SetLogger sets the logger for the tracker
	SetLogger(logger Logger)
}

FieldTracker interface allows feeders to report which fields they populate

type FieldTrackerBridge ¶

type FieldTrackerBridge struct {
	// contains filtered or unexported fields
}

FieldTrackerBridge adapts between the main package's FieldTracker interface and the feeders package's FieldTracker interface

func NewFieldTrackerBridge ¶

func NewFieldTrackerBridge(mainTracker FieldTracker) *FieldTrackerBridge

NewFieldTrackerBridge creates a new bridge adapter

func (*FieldTrackerBridge) RecordFieldPopulation ¶

func (b *FieldTrackerBridge) RecordFieldPopulation(fp feeders.FieldPopulation)

RecordFieldPopulation implements the feeders.FieldTracker interface by converting feeders.FieldPopulation to the main package's FieldPopulation

type FieldTrackingFeeder ¶

type FieldTrackingFeeder interface {
	// SetFieldTracker sets the field tracker for this feeder
	SetFieldTracker(tracker FieldTracker)
}

FieldTrackingFeeder interface allows feeders to support field tracking

type FileBasedTenantConfigLoader ¶

type FileBasedTenantConfigLoader struct {
	// contains filtered or unexported fields
}

FileBasedTenantConfigLoader implements TenantConfigLoader for file-based tenant configurations

func DefaultTenantConfigLoader ¶

func DefaultTenantConfigLoader(configDir string) *FileBasedTenantConfigLoader

DefaultTenantConfigLoader creates a loader with default configuration

func NewFileBasedTenantConfigLoader ¶

func NewFileBasedTenantConfigLoader(params TenantConfigParams) *FileBasedTenantConfigLoader

NewFileBasedTenantConfigLoader creates a new file-based tenant config loader

func (*FileBasedTenantConfigLoader) LoadTenantConfigurations ¶

func (l *FileBasedTenantConfigLoader) LoadTenantConfigurations(app Application, tenantService TenantService) error

LoadTenantConfigurations loads tenant configurations from files

type InstanceAwareConfigProvider ¶

type InstanceAwareConfigProvider struct {
	// contains filtered or unexported fields
}

InstanceAwareConfigProvider handles configuration for multiple instances of the same type

func NewInstanceAwareConfigProvider ¶

func NewInstanceAwareConfigProvider(cfg any, prefixFunc InstancePrefixFunc) *InstanceAwareConfigProvider

NewInstanceAwareConfigProvider creates a new instance-aware configuration provider

func (*InstanceAwareConfigProvider) GetConfig ¶

func (p *InstanceAwareConfigProvider) GetConfig() any

GetConfig returns the configuration object

func (*InstanceAwareConfigProvider) GetInstancePrefixFunc ¶

func (p *InstanceAwareConfigProvider) GetInstancePrefixFunc() InstancePrefixFunc

GetInstancePrefixFunc returns the instance prefix function

type InstanceAwareConfigSupport ¶

type InstanceAwareConfigSupport interface {
	// GetInstanceConfigs returns a map of instance configurations that should be fed with instance-aware feeders
	GetInstanceConfigs() map[string]interface{}
}

InstanceAwareConfigSupport indicates that a configuration supports instance-aware feeding

type InstanceAwareFeeder ¶

type InstanceAwareFeeder interface {
	ComplexFeeder
	// FeedInstances feeds multiple instances from a map[string]ConfigType
	FeedInstances(instances interface{}) error
}

InstanceAwareFeeder provides functionality for feeding multiple instances of the same configuration type

func NewInstanceAwareEnvFeeder ¶

func NewInstanceAwareEnvFeeder(prefixFunc InstancePrefixFunc) InstanceAwareFeeder

NewInstanceAwareEnvFeeder creates a new instance-aware environment variable feeder

type InstancePrefixFunc ¶

type InstancePrefixFunc = feeders.InstancePrefixFunc

InstancePrefixFunc is a function that generates a prefix for an instance key

type LoadAppConfigFunc ¶

type LoadAppConfigFunc func(*StdApplication) error

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.

var AppConfigLoader LoadAppConfigFunc = loadAppConfig

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
}

type Logger ¶

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)
}

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 Module ¶

type Module interface {
	// 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 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
}

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 ModuleConstructor ¶

type ModuleConstructor func(app Application, services map[string]any) (Module, error)

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 ModuleRegistry ¶

type ModuleRegistry map[string]Module

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 ModuleWithConstructor ¶

type ModuleWithConstructor interface {
	Module
	Constructable
}

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 ServiceAware ¶

type ServiceAware interface {
	// 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.
	// 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
}

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 ServiceDependency ¶

type ServiceDependency struct {
	// 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
}

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 ServiceProvider ¶

type ServiceProvider struct {
	// 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 is the actual service implementation.
	// Can be any type - struct, interface implementation, function, etc.
	// Consuming modules are responsible for type assertion.
	Instance any
}

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 ServiceRegistry ¶

type ServiceRegistry map[string]any

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 StandardTenantService ¶

type StandardTenantService struct {
	// contains filtered or unexported fields
}

StandardTenantService provides a basic implementation of the TenantService interface

func NewStandardTenantService ¶

func NewStandardTenantService(logger Logger) *StandardTenantService

NewStandardTenantService creates a new tenant service

func (*StandardTenantService) GetTenantConfig ¶

func (ts *StandardTenantService) GetTenantConfig(tenantID TenantID, section string) (ConfigProvider, error)

GetTenantConfig retrieves tenant-specific configuration

func (*StandardTenantService) GetTenants ¶

func (ts *StandardTenantService) GetTenants() []TenantID

GetTenants returns all registered tenant IDs

func (*StandardTenantService) RegisterTenant ¶

func (ts *StandardTenantService) RegisterTenant(tenantID TenantID, configs map[string]ConfigProvider) error

RegisterTenant registers a new tenant with optional initial configs

func (*StandardTenantService) RegisterTenantAwareModule ¶

func (ts *StandardTenantService) RegisterTenantAwareModule(module TenantAwareModule) error

RegisterTenantAwareModule registers a module to receive tenant events

func (*StandardTenantService) RegisterTenantConfigSection ¶

func (ts *StandardTenantService) RegisterTenantConfigSection(
	tenantID TenantID,
	section string,
	provider ConfigProvider,
) error

RegisterTenantConfigSection registers a configuration section for a specific tenant

func (*StandardTenantService) RemoveTenant ¶

func (ts *StandardTenantService) RemoveTenant(tenantID TenantID) error

RemoveTenant removes a tenant and its configurations

type Startable ¶

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
}

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 StdApplication ¶

type StdApplication struct {
	// contains filtered or unexported fields
}

StdApplication represents the core StdApplication container

func (*StdApplication) ConfigProvider ¶

func (app *StdApplication) ConfigProvider() ConfigProvider

ConfigProvider retrieves the application config provider

func (*StdApplication) ConfigSections ¶

func (app *StdApplication) ConfigSections() map[string]ConfigProvider

ConfigSections retrieves all registered configuration sections

func (*StdApplication) GetConfigSection ¶

func (app *StdApplication) GetConfigSection(section string) (ConfigProvider, error)

GetConfigSection retrieves a configuration section

func (*StdApplication) GetService ¶

func (app *StdApplication) GetService(name string, target any) error

GetService retrieves a service with type assertion

func (*StdApplication) GetTenantConfig ¶

func (app *StdApplication) GetTenantConfig(tenantID TenantID, section string) (ConfigProvider, error)

GetTenantConfig retrieves configuration for a specific tenant and section

func (*StdApplication) GetTenantService ¶

func (app *StdApplication) GetTenantService() (TenantService, error)

GetTenantService returns the application's tenant service if available

func (*StdApplication) Init ¶

func (app *StdApplication) Init() error

Init initializes the application with the provided modules

func (*StdApplication) IsVerboseConfig ¶

func (app *StdApplication) IsVerboseConfig() bool

IsVerboseConfig returns whether verbose configuration debugging is enabled

func (*StdApplication) Logger ¶

func (app *StdApplication) Logger() Logger

Logger represents a logger

func (*StdApplication) RegisterConfigSection ¶

func (app *StdApplication) RegisterConfigSection(section string, cp ConfigProvider)

RegisterConfigSection registers a configuration section with the application

func (*StdApplication) RegisterModule ¶

func (app *StdApplication) RegisterModule(module Module)

RegisterModule adds a module to the application

func (*StdApplication) RegisterService ¶

func (app *StdApplication) RegisterService(name string, service any) error

RegisterService adds a service with type checking

func (*StdApplication) Run ¶

func (app *StdApplication) Run() error

Run starts the application and blocks until termination

func (*StdApplication) SetLogger ¶

func (app *StdApplication) SetLogger(logger Logger)

SetLogger sets the application's logger

func (*StdApplication) SetVerboseConfig ¶

func (app *StdApplication) SetVerboseConfig(enabled bool)

SetVerboseConfig enables or disables verbose configuration debugging

func (*StdApplication) Start ¶

func (app *StdApplication) Start() error

Start starts the application

func (*StdApplication) Stop ¶

func (app *StdApplication) Stop() error

Stop stops the application

func (*StdApplication) SvcRegistry ¶

func (app *StdApplication) SvcRegistry() ServiceRegistry

SvcRegistry retrieves the service svcRegistry

func (*StdApplication) WithTenant ¶

func (app *StdApplication) WithTenant(tenantID TenantID) (*TenantContext, error)

WithTenant creates a tenant context from the application context

type StdConfigProvider ¶

type StdConfigProvider struct {
	// contains filtered or unexported fields
}

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.

func NewStdConfigProvider ¶

func NewStdConfigProvider(cfg any) *StdConfigProvider

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 (*StdConfigProvider) GetConfig ¶

func (s *StdConfigProvider) GetConfig() any

GetConfig returns the configuration object. The returned value is the exact object that was passed to NewStdConfigProvider.

type Stoppable ¶

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
}

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 StructStateDiffer ¶

type StructStateDiffer struct {
	// contains filtered or unexported fields
}

StructStateDiffer captures before/after states to determine field changes

func NewStructStateDiffer ¶

func NewStructStateDiffer(tracker FieldTracker, logger Logger) *StructStateDiffer

NewStructStateDiffer creates a new struct state differ

func (*StructStateDiffer) CaptureAfterStateAndDiff ¶

func (d *StructStateDiffer) CaptureAfterStateAndDiff(structure interface{}, prefix string, feederType, sourceType string)

CaptureAfterStateAndDiff captures the state after feeder processing and computes diffs

func (*StructStateDiffer) CaptureBeforeState ¶

func (d *StructStateDiffer) CaptureBeforeState(structure interface{}, prefix string)

CaptureBeforeState captures the state before feeder processing

func (*StructStateDiffer) Reset ¶

func (d *StructStateDiffer) Reset()

Reset clears the captured states for reuse

type TenantApplication ¶

type TenantApplication interface {
	Application

	// 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.
	// 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.
	// 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)
}

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 TenantAwareConfig ¶

type TenantAwareConfig struct {
	// contains filtered or unexported fields
}

TenantAwareConfig provides configuration that's aware of tenant context

func NewTenantAwareConfig ¶

func NewTenantAwareConfig(
	defaultConfig ConfigProvider,
	tenantService TenantService,
	configSection string,
) *TenantAwareConfig

NewTenantAwareConfig creates a new tenant-aware configuration provider

func (*TenantAwareConfig) GetConfig ¶

func (tac *TenantAwareConfig) GetConfig() any

GetConfig retrieves the default configuration when no tenant is specified

func (*TenantAwareConfig) GetConfigWithContext ¶

func (tac *TenantAwareConfig) GetConfigWithContext(ctx context.Context) any

GetConfigWithContext retrieves tenant-specific configuration based on context

type TenantAwareModule ¶

type TenantAwareModule interface {
	Module

	// 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.
	// 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)
}

TenantAwareModule is an optional interface that modules can implement 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 TenantAwareRegistry ¶

type TenantAwareRegistry interface {
	// GetServiceForTenant returns a service instance for a specific tenant
	GetServiceForTenant(name string, tenantID TenantID, target any) error
}

TenantAwareRegistry provides common service discovery methods that are tenant-aware

type TenantConfigLoader ¶

type TenantConfigLoader interface {
	// LoadTenantConfigurations loads configurations for all tenants
	LoadTenantConfigurations(app Application, tenantService TenantService) error
}

TenantConfigLoader is an interface for loading tenant configurations

type TenantConfigParams ¶

type TenantConfigParams struct {
	// ConfigNameRegex is a regex pattern for the config file names (e.g. "^tenant[0-9]+\\.json$").
	ConfigNameRegex *regexp.Regexp
	// ConfigDir is the directory where tenant config files are located.
	ConfigDir string
	// ConfigFeeders are the feeders to use for loading tenant configs.
	ConfigFeeders []Feeder
}

TenantConfigParams defines parameters for loading tenant configurations

type TenantConfigProvider ¶

type TenantConfigProvider struct {
	// contains filtered or unexported fields
}

TenantConfigProvider manages configurations for multiple tenants

func NewTenantConfigProvider ¶

func NewTenantConfigProvider(defaultConfig ConfigProvider) *TenantConfigProvider

NewTenantConfigProvider creates a new tenant configuration provider

func (*TenantConfigProvider) GetConfig ¶

func (tcp *TenantConfigProvider) GetConfig() any

GetConfig returns the default configuration to satisfy ConfigProvider interface

func (*TenantConfigProvider) GetDefaultConfig ¶

func (tcp *TenantConfigProvider) GetDefaultConfig() ConfigProvider

GetDefaultConfig returns the default configuration (non-tenant specific)

func (*TenantConfigProvider) GetTenantConfig ¶

func (tcp *TenantConfigProvider) GetTenantConfig(tenantID TenantID, section string) (ConfigProvider, error)

GetTenantConfig retrieves a configuration for a specific tenant and section

func (*TenantConfigProvider) HasTenantConfig ¶

func (tcp *TenantConfigProvider) HasTenantConfig(tenantID TenantID, section string) bool

HasTenantConfig checks if a configuration exists for a specific tenant and section

func (*TenantConfigProvider) SetTenantConfig ¶

func (tcp *TenantConfigProvider) SetTenantConfig(tenantID TenantID, section string, provider ConfigProvider)

SetTenantConfig sets a configuration for a specific tenant and section

type TenantContext ¶

type TenantContext struct {
	context.Context
	// contains filtered or unexported fields
}

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.

func NewTenantContext ¶

func NewTenantContext(ctx context.Context, tenantID TenantID) *TenantContext

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 (*TenantContext) GetTenantID ¶

func (tc *TenantContext) GetTenantID() TenantID

GetTenantID returns the tenant ID from the context. This allows modules and services to determine which tenant the current operation is for.

type TenantID ¶

type TenantID string

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"

func GetTenantIDFromContext ¶

func GetTenantIDFromContext(ctx context.Context) (TenantID, bool)

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
}

type TenantService ¶

type TenantService interface {
	// 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 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 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.
	// 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
}

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 VerboseAwareFeeder ¶

type VerboseAwareFeeder interface {
	// SetVerboseDebug enables or disables verbose debug logging
	SetVerboseDebug(enabled bool, logger interface{ Debug(msg string, args ...any) })
}

VerboseAwareFeeder provides functionality for verbose debug logging during configuration feeding

type VerboseLogger ¶

type VerboseLogger interface {
	Debug(msg string, args ...any)
}

VerboseLogger provides a minimal logging interface to avoid circular dependencies

Directories ¶

Path Synopsis
Package feeders provides configuration feeders for reading data from various sources including environment variables, JSON, YAML, TOML files, and .env files.
Package feeders provides configuration feeders for reading data from various sources including environment variables, JSON, YAML, TOML files, and .env files.
modules
auth Module
cache Module
chimux Module
database Module
eventbus Module
httpclient Module
httpserver Module
jsonschema Module
letsencrypt Module
reverseproxy Module
scheduler Module

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL