use crate::error::{ConfigError, Result};
use std::env;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum ConfigLevel {
Project,
Workspace,
Global,
}
#[derive(Debug, Clone)]
pub struct ConfigLocation {
pub level: ConfigLevel,
pub path: PathBuf,
pub exists: bool,
}
pub struct ConfigHierarchy {
locations: Vec<ConfigLocation>,
}
impl ConfigHierarchy {
pub fn discover(start_path: &Path) -> Result<Self> {
let mut locations = Vec::new();
if let Some(project_root) = find_project_root(start_path) {
let config_path = project_root.join(".raz");
locations.push(ConfigLocation {
level: ConfigLevel::Project,
path: config_path.clone(),
exists: config_path.exists(),
});
}
if let Some(workspace_root) = find_workspace_root(start_path) {
let config_path = workspace_root.join(".raz");
if !locations.iter().any(|loc| loc.path == config_path) {
locations.push(ConfigLocation {
level: ConfigLevel::Workspace,
path: config_path.clone(),
exists: config_path.exists(),
});
}
}
if let Some(global_path) = get_global_config_path() {
locations.push(ConfigLocation {
level: ConfigLevel::Global,
path: global_path.clone(),
exists: global_path.exists(),
});
}
Ok(Self { locations })
}
pub fn locations(&self) -> &[ConfigLocation] {
&self.locations
}
pub fn primary_location(&self) -> Option<&ConfigLocation> {
self.locations.iter().find(|loc| loc.exists)
}
pub fn get_or_create_location(
&self,
prefer_level: Option<ConfigLevel>,
) -> Result<&ConfigLocation> {
if let Some(level) = prefer_level {
if let Some(loc) = self.locations.iter().find(|l| l.level == level) {
return Ok(loc);
}
}
self.locations.first().ok_or_else(|| {
ConfigError::ValidationError("No configuration location available".to_string())
})
}
pub fn init_config(&self, level: ConfigLevel) -> Result<PathBuf> {
let location = self
.locations
.iter()
.find(|loc| loc.level == level)
.ok_or_else(|| {
ConfigError::ValidationError(format!("No {level:?} level configuration path found"))
})?;
std::fs::create_dir_all(&location.path)?;
Ok(location.path.clone())
}
pub fn get_override_storage_path(&self) -> Result<PathBuf> {
let location = self.get_or_create_location(Some(ConfigLevel::Project))?;
Ok(location.path.join("overrides.toml"))
}
pub fn get_all_override_paths(&self) -> Vec<PathBuf> {
self.locations
.iter()
.filter(|loc| loc.exists)
.map(|loc| loc.path.join("overrides.toml"))
.filter(|path| path.exists())
.collect()
}
}
fn find_project_root(start_path: &Path) -> Option<PathBuf> {
let mut current = if start_path.is_file() {
start_path.parent()?
} else {
start_path
};
loop {
if current.join("Cargo.toml").exists() {
return Some(current.to_path_buf());
}
if current.join(".raz").exists() {
return Some(current.to_path_buf());
}
current = current.parent()?;
}
}
fn find_workspace_root(start_path: &Path) -> Option<PathBuf> {
let mut current = if start_path.is_file() {
start_path.parent()?
} else {
start_path
};
let mut _last_cargo_root = None;
loop {
let cargo_toml = current.join("Cargo.toml");
if cargo_toml.exists() {
if let Ok(content) = std::fs::read_to_string(&cargo_toml) {
if content.contains("[workspace]") {
return Some(current.to_path_buf());
}
}
_last_cargo_root = Some(current.to_path_buf());
}
current = current.parent()?;
}
}
fn get_global_config_path() -> Option<PathBuf> {
if let Ok(home) = env::var("HOME") {
Some(PathBuf::from(home).join(".raz"))
} else if let Ok(userprofile) = env::var("USERPROFILE") {
Some(PathBuf::from(userprofile).join(".raz"))
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_config_hierarchy_discovery() {
let temp = TempDir::new().unwrap();
let root = temp.path();
let workspace_cargo = "[workspace]\nmembers = [\"crate1\"]";
fs::write(root.join("Cargo.toml"), workspace_cargo).unwrap();
fs::create_dir_all(root.join(".raz")).unwrap();
let crate1 = root.join("crate1");
fs::create_dir_all(&crate1).unwrap();
fs::write(crate1.join("Cargo.toml"), "[package]\nname = \"crate1\"").unwrap();
let hierarchy = ConfigHierarchy::discover(&crate1).unwrap();
let locations = hierarchy.locations();
assert!(locations.len() >= 2);
assert_eq!(locations[0].level, ConfigLevel::Project);
assert_eq!(locations[0].path, crate1.join(".raz"));
assert_eq!(locations[1].level, ConfigLevel::Workspace);
assert_eq!(locations[1].path, root.join(".raz"));
}
#[test]
fn test_standalone_file_config() {
let temp = TempDir::new().unwrap();
let root = temp.path();
let rust_file = root.join("standalone.rs");
fs::write(&rust_file, "fn main() {}").unwrap();
fs::create_dir_all(root.join(".raz")).unwrap();
let hierarchy = ConfigHierarchy::discover(&rust_file).unwrap();
let locations = hierarchy.locations();
assert!(!locations.is_empty());
assert_eq!(locations[0].level, ConfigLevel::Project);
assert_eq!(locations[0].path, root.join(".raz"));
assert!(locations[0].exists);
}
}