package config import ( "fmt" "os" "path/filepath" "strings" "time" "github.com/spf13/viper" ) // Config holds all configuration for the Zwift monitor service type Config struct { // Authentication Username string `mapstructure:"username"` Password string `mapstructure:"password"` // Storage OutputDir string `mapstructure:"output_dir"` StateFile string `mapstructure:"state_file"` TokenCachePath string `mapstructure:"token_cache"` // Monitoring PollInterval time.Duration `mapstructure:"poll_interval"` // API Configuration RateLimit int `mapstructure:"rate_limit"` MaxRetries int `mapstructure:"max_retries"` // Logging LogLevel string `mapstructure:"log_level"` LogFile string `mapstructure:"log_file"` } // Load loads configuration from environment variables and optional config file func Load() (*Config, error) { v := viper.New() // Set defaults v.SetDefault("output_dir", "/data/activities") v.SetDefault("state_file", "/data/zwift-state.json") v.SetDefault("token_cache", "/data/.zwift-token.json") v.SetDefault("poll_interval", "5m") v.SetDefault("rate_limit", 5) v.SetDefault("max_retries", 3) v.SetDefault("log_level", "info") v.SetDefault("log_file", "") // Bind environment variables v.SetEnvPrefix("ZWIFT") v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) v.AutomaticEnv() // Try to load config file if it exists configPaths := []string{ "config.yaml", "/app/config.yaml", filepath.Join(os.Getenv("HOME"), ".zwift-monitor.yaml"), } for _, path := range configPaths { if _, err := os.Stat(path); err == nil { v.SetConfigFile(path) if err := v.ReadInConfig(); err != nil { return nil, fmt.Errorf("failed to read config file %s: %w", path, err) } break } } var cfg Config if err := v.Unmarshal(&cfg); err != nil { return nil, fmt.Errorf("failed to unmarshal config: %w", err) } // Validate required fields if err := cfg.Validate(); err != nil { return nil, err } // Expand paths (handle ~) cfg.OutputDir = expandPath(cfg.OutputDir) cfg.StateFile = expandPath(cfg.StateFile) cfg.TokenCachePath = expandPath(cfg.TokenCachePath) if cfg.LogFile != "" { cfg.LogFile = expandPath(cfg.LogFile) } return &cfg, nil } // Validate checks if required configuration is present func (c *Config) Validate() error { if c.Username == "" { return fmt.Errorf("username is required (set ZWIFT_USERNAME environment variable)") } if c.Password == "" { return fmt.Errorf("password is required (set ZWIFT_PASSWORD environment variable)") } if c.PollInterval < time.Minute { return fmt.Errorf("poll_interval must be at least 1 minute") } if c.RateLimit < 1 { return fmt.Errorf("rate_limit must be at least 1") } if c.MaxRetries < 1 { return fmt.Errorf("max_retries must be at least 1") } return nil } // expandPath expands ~ to home directory func expandPath(path string) string { if strings.HasPrefix(path, "~") { home, err := os.UserHomeDir() if err != nil { return path } return filepath.Join(home, path[1:]) } return path }