package ezconf import ( "os" "github.com/pkg/errors" ) // EnvVar represents a single environment variable with its metadata type EnvVar struct { Name string // The environment variable name (e.g., "LOG_LEVEL") Description string // Description of what this variable does Required bool // Whether this variable is required Default string // Default value if not set CurrentValue string // Current value from environment (empty if not set) Group string // Group name for organizing variables (e.g., "Database", "Logging") } // configStruct holds a config struct pointer and its group name for parsing type configStruct struct { configPtr any groupName string } // ConfigLoader manages configuration loading from multiple sources type ConfigLoader struct { configFuncs map[string]ConfigFunc // Map of config names to ConfigFromEnv functions configStructs []configStruct // Config struct pointers for tag parsing extraEnvVars []EnvVar // Additional environment variables to track envVars []EnvVar // All extracted environment variables configs map[string]any // Loaded configurations } // ConfigFunc is a function that loads configuration from environment variables type ConfigFunc func() (any, error) // New creates a new ConfigLoader func New() *ConfigLoader { return &ConfigLoader{ configFuncs: make(map[string]ConfigFunc), configStructs: make([]configStruct, 0), extraEnvVars: make([]EnvVar, 0), envVars: make([]EnvVar, 0), configs: make(map[string]any), } } // AddConfigFunc adds a ConfigFromEnv function to be called during loading. // The name parameter is used as a key to retrieve the loaded config later. func (cl *ConfigLoader) AddConfigFunc(name string, fn ConfigFunc) error { if fn == nil { return errors.New("config function cannot be nil") } if name == "" { return errors.New("config name cannot be empty") } cl.configFuncs[name] = fn return nil } // AddConfigStruct adds a config struct pointer for parsing ezconf tags. // The configPtr must be a pointer to a struct with ezconf struct tags. // The groupName is used for organizing environment variables in output. func (cl *ConfigLoader) AddConfigStruct(configPtr any, groupName string) error { if configPtr == nil { return errors.New("config pointer cannot be nil") } if groupName == "" { groupName = "Other" } cl.configStructs = append(cl.configStructs, configStruct{ configPtr: configPtr, groupName: groupName, }) return nil } // AddEnvVar adds an additional environment variable to track func (cl *ConfigLoader) AddEnvVar(envVar EnvVar) { cl.extraEnvVars = append(cl.extraEnvVars, envVar) } // ParseEnvVars extracts environment variables from config struct tags and extra vars. // This can be called without having actual environment variables set. func (cl *ConfigLoader) ParseEnvVars() error { // Clear existing env vars to prevent duplicates cl.envVars = make([]EnvVar, 0) // Parse config structs for ezconf tags for _, cs := range cl.configStructs { envVars, err := ParseConfigStruct(cs.configPtr) if err != nil { return errors.Wrap(err, "failed to parse config struct") } // Set group name for these variables for i := range envVars { envVars[i].Group = cs.groupName } cl.envVars = append(cl.envVars, envVars...) } // Add extra env vars cl.envVars = append(cl.envVars, cl.extraEnvVars...) // Populate current values from environment for i := range cl.envVars { cl.envVars[i].CurrentValue = os.Getenv(cl.envVars[i].Name) } return nil } // LoadConfigs executes the config functions to load actual configurations. // This should be called after environment variables are properly set. func (cl *ConfigLoader) LoadConfigs() error { // Load configurations for name, fn := range cl.configFuncs { cfg, err := fn() if err != nil { return errors.Wrapf(err, "failed to load config: %s", name) } cl.configs[name] = cfg } return nil } // Load loads all configurations and extracts environment variables func (cl *ConfigLoader) Load() error { if err := cl.ParseEnvVars(); err != nil { return err } return cl.LoadConfigs() } // GetConfig returns a loaded configuration by name func (cl *ConfigLoader) GetConfig(name string) (any, bool) { cfg, ok := cl.configs[name] return cfg, ok } // GetAllConfigs returns all loaded configurations func (cl *ConfigLoader) GetAllConfigs() map[string]any { return cl.configs } // GetEnvVars returns all extracted environment variables func (cl *ConfigLoader) GetEnvVars() []EnvVar { return cl.envVars }