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