use std::fs;
use std::path::{Path, PathBuf};
use walkdir::WalkDir;
use crate::{
CaseTransform, EmojiOptions, EmojiTransformer, FileRenamer, RenameOptions, WhitespaceCleaner,
WhitespaceOptions,
};
#[derive(Debug, Clone)]
pub struct CombinedOptions {
pub recursive: bool,
pub dry_run: bool,
}
impl Default for CombinedOptions {
fn default() -> Self {
CombinedOptions {
recursive: true,
dry_run: false,
}
}
}
#[derive(Debug, Default)]
pub struct CombinedStats {
pub files_renamed: usize,
pub files_emoji_transformed: usize,
pub emoji_changes: usize,
pub files_whitespace_cleaned: usize,
pub whitespace_lines_cleaned: usize,
}
pub struct CombinedProcessor {
options: CombinedOptions,
rename_options: RenameOptions,
emoji_options: EmojiOptions,
whitespace_options: WhitespaceOptions,
}
impl CombinedProcessor {
pub fn new(options: CombinedOptions) -> Self {
let rename_options = RenameOptions {
case_transform: CaseTransform::Lowercase,
recursive: options.recursive,
dry_run: options.dry_run,
..Default::default()
};
let emoji_options = EmojiOptions {
recursive: options.recursive,
dry_run: options.dry_run,
..Default::default()
};
let whitespace_options = WhitespaceOptions {
recursive: options.recursive,
dry_run: options.dry_run,
..Default::default()
};
CombinedProcessor {
options,
rename_options,
emoji_options,
whitespace_options,
}
}
pub fn with_defaults() -> Self {
CombinedProcessor::new(CombinedOptions::default())
}
pub fn process(&self, path: &Path) -> crate::Result<CombinedStats> {
let mut stats = CombinedStats::default();
if path.is_file() {
self.process_single_file(path, &mut stats)?;
} else if path.is_dir() {
if self.options.recursive {
let mut files: Vec<PathBuf> = WalkDir::new(path)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.file_type().is_file())
.map(|e| e.path().to_path_buf())
.collect();
files.sort_by_key(|b| std::cmp::Reverse(b.components().count()));
for file_path in files {
self.process_single_file(&file_path, &mut stats)?;
}
} else {
let mut files: Vec<PathBuf> = fs::read_dir(path)?
.filter_map(|e| e.ok())
.map(|e| e.path())
.filter(|p| p.is_file())
.collect();
files.sort();
for file_path in files {
self.process_single_file(&file_path, &mut stats)?;
}
}
}
Ok(stats)
}
fn process_single_file(&self, path: &Path, stats: &mut CombinedStats) -> crate::Result<()> {
let renamer = FileRenamer::new(self.rename_options.clone());
let renamed = renamer.rename_file(path, false)?;
if renamed {
stats.files_renamed += 1;
}
let current_path = if renamed && !self.options.dry_run {
let file_name = path
.file_name()
.and_then(|n| n.to_str())
.ok_or_else(|| anyhow::anyhow!("Invalid filename"))?;
let lowercase_name = file_name.to_lowercase();
let parent = path
.parent()
.ok_or_else(|| anyhow::anyhow!("No parent directory"))?;
parent.join(lowercase_name)
} else {
path.to_path_buf()
};
let emoji_transformer = EmojiTransformer::new(self.emoji_options.clone());
let emoji_changes = emoji_transformer.transform_file(¤t_path)?;
if emoji_changes > 0 {
stats.files_emoji_transformed += 1;
stats.emoji_changes += emoji_changes;
}
let whitespace_cleaner = WhitespaceCleaner::new(self.whitespace_options.clone());
let lines_cleaned = whitespace_cleaner.clean_file(¤t_path)?;
if lines_cleaned > 0 {
stats.files_whitespace_cleaned += 1;
stats.whitespace_lines_cleaned += lines_cleaned;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[test]
fn test_combined_processing() {
let test_dir = std::env::temp_dir().join("reformat_combined_test");
let _ = fs::remove_dir_all(&test_dir);
fs::create_dir_all(&test_dir).unwrap();
let test_file = test_dir.join("TestFile.txt");
fs::write(&test_file, "Line 1 \nTask done ✅\nLine 3\t\n").unwrap();
let processor = CombinedProcessor::with_defaults();
let stats = processor.process(&test_file).unwrap();
assert_eq!(stats.files_renamed, 1);
let renamed_file = test_dir.join("testfile.txt");
assert!(renamed_file.exists());
assert_eq!(stats.files_emoji_transformed, 1);
let content = fs::read_to_string(&renamed_file).unwrap();
assert!(content.contains("[x]"));
assert!(!content.contains("✅"));
assert_eq!(stats.files_whitespace_cleaned, 1);
assert!(!content.contains(" \n"));
assert!(!content.contains("\t\n"));
fs::remove_dir_all(&test_dir).unwrap();
}
#[test]
fn test_combined_dry_run() {
let test_dir = std::env::temp_dir().join("reformat_combined_dry");
let _ = fs::remove_dir_all(&test_dir);
fs::create_dir_all(&test_dir).unwrap();
let test_file = test_dir.join("TestFile.txt");
let original_content = "Line 1 \nTask ✅\n";
fs::write(&test_file, original_content).unwrap();
let mut options = CombinedOptions::default();
options.dry_run = true;
let processor = CombinedProcessor::new(options);
let _stats = processor.process(&test_file).unwrap();
assert!(test_file.exists());
let content = fs::read_to_string(&test_file).unwrap();
assert_eq!(content, original_content);
fs::remove_dir_all(&test_dir).unwrap();
}
#[test]
fn test_combined_recursive() {
let test_dir = std::env::temp_dir().join("reformat_combined_recursive");
let _ = fs::remove_dir_all(&test_dir);
fs::create_dir_all(&test_dir).unwrap();
let sub_dir = test_dir.join("subdir");
fs::create_dir_all(&sub_dir).unwrap();
let file1 = test_dir.join("File1.txt");
let file2 = sub_dir.join("File2.md");
fs::write(&file1, "Text \n✅ Done\n").unwrap();
fs::write(&file2, "More text\t\n☐ Todo\n").unwrap();
let processor = CombinedProcessor::with_defaults();
let stats = processor.process(&test_dir).unwrap();
assert_eq!(stats.files_renamed, 2);
assert_eq!(stats.files_emoji_transformed, 2);
assert_eq!(stats.files_whitespace_cleaned, 2);
assert!(test_dir.join("file1.txt").exists());
assert!(sub_dir.join("file2.md").exists());
fs::remove_dir_all(&test_dir).unwrap();
}
#[test]
fn test_combined_non_recursive() {
let test_dir = std::env::temp_dir().join("reformat_combined_nonrec");
let _ = fs::remove_dir_all(&test_dir);
fs::create_dir_all(&test_dir).unwrap();
let sub_dir = test_dir.join("subdir");
fs::create_dir_all(&sub_dir).unwrap();
let file1 = test_dir.join("File1.txt");
let file2 = sub_dir.join("File2.txt");
fs::write(&file1, "Text \n").unwrap();
fs::write(&file2, "More \n").unwrap();
let mut options = CombinedOptions::default();
options.recursive = false;
let processor = CombinedProcessor::new(options);
let stats = processor.process(&test_dir).unwrap();
assert_eq!(stats.files_renamed, 1);
assert!(test_dir.join("file1.txt").exists());
let entries: Vec<_> = fs::read_dir(&sub_dir).unwrap().collect();
assert_eq!(entries.len(), 1);
let actual_name = entries[0].as_ref().unwrap().file_name();
assert_eq!(
actual_name.to_str().unwrap(),
"File2.txt",
"Subdirectory file should not be renamed"
);
fs::remove_dir_all(&test_dir).unwrap();
}
}