1use crate::error::{ConfigError, Result};
9use std::env;
10use std::path::{Path, PathBuf};
11
12#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
14pub enum ConfigLevel {
15 Project,
17 Workspace,
19 Global,
21}
22
23#[derive(Debug, Clone)]
25pub struct ConfigLocation {
26 pub level: ConfigLevel,
27 pub path: PathBuf,
28 pub exists: bool,
29}
30
31pub struct ConfigHierarchy {
33 locations: Vec<ConfigLocation>,
34}
35
36impl ConfigHierarchy {
37 pub fn discover(start_path: &Path) -> Result<Self> {
39 let mut locations = Vec::new();
40
41 if let Some(project_root) = find_project_root(start_path) {
43 let config_path = project_root.join(".raz");
44 locations.push(ConfigLocation {
45 level: ConfigLevel::Project,
46 path: config_path.clone(),
47 exists: config_path.exists(),
48 });
49 }
50
51 if let Some(workspace_root) = find_workspace_root(start_path) {
53 let config_path = workspace_root.join(".raz");
54
55 if !locations.iter().any(|loc| loc.path == config_path) {
57 locations.push(ConfigLocation {
58 level: ConfigLevel::Workspace,
59 path: config_path.clone(),
60 exists: config_path.exists(),
61 });
62 }
63 }
64
65 if let Some(global_path) = get_global_config_path() {
67 locations.push(ConfigLocation {
68 level: ConfigLevel::Global,
69 path: global_path.clone(),
70 exists: global_path.exists(),
71 });
72 }
73
74 Ok(Self { locations })
75 }
76
77 pub fn locations(&self) -> &[ConfigLocation] {
79 &self.locations
80 }
81
82 pub fn primary_location(&self) -> Option<&ConfigLocation> {
84 self.locations.iter().find(|loc| loc.exists)
85 }
86
87 pub fn get_or_create_location(
89 &self,
90 prefer_level: Option<ConfigLevel>,
91 ) -> Result<&ConfigLocation> {
92 if let Some(level) = prefer_level {
94 if let Some(loc) = self.locations.iter().find(|l| l.level == level) {
95 return Ok(loc);
96 }
97 }
98
99 self.locations.first().ok_or_else(|| {
101 ConfigError::ValidationError("No configuration location available".to_string())
102 })
103 }
104
105 pub fn init_config(&self, level: ConfigLevel) -> Result<PathBuf> {
107 let location = self
108 .locations
109 .iter()
110 .find(|loc| loc.level == level)
111 .ok_or_else(|| {
112 ConfigError::ValidationError(format!("No {level:?} level configuration path found"))
113 })?;
114
115 std::fs::create_dir_all(&location.path)?;
117
118 Ok(location.path.clone())
119 }
120
121 pub fn get_override_storage_path(&self) -> Result<PathBuf> {
123 let location = self.get_or_create_location(Some(ConfigLevel::Project))?;
125 Ok(location.path.join("overrides.toml"))
126 }
127
128 pub fn get_all_override_paths(&self) -> Vec<PathBuf> {
130 self.locations
131 .iter()
132 .filter(|loc| loc.exists)
133 .map(|loc| loc.path.join("overrides.toml"))
134 .filter(|path| path.exists())
135 .collect()
136 }
137}
138
139fn find_project_root(start_path: &Path) -> Option<PathBuf> {
141 let mut current = if start_path.is_file() {
142 start_path.parent()?
143 } else {
144 start_path
145 };
146
147 loop {
148 if current.join("Cargo.toml").exists() {
150 return Some(current.to_path_buf());
151 }
152
153 if current.join(".raz").exists() {
155 return Some(current.to_path_buf());
156 }
157
158 current = current.parent()?;
159 }
160}
161
162fn find_workspace_root(start_path: &Path) -> Option<PathBuf> {
164 let mut current = if start_path.is_file() {
165 start_path.parent()?
166 } else {
167 start_path
168 };
169
170 let mut _last_cargo_root = None;
171
172 loop {
173 let cargo_toml = current.join("Cargo.toml");
174 if cargo_toml.exists() {
175 if let Ok(content) = std::fs::read_to_string(&cargo_toml) {
177 if content.contains("[workspace]") {
178 return Some(current.to_path_buf());
179 }
180 }
181 _last_cargo_root = Some(current.to_path_buf());
182 }
183
184 current = current.parent()?;
185 }
186}
187
188fn get_global_config_path() -> Option<PathBuf> {
190 if let Ok(home) = env::var("HOME") {
191 Some(PathBuf::from(home).join(".raz"))
192 } else if let Ok(userprofile) = env::var("USERPROFILE") {
193 Some(PathBuf::from(userprofile).join(".raz"))
195 } else {
196 None
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203 use std::fs;
204 use tempfile::TempDir;
205
206 #[test]
207 fn test_config_hierarchy_discovery() {
208 let temp = TempDir::new().unwrap();
209 let root = temp.path();
210
211 let workspace_cargo = "[workspace]\nmembers = [\"crate1\"]";
213 fs::write(root.join("Cargo.toml"), workspace_cargo).unwrap();
214 fs::create_dir_all(root.join(".raz")).unwrap();
215
216 let crate1 = root.join("crate1");
218 fs::create_dir_all(&crate1).unwrap();
219 fs::write(crate1.join("Cargo.toml"), "[package]\nname = \"crate1\"").unwrap();
220
221 let hierarchy = ConfigHierarchy::discover(&crate1).unwrap();
223 let locations = hierarchy.locations();
224
225 assert!(locations.len() >= 2); assert_eq!(locations[0].level, ConfigLevel::Project);
230 assert_eq!(locations[0].path, crate1.join(".raz"));
231
232 assert_eq!(locations[1].level, ConfigLevel::Workspace);
234 assert_eq!(locations[1].path, root.join(".raz"));
235 }
236
237 #[test]
238 fn test_standalone_file_config() {
239 let temp = TempDir::new().unwrap();
240 let root = temp.path();
241
242 let rust_file = root.join("standalone.rs");
244 fs::write(&rust_file, "fn main() {}").unwrap();
245
246 fs::create_dir_all(root.join(".raz")).unwrap();
248
249 let hierarchy = ConfigHierarchy::discover(&rust_file).unwrap();
250 let locations = hierarchy.locations();
251
252 assert!(!locations.is_empty());
254 assert_eq!(locations[0].level, ConfigLevel::Project);
255 assert_eq!(locations[0].path, root.join(".raz"));
256 assert!(locations[0].exists);
257 }
258}