cc_audit/config/
loading.rs1use std::fs;
4use std::path::{Path, PathBuf};
5use tracing::debug;
6
7use super::error::ConfigError;
8use super::types::Config;
9
10#[derive(Debug)]
12pub struct ConfigLoadResult {
13 pub config: Config,
15 pub path: Option<PathBuf>,
17}
18
19impl Config {
20 pub fn from_file(path: &Path) -> Result<Self, ConfigError> {
22 let content = fs::read_to_string(path).map_err(|e| ConfigError::ReadFile {
23 path: path.display().to_string(),
24 source: e,
25 })?;
26
27 let ext = path
28 .extension()
29 .and_then(|e| e.to_str())
30 .unwrap_or("")
31 .to_lowercase();
32
33 match ext.as_str() {
34 "yaml" | "yml" => serde_yaml::from_str(&content).map_err(|e| ConfigError::ParseYaml {
35 path: path.display().to_string(),
36 source: e,
37 }),
38 "json" => serde_json::from_str(&content).map_err(|e| ConfigError::ParseJson {
39 path: path.display().to_string(),
40 source: e,
41 }),
42 "toml" => toml::from_str(&content).map_err(|e| ConfigError::ParseToml {
43 path: path.display().to_string(),
44 source: e,
45 }),
46 _ => Err(ConfigError::UnsupportedFormat(
47 path.display().to_string(),
48 ext,
49 )),
50 }
51 }
52
53 pub fn find_config_file(project_root: Option<&Path>) -> Option<PathBuf> {
60 const CONFIG_FILENAMES: &[&str] = &[
61 ".cc-audit.yaml",
62 ".cc-audit.yml",
63 ".cc-audit.json",
64 ".cc-audit.toml",
65 ];
66
67 debug!(project_root = ?project_root, "Searching for configuration file");
68
69 if let Some(root) = project_root {
71 let canonical = fs::canonicalize(root).unwrap_or_else(|e| {
74 debug!(error = %e, path = %root.display(), "Failed to canonicalize project root, using as-is");
75 root.to_path_buf()
76 });
77
78 let mut current = canonical.as_path();
79 loop {
80 debug!(directory = %current.display(), "Checking directory for config file");
81 for filename in CONFIG_FILENAMES {
82 let path = current.join(filename);
83 if path.exists() {
84 debug!(path = %path.display(), "Found configuration file");
85 return Some(path);
86 }
87 }
88
89 match current.parent() {
90 Some(parent) if !parent.as_os_str().is_empty() => current = parent,
91 _ => {
92 debug!("Reached filesystem root, stopping search");
93 break;
94 }
95 }
96 }
97 }
98
99 if let Some(config_dir) = dirs::config_dir() {
101 let global_config = config_dir.join("cc-audit").join("config.yaml");
102 if global_config.exists() {
103 debug!(path = %global_config.display(), "Found global configuration file");
104 return Some(global_config);
105 }
106 }
107
108 debug!("No configuration file found");
109 None
110 }
111
112 pub fn try_load(project_root: Option<&Path>) -> ConfigLoadResult {
115 if let Some(path) = Self::find_config_file(project_root)
116 && let Ok(config) = Self::from_file(&path)
117 {
118 return ConfigLoadResult {
119 config,
120 path: Some(path),
121 };
122 }
123
124 ConfigLoadResult {
125 config: Self::default(),
126 path: None,
127 }
128 }
129
130 pub fn load(project_root: Option<&Path>) -> Self {
140 Self::try_load(project_root).config
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147 use std::fs;
148 use tempfile::TempDir;
149
150 fn assert_paths_eq(actual: &Path, expected: &Path) {
152 assert_eq!(
153 actual.canonicalize().unwrap(),
154 expected.canonicalize().unwrap()
155 );
156 }
157
158 #[test]
159 fn test_find_config_file_walks_up_to_parent() {
160 let temp_dir = TempDir::new().unwrap();
161 let subdir = temp_dir.path().join("subdir");
162 fs::create_dir(&subdir).unwrap();
163
164 let config_path = temp_dir.path().join(".cc-audit.yaml");
165 fs::write(&config_path, "# Test config\n").unwrap();
166
167 let found = Config::find_config_file(Some(&subdir))
168 .expect("should find config in parent directory");
169
170 assert_paths_eq(&found, &config_path);
171 }
172
173 #[test]
174 fn test_find_config_file_with_relative_path() {
175 let temp_dir = TempDir::new().unwrap();
176 let subdir = temp_dir.path().join("subdir");
177 fs::create_dir(&subdir).unwrap();
178
179 let config_path = temp_dir.path().join(".cc-audit.yaml");
180 fs::write(&config_path, "# Test config\n").unwrap();
181
182 let original_dir = std::env::current_dir().unwrap();
185 std::env::set_current_dir(temp_dir.path()).unwrap();
186 let found = Config::find_config_file(Some(Path::new("subdir")));
187 std::env::set_current_dir(original_dir).unwrap();
188
189 assert!(found.is_some(), "should find config via relative path");
190 }
191
192 #[test]
193 fn test_find_config_file_returns_none_when_missing() {
194 let temp_dir = TempDir::new().unwrap();
195 let subdir = temp_dir.path().join("subdir");
196 fs::create_dir(&subdir).unwrap();
197
198 let found = Config::find_config_file(Some(&subdir));
199
200 assert!(found.is_none());
201 }
202
203 #[test]
204 fn test_find_config_file_in_same_directory() {
205 let temp_dir = TempDir::new().unwrap();
206
207 let config_path = temp_dir.path().join(".cc-audit.yaml");
208 fs::write(&config_path, "# Test config\n").unwrap();
209
210 let found = Config::find_config_file(Some(temp_dir.path()))
211 .expect("should find config in same directory");
212
213 assert_paths_eq(&found, &config_path);
214 }
215
216 #[test]
217 fn test_find_config_file_traverses_multiple_levels() {
218 let temp_dir = TempDir::new().unwrap();
219 let level3 = temp_dir.path().join("level1").join("level2").join("level3");
220 fs::create_dir_all(&level3).unwrap();
221
222 let config_path = temp_dir.path().join(".cc-audit.yaml");
223 fs::write(&config_path, "# Test config\n").unwrap();
224
225 let found =
226 Config::find_config_file(Some(&level3)).expect("should find config 3 levels up");
227
228 assert_paths_eq(&found, &config_path);
229 }
230
231 #[test]
232 fn test_find_config_file_prefers_closest_config() {
233 let temp_dir = TempDir::new().unwrap();
234 let subdir = temp_dir.path().join("subdir");
235 fs::create_dir(&subdir).unwrap();
236
237 let parent_config = temp_dir.path().join(".cc-audit.yaml");
238 let subdir_config = subdir.join(".cc-audit.yaml");
239 fs::write(&parent_config, "# Parent config\n").unwrap();
240 fs::write(&subdir_config, "# Subdir config\n").unwrap();
241
242 let found = Config::find_config_file(Some(&subdir)).expect("should find closest config");
243
244 assert_paths_eq(&found, &subdir_config);
245 }
246}