use std::env; use std::path::PathBuf; #[derive(Debug, Clone)] pub struct Config { pub database_url: String, #[allow(dead_code)] pub openweathermap_api_key: Option, pub admin_token: Option, pub server_port: u16, pub server_host: String, pub log_level: String, } impl Config { pub fn from_env() -> Self { let database_url = Self::get_database_url(); let openweathermap_api_key = env::var("OPENWEATHERMAP_API_KEY").ok(); let admin_token = env::var("ADMIN_TOKEN").ok(); let server_port = env::var("PORT") .unwrap_or_else(|_| "4000".to_string()) .parse() .unwrap_or(4000); let server_host = env::var("HOST").unwrap_or_else(|_| "0.0.0.0".to_string()); let log_level = env::var("LOG_LEVEL").unwrap_or_else(|_| "info".to_string()); Self { database_url, openweathermap_api_key, admin_token, server_port, server_host, log_level, } } fn get_database_url() -> String { // First, check if DATABASE_URL is explicitly set if let Ok(url) = env::var("DATABASE_URL") { return url; } // If not set, construct the path using XDG_DATA_HOME or fallback let data_dir = if let Ok(xdg_data_home) = env::var("XDG_DATA_HOME") { PathBuf::from(xdg_data_home) } else { // Fallback to ~/.local/share let home = env::var("HOME") .unwrap_or_else(|_| env::var("USERPROFILE").unwrap_or_else(|_| "/tmp".to_string())); PathBuf::from(home).join(".local").join("share") }; let db_path = data_dir.join("silmataivas").join("silmataivas.db"); // Ensure the directory exists if let Some(parent) = db_path.parent() { let _ = std::fs::create_dir_all(parent); } // Use the correct SQLx format: sqlite:path format!("sqlite:{}", db_path.display()) } #[allow(dead_code)] pub fn validate(&self) -> Result<(), String> { if self.openweathermap_api_key.is_none() { return Err("OPENWEATHERMAP_API_KEY environment variable is required".to_string()); } Ok(()) } #[allow(dead_code)] pub fn validate_optional(&self) -> Result<(), String> { // For optional validation (e.g., during development) Ok(()) } } #[cfg(test)] mod tests { use super::*; use std::env; use std::sync::OnceLock; use std::sync::{Mutex, Once}; static INIT: Once = Once::new(); static TEST_MUTEX: OnceLock> = OnceLock::new(); fn setup() { INIT.call_once(|| { // Clear all environment variables that might interfere with tests unsafe { env::remove_var("DATABASE_URL"); env::remove_var("XDG_DATA_HOME"); env::remove_var("PORT"); env::remove_var("HOST"); env::remove_var("LOG_LEVEL"); } }); // Get the test mutex to ensure sequential execution let _guard = TEST_MUTEX.get_or_init(|| Mutex::new(())).lock().unwrap(); // Clear environment variables before each test unsafe { env::remove_var("DATABASE_URL"); env::remove_var("XDG_DATA_HOME"); env::remove_var("PORT"); env::remove_var("HOST"); env::remove_var("LOG_LEVEL"); } } #[test] fn test_database_url_fallback() { setup(); let home = env::var("HOME").unwrap_or_else(|_| "/tmp".to_string()); let expected_path = format!("sqlite:{home}/.local/share/silmataivas/silmataivas.db"); let config = Config::from_env(); assert_eq!(config.database_url, expected_path); } #[test] fn test_database_url_xdg_data_home() { setup(); unsafe { env::set_var("XDG_DATA_HOME", "/custom/data/path"); } let expected_path = "sqlite:/custom/data/path/silmataivas/silmataivas.db"; let config = Config::from_env(); assert_eq!(config.database_url, expected_path); // Clean up after this test unsafe { env::remove_var("XDG_DATA_HOME"); } } #[test] fn test_database_url_explicit() { setup(); unsafe { env::set_var("DATABASE_URL", "sqlite:/explicit/path.db"); } let config = Config::from_env(); assert_eq!(config.database_url, "sqlite:/explicit/path.db"); // Clean up after this test unsafe { env::remove_var("DATABASE_URL"); } } #[test] fn test_server_config() { setup(); unsafe { env::set_var("PORT", "8080"); env::set_var("HOST", "127.0.0.1"); env::set_var("LOG_LEVEL", "debug"); } let config = Config::from_env(); assert_eq!(config.server_port, 8080); assert_eq!(config.server_host, "127.0.0.1"); assert_eq!(config.log_level, "debug"); // Clean up after this test unsafe { env::remove_var("PORT"); env::remove_var("HOST"); env::remove_var("LOG_LEVEL"); } } }