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 { Self::from_env_vars(&env::vars().collect::>()) } pub fn from_env_vars(env_vars: &[(String, String)]) -> Self { let get_env = |key: &str| { env_vars .iter() .find(|(k, _)| k == key) .map(|(_, v)| v.clone()) }; let database_url = Self::get_database_url_from_env_vars(env_vars); let openweathermap_api_key = get_env("OPENWEATHERMAP_API_KEY"); let admin_token = get_env("ADMIN_TOKEN"); let server_port = get_env("PORT") .unwrap_or_else(|| "4000".to_string()) .parse() .unwrap_or(4000); let server_host = get_env("HOST").unwrap_or_else(|| "0.0.0.0".to_string()); let log_level = get_env("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_from_env_vars(env_vars: &[(String, String)]) -> String { let get_env = |key: &str| { env_vars .iter() .find(|(k, _)| k == key) .map(|(_, v)| v.clone()) }; // First, check if DATABASE_URL is explicitly set if let Some(url) = get_env("DATABASE_URL") { return url; } // If not set, construct the path using XDG_DATA_HOME or fallback let data_dir = if let Some(xdg_data_home) = get_env("XDG_DATA_HOME") { PathBuf::from(xdg_data_home) } else { // Fallback to ~/.local/share let home = get_env("HOME").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::*; #[test] fn test_database_url_fallback() { let env_vars = vec![("HOME".to_string(), "/home/testuser".to_string())]; let config = Config::from_env_vars(&env_vars); let expected_path = "sqlite:/home/testuser/.local/share/silmataivas/silmataivas.db"; assert_eq!(config.database_url, expected_path); } #[test] fn test_database_url_xdg_data_home() { let env_vars = vec![("XDG_DATA_HOME".to_string(), "/custom/data/path".to_string())]; let config = Config::from_env_vars(&env_vars); let expected_path = "sqlite:/custom/data/path/silmataivas/silmataivas.db"; assert_eq!(config.database_url, expected_path); } #[test] fn test_database_url_explicit() { let env_vars = vec![( "DATABASE_URL".to_string(), "sqlite:/explicit/path.db".to_string(), )]; let config = Config::from_env_vars(&env_vars); assert_eq!(config.database_url, "sqlite:/explicit/path.db"); } #[test] fn test_server_config() { let env_vars = vec![ ("PORT".to_string(), "8080".to_string()), ("HOST".to_string(), "127.0.0.1".to_string()), ("LOG_LEVEL".to_string(), "debug".to_string()), ]; let config = Config::from_env_vars(&env_vars); assert_eq!(config.server_port, 8080); assert_eq!(config.server_host, "127.0.0.1"); assert_eq!(config.log_level, "debug"); } #[test] fn test_default_values() { let env_vars = vec![]; let config = Config::from_env_vars(&env_vars); assert_eq!(config.server_port, 4000); assert_eq!(config.server_host, "0.0.0.0"); assert_eq!(config.log_level, "info"); } #[test] fn test_home_fallback_to_tmp() { let env_vars = vec![]; let config = Config::from_env_vars(&env_vars); let expected_path = "sqlite:/tmp/.local/share/silmataivas/silmataivas.db"; assert_eq!(config.database_url, expected_path); } }