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)
293 lines
9.2 KiB
Rust
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"
|
|
);
|
|
}
|
|
}
|