220 lines
6.1 KiB
Rust
220 lines
6.1 KiB
Rust
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<PathBuf>,
|
|
sonarr_url: Option<String>,
|
|
sonarr_api_key: Option<String>,
|
|
) -> Result<Self, ConfigError> {
|
|
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::<AppConfig>()?;
|
|
|
|
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<dyn std::error::Error>> {
|
|
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<PathBuf> {
|
|
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");
|
|
}
|
|
}
|