reformat_core/
combined.rs1use std::fs;
4use std::path::{Path, PathBuf};
5use walkdir::WalkDir;
6
7use crate::{
8 CaseTransform, EmojiOptions, EmojiTransformer, FileRenamer, RenameOptions, WhitespaceCleaner,
9 WhitespaceOptions,
10};
11
12#[derive(Debug, Clone)]
14pub struct CombinedOptions {
15 pub recursive: bool,
17 pub dry_run: bool,
19}
20
21impl Default for CombinedOptions {
22 fn default() -> Self {
23 CombinedOptions {
24 recursive: true,
25 dry_run: false,
26 }
27 }
28}
29
30#[derive(Debug, Default)]
32pub struct CombinedStats {
33 pub files_renamed: usize,
35 pub files_emoji_transformed: usize,
37 pub emoji_changes: usize,
39 pub files_whitespace_cleaned: usize,
41 pub whitespace_lines_cleaned: usize,
43}
44
45pub struct CombinedProcessor {
47 options: CombinedOptions,
48 rename_options: RenameOptions,
49 emoji_options: EmojiOptions,
50 whitespace_options: WhitespaceOptions,
51}
52
53impl CombinedProcessor {
54 pub fn new(options: CombinedOptions) -> Self {
56 let rename_options = RenameOptions {
57 case_transform: CaseTransform::Lowercase,
58 recursive: options.recursive,
59 dry_run: options.dry_run,
60 ..Default::default()
61 };
62
63 let emoji_options = EmojiOptions {
64 recursive: options.recursive,
65 dry_run: options.dry_run,
66 ..Default::default()
67 };
68
69 let whitespace_options = WhitespaceOptions {
70 recursive: options.recursive,
71 dry_run: options.dry_run,
72 ..Default::default()
73 };
74
75 CombinedProcessor {
76 options,
77 rename_options,
78 emoji_options,
79 whitespace_options,
80 }
81 }
82
83 pub fn with_defaults() -> Self {
85 CombinedProcessor::new(CombinedOptions::default())
86 }
87
88 pub fn process(&self, path: &Path) -> crate::Result<CombinedStats> {
90 let mut stats = CombinedStats::default();
91
92 if path.is_file() {
93 self.process_single_file(path, &mut stats)?;
94 } else if path.is_dir() {
95 if self.options.recursive {
96 let mut files: Vec<PathBuf> = WalkDir::new(path)
98 .into_iter()
99 .filter_map(|e| e.ok())
100 .filter(|e| e.file_type().is_file())
101 .map(|e| e.path().to_path_buf())
102 .collect();
103
104 files.sort_by_key(|b| std::cmp::Reverse(b.components().count()));
106
107 for file_path in files {
108 self.process_single_file(&file_path, &mut stats)?;
109 }
110 } else {
111 let mut files: Vec<PathBuf> = fs::read_dir(path)?
112 .filter_map(|e| e.ok())
113 .map(|e| e.path())
114 .filter(|p| p.is_file())
115 .collect();
116
117 files.sort();
119
120 for file_path in files {
121 self.process_single_file(&file_path, &mut stats)?;
122 }
123 }
124 }
125
126 Ok(stats)
127 }
128
129 fn process_single_file(&self, path: &Path, stats: &mut CombinedStats) -> crate::Result<()> {
131 let renamer = FileRenamer::new(self.rename_options.clone());
134 let renamed = renamer.rename_file(path, false)?;
135 if renamed {
136 stats.files_renamed += 1;
137 }
138
139 let current_path = if renamed && !self.options.dry_run {
141 let file_name = path
143 .file_name()
144 .and_then(|n| n.to_str())
145 .ok_or_else(|| anyhow::anyhow!("Invalid filename"))?;
146
147 let lowercase_name = file_name.to_lowercase();
148 let parent = path
149 .parent()
150 .ok_or_else(|| anyhow::anyhow!("No parent directory"))?;
151 parent.join(lowercase_name)
152 } else {
153 path.to_path_buf()
154 };
155
156 let emoji_transformer = EmojiTransformer::new(self.emoji_options.clone());
158 let emoji_changes = emoji_transformer.transform_file(¤t_path)?;
159 if emoji_changes > 0 {
160 stats.files_emoji_transformed += 1;
161 stats.emoji_changes += emoji_changes;
162 }
163
164 let whitespace_cleaner = WhitespaceCleaner::new(self.whitespace_options.clone());
166 let lines_cleaned = whitespace_cleaner.clean_file(¤t_path)?;
167 if lines_cleaned > 0 {
168 stats.files_whitespace_cleaned += 1;
169 stats.whitespace_lines_cleaned += lines_cleaned;
170 }
171
172 Ok(())
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179 use std::fs;
180
181 #[test]
182 fn test_combined_processing() {
183 let test_dir = std::env::temp_dir().join("reformat_combined_test");
184 let _ = fs::remove_dir_all(&test_dir);
185 fs::create_dir_all(&test_dir).unwrap();
186
187 let test_file = test_dir.join("TestFile.txt");
189 fs::write(&test_file, "Line 1 \nTask done ✅\nLine 3\t\n").unwrap();
190
191 let processor = CombinedProcessor::with_defaults();
192 let stats = processor.process(&test_file).unwrap();
193
194 assert_eq!(stats.files_renamed, 1);
196 let renamed_file = test_dir.join("testfile.txt");
197 assert!(renamed_file.exists());
198
199 assert_eq!(stats.files_emoji_transformed, 1);
201 let content = fs::read_to_string(&renamed_file).unwrap();
202 assert!(content.contains("[x]"));
203 assert!(!content.contains("✅"));
204
205 assert_eq!(stats.files_whitespace_cleaned, 1);
207 assert!(!content.contains(" \n"));
208 assert!(!content.contains("\t\n"));
209
210 fs::remove_dir_all(&test_dir).unwrap();
211 }
212
213 #[test]
214 fn test_combined_dry_run() {
215 let test_dir = std::env::temp_dir().join("reformat_combined_dry");
216 let _ = fs::remove_dir_all(&test_dir);
217 fs::create_dir_all(&test_dir).unwrap();
218
219 let test_file = test_dir.join("TestFile.txt");
220 let original_content = "Line 1 \nTask ✅\n";
221 fs::write(&test_file, original_content).unwrap();
222
223 let mut options = CombinedOptions::default();
224 options.dry_run = true;
225
226 let processor = CombinedProcessor::new(options);
227 let _stats = processor.process(&test_file).unwrap();
228
229 assert!(test_file.exists());
231 let content = fs::read_to_string(&test_file).unwrap();
232 assert_eq!(content, original_content);
233
234 fs::remove_dir_all(&test_dir).unwrap();
235 }
236
237 #[test]
238 fn test_combined_recursive() {
239 let test_dir = std::env::temp_dir().join("reformat_combined_recursive");
240 let _ = fs::remove_dir_all(&test_dir);
241 fs::create_dir_all(&test_dir).unwrap();
242
243 let sub_dir = test_dir.join("subdir");
244 fs::create_dir_all(&sub_dir).unwrap();
245
246 let file1 = test_dir.join("File1.txt");
247 let file2 = sub_dir.join("File2.md");
248
249 fs::write(&file1, "Text \n✅ Done\n").unwrap();
250 fs::write(&file2, "More text\t\n☐ Todo\n").unwrap();
251
252 let processor = CombinedProcessor::with_defaults();
253 let stats = processor.process(&test_dir).unwrap();
254
255 assert_eq!(stats.files_renamed, 2);
257 assert_eq!(stats.files_emoji_transformed, 2);
258 assert_eq!(stats.files_whitespace_cleaned, 2);
259
260 assert!(test_dir.join("file1.txt").exists());
262 assert!(sub_dir.join("file2.md").exists());
263
264 fs::remove_dir_all(&test_dir).unwrap();
265 }
266
267 #[test]
268 fn test_combined_non_recursive() {
269 let test_dir = std::env::temp_dir().join("reformat_combined_nonrec");
270 let _ = fs::remove_dir_all(&test_dir);
271 fs::create_dir_all(&test_dir).unwrap();
272
273 let sub_dir = test_dir.join("subdir");
274 fs::create_dir_all(&sub_dir).unwrap();
275
276 let file1 = test_dir.join("File1.txt");
277 let file2 = sub_dir.join("File2.txt");
278
279 fs::write(&file1, "Text \n").unwrap();
280 fs::write(&file2, "More \n").unwrap();
281
282 let mut options = CombinedOptions::default();
283 options.recursive = false;
284
285 let processor = CombinedProcessor::new(options);
286 let stats = processor.process(&test_dir).unwrap();
287
288 assert_eq!(stats.files_renamed, 1);
290 assert!(test_dir.join("file1.txt").exists());
291
292 let entries: Vec<_> = fs::read_dir(&sub_dir).unwrap().collect();
295 assert_eq!(entries.len(), 1);
296 let actual_name = entries[0].as_ref().unwrap().file_name();
297 assert_eq!(
298 actual_name.to_str().unwrap(),
299 "File2.txt",
300 "Subdirectory file should not be renamed"
301 );
302
303 fs::remove_dir_all(&test_dir).unwrap();
304 }
305}