1use chrono::Utc;
8use std::path::{Path, PathBuf};
9
10use crate::cli::Args;
11use crate::config::Config;
12
13#[derive(Debug, Clone)]
15pub struct ResolvedConfig {
16 pub input: String,
17 pub output: String,
18 pub filter: Vec<String>,
19 pub ignore: Vec<String>,
20 pub line_numbers: bool,
21 pub preview: bool,
22 pub token_count: bool,
23 pub yes: bool,
24 pub diff_only: bool,
25 pub clear_cache: bool,
26 pub auto_diff: bool,
27 pub diff_context_lines: usize,
28 pub max_tokens: Option<usize>,
29 pub init: bool,
30 pub signatures: bool,
31 pub structure: bool,
32 pub truncate: String,
33 pub visibility: String,
34}
35
36#[derive(Debug)]
38pub struct ConfigResolution {
39 pub config: ResolvedConfig,
40 pub warnings: Vec<String>,
41}
42
43pub fn resolve_final_config(mut args: Args, config: Option<Config>) -> ConfigResolution {
55 let mut warnings = Vec::new();
56
57 let final_config = if let Some(config) = config {
59 apply_config_to_args(&mut args, &config, &mut warnings);
60 resolve_output_path(&mut args, &config, &mut warnings);
61 config
62 } else {
63 Config::default()
64 };
65
66 let resolved = ResolvedConfig {
67 input: args.input,
68 output: args.output,
69 filter: args.filter,
70 ignore: args.ignore,
71 line_numbers: args.line_numbers,
72 preview: args.preview,
73 token_count: args.token_count,
74 yes: args.yes,
75 diff_only: args.diff_only,
76 clear_cache: args.clear_cache,
77 auto_diff: final_config.auto_diff.unwrap_or(false),
78 diff_context_lines: final_config.diff_context_lines.unwrap_or(3),
79 max_tokens: args.max_tokens.or(final_config.max_tokens),
80 init: args.init,
81 signatures: args.signatures || final_config.signatures.unwrap_or(false),
82 structure: args.structure || final_config.structure.unwrap_or(false),
83 truncate: if args.truncate != "smart" {
84 args.truncate.clone()
85 } else {
86 final_config
87 .truncate
88 .clone()
89 .unwrap_or_else(|| args.truncate.clone())
90 },
91 visibility: if args.visibility != "all" {
92 args.visibility.clone()
93 } else {
94 final_config
95 .visibility
96 .clone()
97 .unwrap_or_else(|| args.visibility.clone())
98 },
99 };
100
101 ConfigResolution {
102 config: resolved,
103 warnings,
104 }
105}
106
107fn apply_config_to_args(args: &mut Args, config: &Config, warnings: &mut Vec<String>) {
109 if args.output == "output.md"
111 && let Some(ref output) = config.output
112 {
113 args.output = output.clone();
114 }
115
116 if args.filter.is_empty()
118 && let Some(ref filter) = config.filter
119 {
120 args.filter = filter.clone();
121 }
122
123 if args.ignore.is_empty()
125 && let Some(ref ignore) = config.ignore
126 {
127 args.ignore = ignore.clone();
128 }
129
130 if !args.line_numbers
134 && let Some(line_numbers) = config.line_numbers
135 {
136 args.line_numbers = line_numbers;
137 }
138
139 if !args.preview
140 && let Some(preview) = config.preview
141 {
142 args.preview = preview;
143 }
144
145 if !args.token_count
146 && let Some(token_count) = config.token_count
147 {
148 args.token_count = token_count;
149 }
150
151 if !args.yes
152 && let Some(yes) = config.yes
153 {
154 args.yes = yes;
155 }
156
157 if !args.diff_only
159 && let Some(true) = config.diff_only
160 {
161 args.diff_only = true;
162 }
163
164 if let Some(true) = config.auto_diff
166 && config.timestamped_output != Some(true)
167 {
168 warnings.push(
169 "auto_diff is enabled but timestamped_output is not enabled. \
170 Auto-diff requires timestamped_output = true to function properly."
171 .to_string(),
172 );
173 }
174}
175
176fn resolve_output_path(args: &mut Args, config: &Config, warnings: &mut Vec<String>) {
178 let mut output_folder_path: Option<PathBuf> = None;
179
180 if let Some(ref output_folder) = config.output_folder {
182 let mut path = PathBuf::from(output_folder);
183 path.push(&args.output);
184 args.output = path.to_string_lossy().to_string();
185 output_folder_path = Some(PathBuf::from(output_folder));
186 }
187
188 if let Some(true) = config.timestamped_output {
190 let timestamp = Utc::now().format("%Y%m%d%H%M%S").to_string();
191 let path = Path::new(&args.output);
192
193 let stem = path
194 .file_stem()
195 .and_then(|s| s.to_str())
196 .unwrap_or("output");
197
198 let extension = path.extension().and_then(|s| s.to_str()).unwrap_or("md");
199
200 let new_filename = format!("{}_{}.{}", stem, timestamp, extension);
201
202 if let Some(output_folder) = output_folder_path {
203 args.output = output_folder
204 .join(new_filename)
205 .to_string_lossy()
206 .to_string();
207 } else {
208 let new_path = path.with_file_name(new_filename);
209 args.output = new_path.to_string_lossy().to_string();
210 }
211 }
212
213 if let Some(ref output_folder) = config.output_folder {
215 let folder_path = Path::new(output_folder);
216 if !folder_path.exists() {
217 warnings.push(format!(
218 "Output folder '{}' does not exist. It will be created if possible.",
219 output_folder
220 ));
221 }
222 }
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228
229 #[test]
230 fn test_config_precedence_cli_over_config() {
231 let args = Args {
232 input: "src".to_string(),
233 output: "custom.md".to_string(), filter: vec!["rs".to_string()], ignore: vec![],
236 line_numbers: true, preview: false,
238 token_count: false,
239 yes: false,
240 diff_only: false,
241 clear_cache: false,
242 init: false,
243 max_tokens: None,
244 signatures: false,
245 structure: false,
246 truncate: "smart".to_string(),
247 visibility: "all".to_string(),
248 };
249
250 let config = Config {
251 output: Some("config.md".to_string()), filter: Some(vec!["toml".to_string()]), line_numbers: Some(false), preview: Some(true), ..Default::default()
256 };
257
258 let resolution = resolve_final_config(args.clone(), Some(config));
259
260 assert_eq!(resolution.config.output, "custom.md"); assert_eq!(resolution.config.filter, vec!["rs"]); assert!(resolution.config.line_numbers); assert!(resolution.config.preview); }
265
266 #[test]
267 fn test_config_applies_when_cli_uses_defaults() {
268 let args = Args {
269 input: "src".to_string(),
270 output: "output.md".to_string(), filter: vec![], ignore: vec![], line_numbers: false, preview: false, token_count: false, yes: false, diff_only: false, clear_cache: false,
279 init: false,
280 max_tokens: None,
281 signatures: false,
282 structure: false,
283 truncate: "smart".to_string(),
284 visibility: "all".to_string(),
285 };
286
287 let config = Config {
288 output: Some("from_config.md".to_string()),
289 filter: Some(vec!["rs".to_string(), "toml".to_string()]),
290 ignore: Some(vec!["target".to_string()]),
291 line_numbers: Some(true),
292 preview: Some(true),
293 token_count: Some(true),
294 yes: Some(true),
295 diff_only: Some(true),
296 ..Default::default()
297 };
298
299 let resolution = resolve_final_config(args, Some(config));
300
301 assert_eq!(resolution.config.output, "from_config.md");
302 assert_eq!(
303 resolution.config.filter,
304 vec!["rs".to_string(), "toml".to_string()]
305 );
306 assert_eq!(resolution.config.ignore, vec!["target".to_string()]);
307 assert!(resolution.config.line_numbers);
308 assert!(resolution.config.preview);
309 assert!(resolution.config.token_count);
310 assert!(resolution.config.yes);
311 assert!(resolution.config.diff_only);
312 }
313
314 #[test]
315 fn test_timestamped_output_resolution() {
316 let args = Args {
317 input: "src".to_string(),
318 output: "test.md".to_string(),
319 filter: vec![],
320 ignore: vec![],
321 line_numbers: false,
322 preview: false,
323 token_count: false,
324 yes: false,
325 diff_only: false,
326 clear_cache: false,
327 init: false,
328 max_tokens: None,
329 signatures: false,
330 structure: false,
331 truncate: "smart".to_string(),
332 visibility: "all".to_string(),
333 };
334
335 let config = Config {
336 timestamped_output: Some(true),
337 ..Default::default()
338 };
339
340 let resolution = resolve_final_config(args, Some(config));
341
342 assert!(resolution.config.output.starts_with("test_"));
344 assert!(resolution.config.output.ends_with(".md"));
345 assert!(resolution.config.output.len() > "test_.md".len());
346 }
347
348 #[test]
349 fn test_output_folder_resolution() {
350 let args = Args {
351 input: "src".to_string(),
352 output: "test.md".to_string(),
353 filter: vec![],
354 ignore: vec![],
355 line_numbers: false,
356 preview: false,
357 token_count: false,
358 yes: false,
359 diff_only: false,
360 clear_cache: false,
361 init: false,
362 max_tokens: None,
363 signatures: false,
364 structure: false,
365 truncate: "smart".to_string(),
366 visibility: "all".to_string(),
367 };
368
369 let config = Config {
370 output_folder: Some("docs".to_string()),
371 ..Default::default()
372 };
373
374 let resolution = resolve_final_config(args, Some(config));
375
376 assert!(resolution.config.output.contains("docs"));
377 assert!(resolution.config.output.ends_with("test.md"));
378 }
379
380 #[test]
381 fn test_output_folder_with_timestamping() {
382 let args = Args {
383 input: "src".to_string(),
384 output: "test.md".to_string(),
385 filter: vec![],
386 ignore: vec![],
387 line_numbers: false,
388 preview: false,
389 token_count: false,
390 yes: false,
391 diff_only: false,
392 clear_cache: false,
393 init: false,
394 max_tokens: None,
395 signatures: false,
396 structure: false,
397 truncate: "smart".to_string(),
398 visibility: "all".to_string(),
399 };
400
401 let config = Config {
402 output_folder: Some("docs".to_string()),
403 timestamped_output: Some(true),
404 ..Default::default()
405 };
406
407 let resolution = resolve_final_config(args, Some(config));
408
409 assert!(resolution.config.output.contains("docs"));
410 assert!(resolution.config.output.contains("test_"));
411 assert!(resolution.config.output.ends_with(".md"));
412 }
413
414 #[test]
415 fn test_auto_diff_without_timestamping_warning() {
416 let args = Args {
417 input: "src".to_string(),
418 output: "test.md".to_string(),
419 filter: vec![],
420 ignore: vec![],
421 line_numbers: false,
422 preview: false,
423 token_count: false,
424 yes: false,
425 diff_only: false,
426 clear_cache: false,
427 init: false,
428 max_tokens: None,
429 signatures: false,
430 structure: false,
431 truncate: "smart".to_string(),
432 visibility: "all".to_string(),
433 };
434
435 let config = Config {
436 auto_diff: Some(true),
437 timestamped_output: Some(false), ..Default::default()
439 };
440
441 let resolution = resolve_final_config(args, Some(config));
442
443 assert!(!resolution.warnings.is_empty());
444 assert!(resolution.warnings[0].contains("auto_diff"));
445 assert!(resolution.warnings[0].contains("timestamped_output"));
446 }
447
448 #[test]
449 fn test_no_config_uses_cli_defaults() {
450 let args = Args {
451 input: "src".to_string(),
452 output: "output.md".to_string(),
453 filter: vec![],
454 ignore: vec![],
455 line_numbers: false,
456 preview: false,
457 token_count: false,
458 yes: false,
459 diff_only: false,
460 clear_cache: false,
461 init: false,
462 max_tokens: None,
463 signatures: false,
464 structure: false,
465 truncate: "smart".to_string(),
466 visibility: "all".to_string(),
467 };
468
469 let resolution = resolve_final_config(args.clone(), None);
470
471 assert_eq!(resolution.config.input, args.input);
472 assert_eq!(resolution.config.output, args.output);
473 assert_eq!(resolution.config.filter, args.filter);
474 assert_eq!(resolution.config.ignore, args.ignore);
475 assert_eq!(resolution.config.line_numbers, args.line_numbers);
476 assert_eq!(resolution.config.preview, args.preview);
477 assert_eq!(resolution.config.token_count, args.token_count);
478 assert_eq!(resolution.config.yes, args.yes);
479 assert_eq!(resolution.config.diff_only, args.diff_only);
480 assert!(!resolution.config.auto_diff);
481 assert_eq!(resolution.config.diff_context_lines, 3);
482 assert!(resolution.warnings.is_empty());
483 }
484}