package config import ( "fmt" "os" "reflect" "strconv" "strings" "time" "gopkg.in/yaml.v3" ) // Config holds the application configuration. type Config struct { Logging LoggingConfig `yaml:"logging"` Server ServerConfig `yaml:"server"` } // LoggingConfig holds logging-specific configuration. type LoggingConfig struct { Level string `yaml:"level"` } // ServerConfig holds server-specific configuration. type ServerConfig struct { Host string `yaml:"host"` Port int `yaml:"port"` } // Load reads configuration from a YAML file and overrides it with environment variables. // The envPrefix is used to namespace environment variables (e.g. "APP"). // Env vars should be in the format: PREFIX_FIELD_NAME (uppercase). func Load(configPath string, envPrefix string) (*Config, error) { cfg := &Config{} // 1. Read YAML file file, err := os.Open(configPath) if err != nil { return nil, fmt.Errorf("failed to open config file: %w", err) } defer file.Close() decoder := yaml.NewDecoder(file) if err := decoder.Decode(cfg); err != nil { return nil, fmt.Errorf("failed to decode config file: %w", err) } // 2. Override from Environment Variables if err := overrideFromEnv(cfg, envPrefix); err != nil { return nil, fmt.Errorf("failed to process environment variables: %w", err) } return cfg, nil } // overrideFromEnv uses reflection to traverse the struct and update fields from env vars. func overrideFromEnv(cfg interface{}, prefix string) error { v := reflect.ValueOf(cfg) if v.Kind() == reflect.Ptr { v = v.Elem() } return setFromEnv(v, prefix) } func setFromEnv(v reflect.Value, prefix string) error { t := v.Type() for i := 0; i < v.NumField(); i++ { field := t.Field(i) fieldVal := v.Field(i) // Determine the key for this field // We use the yaml tag if available to guess the name, or just the field name keyName := field.Name if tag := field.Tag.Get("yaml"); tag != "" { parts := strings.Split(tag, ",") if parts[0] != "" { keyName = parts[0] } } // Sanitize key name for Env (uppercase, replace non-alphanum with _) envKey := strings.ToUpper(fmt.Sprintf("%s_%s", prefix, keyName)) // Clean up common separators like hyphens in yaml keys envKey = strings.ReplaceAll(envKey, "-", "_") if fieldVal.Kind() == reflect.Struct { // Recursive call for nested structs if err := setFromEnv(fieldVal, envKey); err != nil { return err } continue } if !fieldVal.CanSet() { continue } val, present := os.LookupEnv(envKey) if present { if err := setField(fieldVal, val); err != nil { return fmt.Errorf("failed to set field %s from env %s: %w", field.Name, envKey, err) } } } return nil } func setField(field reflect.Value, value string) error { switch field.Kind() { case reflect.String: field.SetString(value) case reflect.Bool: b, err := strconv.ParseBool(value) if err != nil { return err } field.SetBool(b) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: // Handle Duration specially if it's an int64 underlying type? // For now, let's assume standard ints. // Real generic implementations often handle time.Duration if field.Type() == reflect.TypeOf(time.Duration(0)) { d, err := time.ParseDuration(value) if err != nil { return err } field.SetInt(int64(d)) } else { i, err := strconv.ParseInt(value, 10, 64) if err != nil { return err } field.SetInt(i) } case reflect.Float32, reflect.Float64: f, err := strconv.ParseFloat(value, 64) if err != nil { return err } field.SetFloat(f) default: return fmt.Errorf("unsupported type: %s", field.Kind()) } return nil }