use config::{Config, ConfigError, Environment, File}; use serde::{Deserialize, Serialize}; use std::path::PathBuf; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AppConfig { pub sonarr: SonarrConfig, pub ui: UiConfig, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SonarrConfig { pub url: String, pub api_key: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UiConfig { pub keybind_mode: KeybindMode, pub show_help: bool, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub enum KeybindMode { Normal, Vim, } impl Default for KeybindMode { fn default() -> Self { Self::Normal } } impl Default for UiConfig { fn default() -> Self { Self { keybind_mode: KeybindMode::default(), show_help: true, } } } impl Default for AppConfig { fn default() -> Self { Self { sonarr: SonarrConfig { url: "http://localhost:8989".to_string(), api_key: String::new(), }, ui: UiConfig::default(), } } } impl AppConfig { /// Load configuration from various sources with the following priority: /// 1. Command line arguments (highest priority) /// 2. Environment variables /// 3. Config file /// 4. Default values (lowest priority) pub fn load( config_path: Option, sonarr_url: Option, sonarr_api_key: Option, ) -> Result { let mut builder = Config::builder(); // Start with default config builder = builder.add_source(Config::try_from(&AppConfig::default())?); // Add config file if it exists if let Some(path) = config_path { if path.exists() { builder = builder.add_source(File::from(path)); } } else { // Try to load from default locations if let Some(config_dir) = dirs::config_dir() { let yarr_config = config_dir.join("yarr").join("config.toml"); if yarr_config.exists() { builder = builder.add_source(File::from(yarr_config)); } } // Also try current directory let local_config = std::env::current_dir() .map(|dir| dir.join("yarr.toml")) .unwrap_or_else(|_| PathBuf::from("yarr.toml")); if local_config.exists() { builder = builder.add_source(File::from(local_config)); } } // Add environment variables with YARR_ prefix builder = builder.add_source( Environment::with_prefix("YARR") .try_parsing(true) .separator("_"), ); // Override with command line arguments if provided let mut config = builder.build()?.try_deserialize::()?; if let Some(url) = sonarr_url { config.sonarr.url = url; } if let Some(api_key) = sonarr_api_key { config.sonarr.api_key = api_key; } Ok(config) } /// Create a sample config file pub fn create_sample_config(path: &PathBuf) -> Result<(), Box> { let sample_config = AppConfig { sonarr: SonarrConfig { url: "http://localhost:8989".to_string(), api_key: "your-api-key-here".to_string(), }, ui: UiConfig { keybind_mode: KeybindMode::Normal, show_help: true, }, }; let toml_content = toml::to_string_pretty(&sample_config)?; // Create directory if it doesn't exist if let Some(parent) = path.parent() { std::fs::create_dir_all(parent)?; } std::fs::write(path, toml_content)?; Ok(()) } /// Validate the configuration pub fn validate(&self) -> Result<(), String> { if self.sonarr.url.is_empty() { return Err("Sonarr URL is required".to_string()); } if self.sonarr.api_key.is_empty() { return Err("Sonarr API key is required".to_string()); } // Validate URL format if !self.sonarr.url.starts_with("http://") && !self.sonarr.url.starts_with("https://") { return Err("Sonarr URL must start with http:// or https://".to_string()); } Ok(()) } /// Get the default config file paths pub fn get_default_config_paths() -> Vec { let mut paths = Vec::new(); // Current directory if let Ok(current_dir) = std::env::current_dir() { paths.push(current_dir.join("yarr.toml")); } // User config directory if let Some(config_dir) = dirs::config_dir() { paths.push(config_dir.join("yarr").join("config.toml")); } paths } } #[cfg(test)] mod tests { use super::*; use std::env; #[test] fn test_default_config() { let config = AppConfig::default(); assert_eq!(config.sonarr.url, "http://localhost:8989"); assert_eq!(config.sonarr.api_key, ""); } #[test] fn test_config_validation() { let mut config = AppConfig::default(); // Should fail validation with empty API key assert!(config.validate().is_err()); // Should fail with invalid URL config.sonarr.api_key = "test-key".to_string(); config.sonarr.url = "invalid-url".to_string(); assert!(config.validate().is_err()); // Should pass with valid config config.sonarr.url = "https://example.com".to_string(); assert!(config.validate().is_ok()); } #[test] fn test_config_override() { // Test that CLI args override config let config = AppConfig::load( None, Some("https://cli-url.com".to_string()), Some("cli-api-key".to_string()), ) .unwrap(); assert_eq!(config.sonarr.url, "https://cli-url.com"); assert_eq!(config.sonarr.api_key, "cli-api-key"); } }