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