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 std::fs;
136 use tempfile::tempdir;
137
138 #[test]
139 fn load_config_nonexistent_file() {
140 let temp_dir = tempdir().unwrap();
142 let original_dir = std::env::current_dir().unwrap();
143
144 std::env::set_current_dir(&temp_dir).unwrap();
146
147 let result = load_config();
148
149 std::env::set_current_dir(original_dir).unwrap();
151
152 assert!(result.is_none());
153 }
154
155 #[test]
156 fn load_config_from_path_nonexistent_file() {
157 let dir = tempdir().unwrap();
158 let result = load_config_from_path(dir.path());
159 assert!(result.is_none());
160 }
161
162 #[test]
163 fn load_config_from_path_valid_config() {
164 let dir = tempdir().unwrap();
165 let config_path = dir.path().join("context-builder.toml");
166
167 let config_content = r#"
168output = "test-output.md"
169filter = ["rs", "toml"]
170ignore = ["target", ".git"]
171line_numbers = true
172preview = false
173token_count = true
174timestamped_output = true
175yes = false
176auto_diff = true
177diff_context_lines = 5
178diff_only = false
179encoding_strategy = "detect"
180"#;
181
182 fs::write(&config_path, config_content).unwrap();
183
184 let config = load_config_from_path(dir.path()).unwrap();
185 assert_eq!(config.output.unwrap(), "test-output.md");
186 assert_eq!(config.filter.unwrap(), vec!["rs", "toml"]);
187 assert_eq!(config.ignore.unwrap(), vec!["target", ".git"]);
188 assert!(config.line_numbers.unwrap());
189 assert!(!config.preview.unwrap());
190 assert!(config.token_count.unwrap());
191 assert!(config.timestamped_output.unwrap());
192 assert!(!config.yes.unwrap());
193 assert!(config.auto_diff.unwrap());
194 assert_eq!(config.diff_context_lines.unwrap(), 5);
195 assert!(!config.diff_only.unwrap());
196 assert_eq!(config.encoding_strategy.unwrap(), "detect");
197 }
198
199 #[test]
200 fn load_config_from_path_partial_config() {
201 let dir = tempdir().unwrap();
202 let config_path = dir.path().join("context-builder.toml");
203
204 let config_content = r#"
205output = "minimal.md"
206filter = ["py"]
207"#;
208
209 fs::write(&config_path, config_content).unwrap();
210
211 let config = load_config_from_path(dir.path()).unwrap();
212 assert_eq!(config.output.unwrap(), "minimal.md");
213 assert_eq!(config.filter.unwrap(), vec!["py"]);
214 assert!(config.ignore.is_none());
215 assert!(config.line_numbers.is_none());
216 assert!(config.auto_diff.is_none());
217 }
218
219 #[test]
220 fn load_config_from_path_invalid_toml() {
221 let dir = tempdir().unwrap();
222 let config_path = dir.path().join("context-builder.toml");
223
224 let config_content = r#"
226output = "test.md"
227invalid_toml [
228"#;
229
230 fs::write(&config_path, config_content).unwrap();
231
232 let config = load_config_from_path(dir.path());
233 assert!(config.is_none());
234 }
235
236 #[test]
237 fn load_config_from_path_empty_config() {
238 let dir = tempdir().unwrap();
239 let config_path = dir.path().join("context-builder.toml");
240
241 fs::write(&config_path, "").unwrap();
242
243 let config = load_config_from_path(dir.path()).unwrap();
244 assert!(config.output.is_none());
245 assert!(config.filter.is_none());
246 assert!(config.ignore.is_none());
247 }
248
249 #[test]
250 fn config_default_implementation() {
251 let config = Config::default();
252 assert!(config.output.is_none());
253 assert!(config.filter.is_none());
254 assert!(config.ignore.is_none());
255 assert!(config.line_numbers.is_none());
256 assert!(config.preview.is_none());
257 assert!(config.token_count.is_none());
258 assert!(config.output_folder.is_none());
259 assert!(config.timestamped_output.is_none());
260 assert!(config.yes.is_none());
261 assert!(config.auto_diff.is_none());
262 assert!(config.diff_context_lines.is_none());
263 assert!(config.diff_only.is_none());
264 assert!(config.encoding_strategy.is_none());
265 assert!(config.max_tokens.is_none());
266 assert!(config.signatures.is_none());
267 assert!(config.structure.is_none());
268 assert!(config.truncate.is_none());
269 assert!(config.visibility.is_none());
270 }
271}