context_builder/
config.rs1use serde::Deserialize;
2use std::fs;
3use std::path::Path;
4
5#[derive(Deserialize, Debug, Default, Clone)]
24pub struct Config {
25 pub output: Option<String>,
27
28 pub filter: Option<Vec<String>>,
30
31 pub ignore: Option<Vec<String>>,
33
34 pub line_numbers: Option<bool>,
36
37 pub preview: Option<bool>,
39
40 pub token_count: Option<bool>,
42
43 pub output_folder: Option<String>,
45
46 pub timestamped_output: Option<bool>,
48
49 pub yes: Option<bool>,
51
52 pub auto_diff: Option<bool>,
54
55 pub diff_context_lines: Option<usize>,
57
58 pub diff_only: Option<bool>,
66
67 pub encoding_strategy: Option<String>,
72
73 pub max_tokens: Option<usize>,
75
76 pub signatures: Option<bool>,
78
79 pub structure: Option<bool>,
81
82 pub truncate: Option<String>,
84
85 pub visibility: Option<String>,
87}
88
89pub fn load_config() -> Option<Config> {
92 let config_path = Path::new("context-builder.toml");
93 if config_path.exists() {
94 let content = fs::read_to_string(config_path).ok()?;
95 match toml::from_str(&content) {
96 Ok(config) => Some(config),
97 Err(e) => {
98 eprintln!(
99 "⚠️ Failed to parse context-builder.toml: {}. Config will be ignored.",
100 e
101 );
102 None
103 }
104 }
105 } else {
106 None
107 }
108}
109
110pub fn load_config_from_path(project_root: &Path) -> Option<Config> {
113 let config_path = project_root.join("context-builder.toml");
114 if config_path.exists() {
115 let content = fs::read_to_string(&config_path).ok()?;
116 match toml::from_str(&content) {
117 Ok(config) => Some(config),
118 Err(e) => {
119 eprintln!(
120 "⚠️ Failed to parse {}: {}. Config will be ignored.",
121 config_path.display(),
122 e
123 );
124 None
125 }
126 }
127 } else {
128 None
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135 use serial_test::serial;
136 use std::fs;
137 use tempfile::tempdir;
138
139 #[test]
140 #[serial]
141 fn load_config_nonexistent_file() {
142 let temp_dir = tempdir().unwrap();
144 let original_dir = std::env::current_dir().unwrap();
145
146 std::env::set_current_dir(&temp_dir).unwrap();
148
149 let result = load_config();
150
151 std::env::set_current_dir(original_dir).unwrap();
153
154 assert!(result.is_none());
155 }
156
157 #[test]
158 fn load_config_from_path_nonexistent_file() {
159 let dir = tempdir().unwrap();
160 let result = load_config_from_path(dir.path());
161 assert!(result.is_none());
162 }
163
164 #[test]
165 fn load_config_from_path_valid_config() {
166 let dir = tempdir().unwrap();
167 let config_path = dir.path().join("context-builder.toml");
168
169 let config_content = r#"
170output = "test-output.md"
171filter = ["rs", "toml"]
172ignore = ["target", ".git"]
173line_numbers = true
174preview = false
175token_count = true
176timestamped_output = true
177yes = false
178auto_diff = true
179diff_context_lines = 5
180diff_only = false
181encoding_strategy = "detect"
182"#;
183
184 fs::write(&config_path, config_content).unwrap();
185
186 let config = load_config_from_path(dir.path()).unwrap();
187 assert_eq!(config.output.unwrap(), "test-output.md");
188 assert_eq!(config.filter.unwrap(), vec!["rs", "toml"]);
189 assert_eq!(config.ignore.unwrap(), vec!["target", ".git"]);
190 assert!(config.line_numbers.unwrap());
191 assert!(!config.preview.unwrap());
192 assert!(config.token_count.unwrap());
193 assert!(config.timestamped_output.unwrap());
194 assert!(!config.yes.unwrap());
195 assert!(config.auto_diff.unwrap());
196 assert_eq!(config.diff_context_lines.unwrap(), 5);
197 assert!(!config.diff_only.unwrap());
198 assert_eq!(config.encoding_strategy.unwrap(), "detect");
199 }
200
201 #[test]
202 fn load_config_from_path_partial_config() {
203 let dir = tempdir().unwrap();
204 let config_path = dir.path().join("context-builder.toml");
205
206 let config_content = r#"
207output = "minimal.md"
208filter = ["py"]
209"#;
210
211 fs::write(&config_path, config_content).unwrap();
212
213 let config = load_config_from_path(dir.path()).unwrap();
214 assert_eq!(config.output.unwrap(), "minimal.md");
215 assert_eq!(config.filter.unwrap(), vec!["py"]);
216 assert!(config.ignore.is_none());
217 assert!(config.line_numbers.is_none());
218 assert!(config.auto_diff.is_none());
219 }
220
221 #[test]
222 fn load_config_from_path_invalid_toml() {
223 let dir = tempdir().unwrap();
224 let config_path = dir.path().join("context-builder.toml");
225
226 let config_content = r#"
228output = "test.md"
229invalid_toml [
230"#;
231
232 fs::write(&config_path, config_content).unwrap();
233
234 let config = load_config_from_path(dir.path());
235 assert!(config.is_none());
236 }
237
238 #[test]
239 fn load_config_from_path_empty_config() {
240 let dir = tempdir().unwrap();
241 let config_path = dir.path().join("context-builder.toml");
242
243 fs::write(&config_path, "").unwrap();
244
245 let config = load_config_from_path(dir.path()).unwrap();
246 assert!(config.output.is_none());
247 assert!(config.filter.is_none());
248 assert!(config.ignore.is_none());
249 }
250
251 #[test]
252 fn config_default_implementation() {
253 let config = Config::default();
254 assert!(config.output.is_none());
255 assert!(config.filter.is_none());
256 assert!(config.ignore.is_none());
257 assert!(config.line_numbers.is_none());
258 assert!(config.preview.is_none());
259 assert!(config.token_count.is_none());
260 assert!(config.output_folder.is_none());
261 assert!(config.timestamped_output.is_none());
262 assert!(config.yes.is_none());
263 assert!(config.auto_diff.is_none());
264 assert!(config.diff_context_lines.is_none());
265 assert!(config.diff_only.is_none());
266 assert!(config.encoding_strategy.is_none());
267 assert!(config.max_tokens.is_none());
268 assert!(config.signatures.is_none());
269 assert!(config.structure.is_none());
270 assert!(config.truncate.is_none());
271 assert!(config.visibility.is_none());
272 }
273
274 #[test]
275 #[serial]
276 fn load_config_invalid_toml_in_cwd() {
277 let temp_dir = tempdir().unwrap();
278 let original_dir = std::env::current_dir().unwrap();
279
280 std::env::set_current_dir(&temp_dir).unwrap();
281
282 let config_path = temp_dir.path().join("context-builder.toml");
283 let invalid_toml = r#"
284output = "test.md"
285invalid_toml [
286"#;
287 fs::write(&config_path, invalid_toml).unwrap();
288
289 let result = load_config();
290
291 std::env::set_current_dir(original_dir).unwrap();
292
293 assert!(result.is_none());
294 }
295
296 #[test]
297 #[serial]
298 fn load_config_valid_in_cwd() {
299 let temp_dir = tempdir().unwrap();
300 let original_dir = std::env::current_dir().unwrap();
301
302 std::env::set_current_dir(&temp_dir).unwrap();
303
304 let config_path = temp_dir.path().join("context-builder.toml");
305 let valid_toml = r#"
306output = "context.md"
307filter = ["rs"]
308"#;
309 fs::write(&config_path, valid_toml).unwrap();
310
311 let result = load_config();
312
313 std::env::set_current_dir(original_dir).unwrap();
314
315 assert!(result.is_some());
316 let config = result.unwrap();
317 assert_eq!(config.output, Some("context.md".to_string()));
318 }
319}