//! Configuration file finder with proper precedence hierarchy //! //! Provides utilities to find configuration files following this strict precedence: //! //! 1. **Explicit config path** - Via `--config` CLI argument or API parameter //! 2. **Global config** - `~/.syntaxis/config.toml` or `~/.vapora/config.toml` (vapora takes precedence) //! 3. **Local project config** - `./.vapora/config.toml` (always takes precedence over `.syntaxis/`) //! or `./.syntaxis/config.toml` //! //! # Configuration Discovery //! //! ```ignore //! use config_finder::find_config_path; //! //! // Find config with proper precedence //! let config_path = find_config_path("config.toml")?; //! println!("Using config: {}", config_path.display()); //! ``` use std::path::{Path, PathBuf}; /// Finds configuration file with proper precedence hierarchy /// /// # Search Order (in priority) /// /// 1. Explicit config path (passed via argument) /// 2. Global vapora config (`~/.vapora/{filename}`) /// 3. Global syntaxis config (`~/.syntaxis/{filename}`) /// 4. Local vapora config (`./.vapora/{filename}` - always takes precedence) /// 5. Local syntaxis config (`./.syntaxis/{filename}`) /// /// # Arguments /// /// * `filename` - The config filename to search for (e.g., "config.toml") /// * `explicit_config_path` - Optional explicit config path (highest priority) /// /// # Returns /// /// `Some(PathBuf)` pointing to the first existing config, or `None` if not found. pub fn find_config_path(filename: &str, explicit_config_path: Option<&Path>) -> Option { // 1. Check explicit config path (highest priority) if let Some(explicit_path) = explicit_config_path { if explicit_path.exists() { return Some(explicit_path.to_path_buf()); } } // 2. Check global vapora config (takes precedence over syntaxis) if let Some(home) = dirs::home_dir() { let global_vapora = home.join(".vapora").join(filename); if global_vapora.exists() { return Some(global_vapora); } } // 3. Check global syntaxis config if let Some(home) = dirs::home_dir() { let global_syntaxis = home.join(".syntaxis").join(filename); if global_syntaxis.exists() { return Some(global_syntaxis); } } // 4. Check local vapora config (takes precedence over syntaxis) let local_vapora = PathBuf::from(".vapora").join(filename); if local_vapora.exists() { return Some(local_vapora); } // 5. Check local syntaxis config let local_syntaxis = PathBuf::from(".syntaxis").join(filename); if local_syntaxis.exists() { return Some(local_syntaxis); } None } /// Finds configuration file with default fallback /// /// Like `find_config_path`, but returns the provided default if no config exists. /// /// # Arguments /// /// * `filename` - The config filename to search for /// * `explicit_config_path` - Optional explicit config path /// * `default` - Default path to return if not found /// /// # Returns /// /// First existing config path, or `default` if none found. pub fn find_config_path_or>( filename: &str, explicit_config_path: Option<&Path>, default: P, ) -> PathBuf { find_config_path(filename, explicit_config_path).unwrap_or_else(|| default.into()) } /// Gets the standard global syntaxis config directory /// /// # Returns /// /// `~/.syntaxis/` directory path pub fn get_global_syntaxis_config_dir() -> Option { dirs::home_dir().map(|home| home.join(".syntaxis")) } /// Gets the standard global vapora config directory /// /// # Returns /// /// `~/.vapora/` directory path pub fn get_global_vapora_config_dir() -> Option { dirs::home_dir().map(|home| home.join(".vapora")) } /// Gets the local syntaxis config directory /// /// # Returns /// /// `./.syntaxis/` directory path pub fn get_local_syntaxis_config_dir() -> PathBuf { PathBuf::from(".syntaxis") } /// Gets the local vapora config directory /// /// # Returns /// /// `./.vapora/` directory path pub fn get_local_vapora_config_dir() -> PathBuf { PathBuf::from(".vapora") } /// Finds config file with warnings for conflicting configurations /// /// Like `find_config_path`, but emits a warning if multiple config locations are found. /// Uses .vapora as precedence over .syntaxis at both global and local levels. /// /// # Arguments /// /// * `filename` - The config filename to search for /// * `explicit_config_path` - Optional explicit config path /// /// # Returns /// /// First existing config path using precedence order, or `None` if not found. pub fn find_config_path_warn_conflicts( filename: &str, explicit_config_path: Option<&Path>, ) -> Option { if let Some(explicit_path) = explicit_config_path { if explicit_path.exists() { return Some(explicit_path.to_path_buf()); } } let mut found_configs = vec![]; // Global vapora (highest priority among globals) if let Some(home) = dirs::home_dir() { let global_vapora = home.join(".vapora").join(filename); if global_vapora.exists() { found_configs.push(("global .vapora".to_string(), global_vapora.clone())); } } // Global syntaxis if let Some(home) = dirs::home_dir() { let global_syntaxis = home.join(".syntaxis").join(filename); if global_syntaxis.exists() { found_configs.push(("global .syntaxis".to_string(), global_syntaxis.clone())); } } // Local vapora (takes precedence over local syntaxis) let local_vapora = PathBuf::from(".vapora").join(filename); if local_vapora.exists() { found_configs.push(("local .vapora".to_string(), local_vapora.clone())); } // Local syntaxis let local_syntaxis = PathBuf::from(".syntaxis").join(filename); if local_syntaxis.exists() { found_configs.push(("local .syntaxis".to_string(), local_syntaxis.clone())); } match found_configs.len() { 0 => None, 1 => Some(found_configs.into_iter().next().unwrap().1), _ => { // Multiple configs found - warn and use the highest priority one eprintln!( "⚠️ WARNING: Configuration {} found in multiple locations:", filename ); for (location, _) in &found_configs { eprintln!(" • {}", location); } eprintln!(" Using: {} (highest precedence)", found_configs[0].0); eprintln!(" Please consolidate to a single location per precedence order"); Some(found_configs.into_iter().next().unwrap().1) } } } #[cfg(test)] mod tests { use super::*; use std::fs; use tempfile::TempDir; #[test] fn test_find_config_explicit_path() { let temp = TempDir::new().unwrap(); let temp_path = temp.path().to_path_buf(); let explicit_file = temp_path.join("explicit.toml"); fs::write(&explicit_file, "explicit").unwrap(); let result = find_config_path("config.toml", Some(&explicit_file)); assert_eq!( result, Some(explicit_file), "Should prefer explicit config path" ); } #[test] fn test_find_config_local_vapora_precedence() { let temp = TempDir::new().unwrap(); let temp_path = temp.path().to_path_buf(); // Create both local vapora and syntaxis let local_vapora = temp_path.join(".vapora"); let local_syntaxis = temp_path.join(".syntaxis"); fs::create_dir(&local_vapora).unwrap(); fs::create_dir(&local_syntaxis).unwrap(); fs::write(local_vapora.join("test.toml"), "vapora").unwrap(); fs::write(local_syntaxis.join("test.toml"), "syntaxis").unwrap(); let original = std::env::current_dir().unwrap(); std::env::set_current_dir(&temp_path).unwrap(); let result = find_config_path("test.toml", None); std::env::set_current_dir(&original).unwrap(); assert_eq!( result, Some(PathBuf::from(".vapora/test.toml")), "Should prefer .vapora/ over .syntaxis/" ); } #[test] fn test_find_config_not_found() { let temp = TempDir::new().unwrap(); let original = std::env::current_dir().unwrap(); std::env::set_current_dir(temp.path()).unwrap(); let result = find_config_path("nonexistent.toml", None); assert_eq!(result, None, "Should return None when no config exists"); std::env::set_current_dir(original).unwrap(); } #[test] fn test_find_config_local_syntaxis() { let temp = TempDir::new().unwrap(); let temp_path = temp.path().to_path_buf(); // Create only local syntaxis let local_syntaxis = temp_path.join(".syntaxis"); fs::create_dir(&local_syntaxis).unwrap(); fs::write(local_syntaxis.join("test.toml"), "syntaxis").unwrap(); let original = std::env::current_dir().unwrap(); std::env::set_current_dir(&temp_path).unwrap(); let result = find_config_path("test.toml", None); std::env::set_current_dir(&original).unwrap(); assert_eq!( result, Some(PathBuf::from(".syntaxis/test.toml")), "Should find .syntaxis/ when .vapora/ doesn't exist" ); } }