package config import ( "encoding/json" "fmt" "os" "path/filepath" "strconv" "time" ) // Config holds process configuration loaded from environment variables. type Config struct { HTTPAddr string ReadTimeout time.Duration WriteTimeout time.Duration ShutdownGrace time.Duration DatabaseDSN string MigrationsDir string } func Load() (Config, error) { readTimeout, err := envDuration("READ_TIMEOUT", 120*time.Second) if err != nil { return Config{}, err } writeTimeout, err := envDuration("WRITE_TIMEOUT", 300*time.Second) if err != nil { return Config{}, err } shutdownGrace, err := envDuration("SHUTDOWN_GRACE", 10*time.Second) if err != nil { return Config{}, err } cfg := Config{ HTTPAddr: envOrDefault("HTTP_ADDR", ":9999"), ReadTimeout: readTimeout, WriteTimeout: writeTimeout, ShutdownGrace: shutdownGrace, DatabaseDSN: os.Getenv("DATABASE_DSN"), MigrationsDir: envOrDefault("MIGRATIONS_DIR", "migrations"), } fileCfg, err := loadFileConfig() if err != nil { return Config{}, err } applyFileConfig(&cfg, fileCfg) return cfg, nil } func envOrDefault(key, fallback string) string { if value := os.Getenv(key); value != "" { return value } return fallback } func envDuration(key string, fallback time.Duration) (time.Duration, error) { value := os.Getenv(key) if value == "" { return fallback, nil } seconds, err := strconv.Atoi(value) if err != nil { return 0, fmt.Errorf("%s must be an integer number of seconds: %w", key, err) } if seconds <= 0 { return 0, fmt.Errorf("%s must be > 0", key) } return time.Duration(seconds) * time.Second, nil } type fileConfig struct { HTTPAddr *string `json:"http_addr"` ReadTimeoutSeconds *int `json:"read_timeout_seconds"` WriteTimeoutSeconds *int `json:"write_timeout_seconds"` ShutdownGraceSeconds *int `json:"shutdown_grace_seconds"` DatabaseDSN *string `json:"database_dsn"` Database *dbFileConfig `json:"database"` MigrationsDir *string `json:"migrations_dir"` } type dbFileConfig struct { User *string `json:"user"` Password *string `json:"password"` Host *string `json:"host"` Port *int `json:"port"` Name *string `json:"name"` Params *string `json:"params"` } func loadFileConfig() (fileConfig, error) { path := os.Getenv("CONFIG_FILE") if path == "" { path = "config.json" if _, err := os.Stat(path); err != nil { if os.IsNotExist(err) { return fileConfig{}, nil } return fileConfig{}, err } } else { if _, err := os.Stat(path); err != nil { return fileConfig{}, fmt.Errorf("config file %s: %w", path, err) } } raw, err := os.ReadFile(filepath.Clean(path)) if err != nil { return fileConfig{}, err } var cfg fileConfig if err := json.Unmarshal(raw, &cfg); err != nil { return fileConfig{}, fmt.Errorf("parse config file %s: %w", path, err) } return cfg, nil } func applyFileConfig(cfg *Config, fileCfg fileConfig) { if fileCfg.HTTPAddr != nil && os.Getenv("HTTP_ADDR") == "" { cfg.HTTPAddr = *fileCfg.HTTPAddr } if fileCfg.ReadTimeoutSeconds != nil && os.Getenv("READ_TIMEOUT") == "" { if *fileCfg.ReadTimeoutSeconds > 0 { cfg.ReadTimeout = time.Duration(*fileCfg.ReadTimeoutSeconds) * time.Second } } if fileCfg.WriteTimeoutSeconds != nil && os.Getenv("WRITE_TIMEOUT") == "" { if *fileCfg.WriteTimeoutSeconds > 0 { cfg.WriteTimeout = time.Duration(*fileCfg.WriteTimeoutSeconds) * time.Second } } if fileCfg.ShutdownGraceSeconds != nil && os.Getenv("SHUTDOWN_GRACE") == "" { if *fileCfg.ShutdownGraceSeconds > 0 { cfg.ShutdownGrace = time.Duration(*fileCfg.ShutdownGraceSeconds) * time.Second } } if fileCfg.DatabaseDSN != nil && os.Getenv("DATABASE_DSN") == "" { cfg.DatabaseDSN = *fileCfg.DatabaseDSN } if cfg.DatabaseDSN == "" && os.Getenv("DATABASE_DSN") == "" { if dsn := buildDSN(fileCfg.Database); dsn != "" { cfg.DatabaseDSN = dsn } } if fileCfg.MigrationsDir != nil && os.Getenv("MIGRATIONS_DIR") == "" { cfg.MigrationsDir = *fileCfg.MigrationsDir } } func buildDSN(db *dbFileConfig) string { if db == nil || db.Name == nil { return "" } user := valueOrDefault(db.User, "reanimator") password := valueOrDefault(db.Password, "reanimator") host := valueOrDefault(db.Host, "127.0.0.1") port := intValueOrDefault(db.Port, 3306) params := valueOrDefault(db.Params, "parseTime=true") return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?%s", user, password, host, port, *db.Name, params) } func valueOrDefault(value *string, fallback string) string { if value == nil || *value == "" { return fallback } return *value } func intValueOrDefault(value *int, fallback int) int { if value == nil || *value <= 0 { return fallback } return *value }