syntaxis/shared/rust/config_finder.rs
Jesús Pérez 9cef9b8d57 refactor: consolidate configuration directories
Merge _configs/ into config/ for single configuration directory.
Update all path references.

Changes:
- Move _configs/* to config/
- Update .gitignore for new patterns
- No code references to _configs/ found

Impact: -1 root directory (layout_conventions.md compliance)
2025-12-26 18:36:23 +00:00

293 lines
9.2 KiB
Rust

//! 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<PathBuf> {
// 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<P: Into<PathBuf>>(
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<PathBuf> {
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<PathBuf> {
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<PathBuf> {
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"
);
}
}