//! Configuration file support for TLI //! //! This module provides configuration file management for TLI client settings. //! Configuration is loaded from `~/.foxhunt/config.toml` and can be overridden //! by CLI arguments (following standard CLI precedence). //! //! ## Precedence //! CLI arguments > Config file > Defaults //! //! ## Example Config File //! ```toml //! # ~/.foxhunt/config.toml //! api_gateway_url = "http://localhost:50051" //! log_level = "info" //! token_storage = "keyring" //! ``` use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; use std::path::PathBuf; /// TLI configuration structure /// /// Contains all configurable settings for the TLI client application. /// Settings can be loaded from `~/.foxhunt/config.toml` and overridden /// by CLI arguments or environment variables. #[derive(Debug, Serialize, Deserialize)] pub struct TliConfig { /// API Gateway URL (default: ) #[serde(default = "default_api_gateway_url")] pub api_gateway_url: String, /// Log level (default: info) #[serde(default = "default_log_level")] pub log_level: String, /// Token storage backend (default: keyring) #[serde(default = "default_token_storage")] pub token_storage: String, } fn default_api_gateway_url() -> String { "http://localhost:50051".to_owned() } fn default_log_level() -> String { "info".to_owned() } fn default_token_storage() -> String { "keyring".to_owned() } impl Default for TliConfig { fn default() -> Self { Self { api_gateway_url: default_api_gateway_url(), log_level: default_log_level(), token_storage: default_token_storage(), } } } impl TliConfig { /// Load config from ~/.foxhunt/config.toml /// /// Returns default configuration if file doesn't exist. /// Returns error if file exists but is malformed. /// /// # Examples /// ```no_run /// use tli::config::TliConfig; /// /// let config = TliConfig::load().unwrap(); /// println!("API Gateway: {}", config.api_gateway_url); /// ``` pub fn load() -> Result { let config_path = Self::config_path()?; if !config_path.exists() { // Return defaults if no config file return Ok(Self::default()); } let content = std::fs::read_to_string(&config_path).context("Failed to read config file")?; let config: Self = toml::from_str(&content).context("Failed to parse config file")?; Ok(config) } /// Save config to ~/.foxhunt/config.toml /// /// Creates parent directory if it doesn't exist. /// /// # Examples /// ```no_run /// use tli::config::TliConfig; /// /// let config = TliConfig { /// api_gateway_url: "http://localhost:50051".to_string(), /// log_level: "debug".to_string(), /// token_storage: "keyring".to_string(), /// }; /// config.save().unwrap(); /// ``` pub fn save(&self) -> Result<()> { let config_path = Self::config_path()?; // Create parent directory if needed if let Some(parent) = config_path.parent() { std::fs::create_dir_all(parent)?; } let content = toml::to_string_pretty(self).context("Failed to serialize config")?; std::fs::write(&config_path, content).context("Failed to write config file")?; Ok(()) } /// Get config file path: ~/.foxhunt/config.toml /// /// # Errors /// Returns error if home directory cannot be determined fn config_path() -> Result { let home = dirs::home_dir().ok_or_else(|| anyhow::anyhow!("Cannot find home directory"))?; Ok(home.join(".foxhunt").join("config.toml")) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_default_config() { // Test that Default trait provides correct values let config = TliConfig::default(); assert_eq!(config.api_gateway_url, "http://localhost:50051"); assert_eq!(config.log_level, "info"); assert_eq!(config.token_storage, "keyring"); } #[test] fn test_config_serialization() { let config = TliConfig { api_gateway_url: "http://example.com:50051".to_owned(), log_level: "debug".to_owned(), token_storage: "file".to_owned(), }; let toml_str = toml::to_string(&config).unwrap(); let parsed: TliConfig = toml::from_str(&toml_str).unwrap(); assert_eq!(config.api_gateway_url, parsed.api_gateway_url); assert_eq!(config.log_level, parsed.log_level); assert_eq!(config.token_storage, parsed.token_storage); } #[test] fn test_load_nonexistent_config() { // This should return default config without error when file doesn't exist let result = TliConfig::load(); assert!(result.is_ok()); let config = result.unwrap(); // When config file doesn't exist, load() returns Self::default() // which should have all default values assert_eq!(config.api_gateway_url, "http://localhost:50051"); assert_eq!(config.log_level, "info"); assert_eq!(config.token_storage, "keyring"); } #[test] fn test_serde_defaults() { // Test that empty TOML string deserializes to defaults let empty_toml = ""; let config: TliConfig = toml::from_str(empty_toml).unwrap(); assert_eq!(config.api_gateway_url, "http://localhost:50051"); assert_eq!(config.log_level, "info"); assert_eq!(config.token_storage, "keyring"); } }