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}
31
32#[derive(Debug)]
34pub struct ConfigResolution {
35 pub config: ResolvedConfig,
36 pub warnings: Vec<String>,
37}
38
39pub fn resolve_final_config(mut args: Args, config: Option<Config>) -> ConfigResolution {
51 let mut warnings = Vec::new();
52
53 let final_config = if let Some(config) = config {
55 apply_config_to_args(&mut args, &config, &mut warnings);
56 resolve_output_path(&mut args, &config, &mut warnings);
57 config
58 } else {
59 Config::default()
60 };
61
62 let resolved = ResolvedConfig {
63 input: args.input,
64 output: args.output,
65 filter: args.filter,
66 ignore: args.ignore,
67 line_numbers: args.line_numbers,
68 preview: args.preview,
69 token_count: args.token_count,
70 yes: args.yes,
71 diff_only: args.diff_only,
72 clear_cache: args.clear_cache,
73 auto_diff: final_config.auto_diff.unwrap_or(false),
74 diff_context_lines: final_config.diff_context_lines.unwrap_or(3),
75 max_tokens: args.max_tokens.or(final_config.max_tokens),
76 init: args.init,
77 };
78
79 ConfigResolution {
80 config: resolved,
81 warnings,
82 }
83}
84
85fn apply_config_to_args(args: &mut Args, config: &Config, warnings: &mut Vec<String>) {
87 if args.output == "output.md"
89 && let Some(ref output) = config.output
90 {
91 args.output = output.clone();
92 }
93
94 if args.filter.is_empty()
96 && let Some(ref filter) = config.filter
97 {
98 args.filter = filter.clone();
99 }
100
101 if args.ignore.is_empty()
103 && let Some(ref ignore) = config.ignore
104 {
105 args.ignore = ignore.clone();
106 }
107
108 if !args.line_numbers
112 && let Some(line_numbers) = config.line_numbers
113 {
114 args.line_numbers = line_numbers;
115 }
116
117 if !args.preview
118 && let Some(preview) = config.preview
119 {
120 args.preview = preview;
121 }
122
123 if !args.token_count
124 && let Some(token_count) = config.token_count
125 {
126 args.token_count = token_count;
127 }
128
129 if !args.yes
130 && let Some(yes) = config.yes
131 {
132 args.yes = yes;
133 }
134
135 if !args.diff_only
137 && let Some(true) = config.diff_only
138 {
139 args.diff_only = true;
140 }
141
142 if let Some(true) = config.auto_diff
144 && config.timestamped_output != Some(true)
145 {
146 warnings.push(
147 "auto_diff is enabled but timestamped_output is not enabled. \
148 Auto-diff requires timestamped_output = true to function properly."
149 .to_string(),
150 );
151 }
152}
153
154fn resolve_output_path(args: &mut Args, config: &Config, warnings: &mut Vec<String>) {
156 let mut output_folder_path: Option<PathBuf> = None;
157
158 if let Some(ref output_folder) = config.output_folder {
160 let mut path = PathBuf::from(output_folder);
161 path.push(&args.output);
162 args.output = path.to_string_lossy().to_string();
163 output_folder_path = Some(PathBuf::from(output_folder));
164 }
165
166 if let Some(true) = config.timestamped_output {
168 let timestamp = Utc::now().format("%Y%m%d%H%M%S").to_string();
169 let path = Path::new(&args.output);
170
171 let stem = path
172 .file_stem()
173 .and_then(|s| s.to_str())
174 .unwrap_or("output");
175
176 let extension = path.extension().and_then(|s| s.to_str()).unwrap_or("md");
177
178 let new_filename = format!("{}_{}.{}", stem, timestamp, extension);
179
180 if let Some(output_folder) = output_folder_path {
181 args.output = output_folder
182 .join(new_filename)
183 .to_string_lossy()
184 .to_string();
185 } else {
186 let new_path = path.with_file_name(new_filename);
187 args.output = new_path.to_string_lossy().to_string();
188 }
189 }
190
191 if let Some(ref output_folder) = config.output_folder {
193 let folder_path = Path::new(output_folder);
194 if !folder_path.exists() {
195 warnings.push(format!(
196 "Output folder '{}' does not exist. It will be created if possible.",
197 output_folder
198 ));
199 }
200 }
201}
202
203#[allow(dead_code)]
206fn detect_explicit_args() -> ExplicitArgs {
207 let args: Vec<String> = std::env::args().collect();
208
209 ExplicitArgs {
210 output: args.iter().any(|arg| arg == "-o" || arg == "--output"),
211 filter: args.iter().any(|arg| arg == "-f" || arg == "--filter"),
212 ignore: args.iter().any(|arg| arg == "-i" || arg == "--ignore"),
213 line_numbers: args.iter().any(|arg| arg == "--line-numbers"),
214 preview: args.iter().any(|arg| arg == "--preview"),
215 token_count: args.iter().any(|arg| arg == "--token-count"),
216 yes: args.iter().any(|arg| arg == "-y" || arg == "--yes"),
217 diff_only: args.iter().any(|arg| arg == "--diff-only"),
218 }
219}
220
221#[allow(dead_code)]
223struct ExplicitArgs {
224 output: bool,
225 filter: bool,
226 ignore: bool,
227 line_numbers: bool,
228 preview: bool,
229 token_count: bool,
230 yes: bool,
231 diff_only: bool,
232}
233
234#[cfg(test)]
235mod tests {
236 use super::*;
237
238 #[test]
239 fn test_config_precedence_cli_over_config() {
240 let args = Args {
241 input: "src".to_string(),
242 output: "custom.md".to_string(), filter: vec!["rs".to_string()], ignore: vec![],
245 line_numbers: true, preview: false,
247 token_count: false,
248 yes: false,
249 diff_only: false,
250 clear_cache: false,
251 init: false,
252 max_tokens: None,
253 };
254
255 let config = Config {
256 output: Some("config.md".to_string()), filter: Some(vec!["toml".to_string()]), line_numbers: Some(false), preview: Some(true), ..Default::default()
261 };
262
263 let resolution = resolve_final_config(args.clone(), Some(config));
264
265 assert_eq!(resolution.config.output, "custom.md"); assert_eq!(resolution.config.filter, vec!["rs"]); assert!(resolution.config.line_numbers); assert!(resolution.config.preview); }
270
271 #[test]
272 fn test_config_applies_when_cli_uses_defaults() {
273 let args = Args {
274 input: "src".to_string(),
275 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,
284 init: false,
285 max_tokens: None,
286 };
287
288 let config = Config {
289 output: Some("from_config.md".to_string()),
290 filter: Some(vec!["rs".to_string(), "toml".to_string()]),
291 ignore: Some(vec!["target".to_string()]),
292 line_numbers: Some(true),
293 preview: Some(true),
294 token_count: Some(true),
295 yes: Some(true),
296 diff_only: Some(true),
297 ..Default::default()
298 };
299
300 let resolution = resolve_final_config(args, Some(config));
301
302 assert_eq!(resolution.config.output, "from_config.md");
303 assert_eq!(
304 resolution.config.filter,
305 vec!["rs".to_string(), "toml".to_string()]
306 );
307 assert_eq!(resolution.config.ignore, vec!["target".to_string()]);
308 assert!(resolution.config.line_numbers);
309 assert!(resolution.config.preview);
310 assert!(resolution.config.token_count);
311 assert!(resolution.config.yes);
312 assert!(resolution.config.diff_only);
313 }
314
315 #[test]
316 fn test_timestamped_output_resolution() {
317 let args = Args {
318 input: "src".to_string(),
319 output: "test.md".to_string(),
320 filter: vec![],
321 ignore: vec![],
322 line_numbers: false,
323 preview: false,
324 token_count: false,
325 yes: false,
326 diff_only: false,
327 clear_cache: false,
328 init: false,
329 max_tokens: None,
330 };
331
332 let config = Config {
333 timestamped_output: Some(true),
334 ..Default::default()
335 };
336
337 let resolution = resolve_final_config(args, Some(config));
338
339 assert!(resolution.config.output.starts_with("test_"));
341 assert!(resolution.config.output.ends_with(".md"));
342 assert!(resolution.config.output.len() > "test_.md".len());
343 }
344
345 #[test]
346 fn test_output_folder_resolution() {
347 let args = Args {
348 input: "src".to_string(),
349 output: "test.md".to_string(),
350 filter: vec![],
351 ignore: vec![],
352 line_numbers: false,
353 preview: false,
354 token_count: false,
355 yes: false,
356 diff_only: false,
357 clear_cache: false,
358 init: false,
359 max_tokens: None,
360 };
361
362 let config = Config {
363 output_folder: Some("docs".to_string()),
364 ..Default::default()
365 };
366
367 let resolution = resolve_final_config(args, Some(config));
368
369 assert!(resolution.config.output.contains("docs"));
370 assert!(resolution.config.output.ends_with("test.md"));
371 }
372
373 #[test]
374 fn test_output_folder_with_timestamping() {
375 let args = Args {
376 input: "src".to_string(),
377 output: "test.md".to_string(),
378 filter: vec![],
379 ignore: vec![],
380 line_numbers: false,
381 preview: false,
382 token_count: false,
383 yes: false,
384 diff_only: false,
385 clear_cache: false,
386 init: false,
387 max_tokens: None,
388 };
389
390 let config = Config {
391 output_folder: Some("docs".to_string()),
392 timestamped_output: Some(true),
393 ..Default::default()
394 };
395
396 let resolution = resolve_final_config(args, Some(config));
397
398 assert!(resolution.config.output.contains("docs"));
399 assert!(resolution.config.output.contains("test_"));
400 assert!(resolution.config.output.ends_with(".md"));
401 }
402
403 #[test]
404 fn test_auto_diff_without_timestamping_warning() {
405 let args = Args {
406 input: "src".to_string(),
407 output: "test.md".to_string(),
408 filter: vec![],
409 ignore: vec![],
410 line_numbers: false,
411 preview: false,
412 token_count: false,
413 yes: false,
414 diff_only: false,
415 clear_cache: false,
416 init: false,
417 max_tokens: None,
418 };
419
420 let config = Config {
421 auto_diff: Some(true),
422 timestamped_output: Some(false), ..Default::default()
424 };
425
426 let resolution = resolve_final_config(args, Some(config));
427
428 assert!(!resolution.warnings.is_empty());
429 assert!(resolution.warnings[0].contains("auto_diff"));
430 assert!(resolution.warnings[0].contains("timestamped_output"));
431 }
432
433 #[test]
434 fn test_no_config_uses_cli_defaults() {
435 let args = Args {
436 input: "src".to_string(),
437 output: "output.md".to_string(),
438 filter: vec![],
439 ignore: vec![],
440 line_numbers: false,
441 preview: false,
442 token_count: false,
443 yes: false,
444 diff_only: false,
445 clear_cache: false,
446 init: false,
447 max_tokens: None,
448 };
449
450 let resolution = resolve_final_config(args.clone(), None);
451
452 assert_eq!(resolution.config.input, args.input);
453 assert_eq!(resolution.config.output, args.output);
454 assert_eq!(resolution.config.filter, args.filter);
455 assert_eq!(resolution.config.ignore, args.ignore);
456 assert_eq!(resolution.config.line_numbers, args.line_numbers);
457 assert_eq!(resolution.config.preview, args.preview);
458 assert_eq!(resolution.config.token_count, args.token_count);
459 assert_eq!(resolution.config.yes, args.yes);
460 assert_eq!(resolution.config.diff_only, args.diff_only);
461 assert!(!resolution.config.auto_diff);
462 assert_eq!(resolution.config.diff_context_lines, 3);
463 assert!(resolution.warnings.is_empty());
464 }
465}