Skip to main content

reformat_core/
converter.rs

1//! Case converter implementation for file processing
2
3use crate::case::CaseFormat;
4use regex::Regex;
5use std::fs;
6use std::path::Path;
7use walkdir::WalkDir;
8
9/// Main converter for transforming case formats in files
10pub struct CaseConverter {
11    from_format: CaseFormat,
12    to_format: CaseFormat,
13    file_extensions: Vec<String>,
14    recursive: bool,
15    dry_run: bool,
16    prefix: String,
17    suffix: String,
18    strip_prefix: Option<String>,
19    strip_suffix: Option<String>,
20    replace_prefix_from: Option<String>,
21    replace_prefix_to: Option<String>,
22    replace_suffix_from: Option<String>,
23    replace_suffix_to: Option<String>,
24    glob_pattern: Option<glob::Pattern>,
25    word_filter: Option<Regex>,
26    source_pattern: Regex,
27}
28
29impl CaseConverter {
30    /// Creates a new case converter
31    #[allow(clippy::too_many_arguments)]
32    pub fn new(
33        from_format: CaseFormat,
34        to_format: CaseFormat,
35        file_extensions: Option<Vec<String>>,
36        recursive: bool,
37        dry_run: bool,
38        prefix: String,
39        suffix: String,
40        strip_prefix: Option<String>,
41        strip_suffix: Option<String>,
42        replace_prefix_from: Option<String>,
43        replace_prefix_to: Option<String>,
44        replace_suffix_from: Option<String>,
45        replace_suffix_to: Option<String>,
46        glob_pattern: Option<String>,
47        word_filter: Option<String>,
48    ) -> crate::Result<Self> {
49        let file_extensions = file_extensions.unwrap_or_else(|| {
50            [
51                ".c", ".h", ".py", ".md", ".js", ".ts", ".java", ".cpp", ".hpp",
52            ]
53            .iter()
54            .map(|s| s.to_string())
55            .collect()
56        });
57
58        let source_pattern = Regex::new(from_format.pattern())?;
59        let glob_pattern = match glob_pattern {
60            Some(pattern) => Some(glob::Pattern::new(&pattern)?),
61            None => None,
62        };
63        let word_filter = match word_filter {
64            Some(pattern) => Some(Regex::new(&pattern)?),
65            None => None,
66        };
67
68        Ok(CaseConverter {
69            from_format,
70            to_format,
71            file_extensions,
72            recursive,
73            dry_run,
74            prefix,
75            suffix,
76            strip_prefix,
77            strip_suffix,
78            replace_prefix_from,
79            replace_prefix_to,
80            replace_suffix_from,
81            replace_suffix_to,
82            glob_pattern,
83            word_filter,
84            source_pattern,
85        })
86    }
87
88    /// Converts a single identifier
89    fn convert(&self, name: &str) -> String {
90        let mut processed_name = name.to_string();
91
92        // Step 1: Strip prefix if specified
93        if let Some(ref strip_pfx) = self.strip_prefix {
94            if processed_name.starts_with(strip_pfx) {
95                processed_name = processed_name[strip_pfx.len()..].to_string();
96            }
97        }
98
99        // Step 2: Strip suffix if specified
100        if let Some(ref strip_sfx) = self.strip_suffix {
101            if processed_name.ends_with(strip_sfx) {
102                processed_name =
103                    processed_name[..processed_name.len() - strip_sfx.len()].to_string();
104            }
105        }
106
107        // Step 3: Replace prefix if specified
108        if let (Some(ref from_pfx), Some(ref to_pfx)) =
109            (&self.replace_prefix_from, &self.replace_prefix_to)
110        {
111            if processed_name.starts_with(from_pfx) {
112                processed_name = format!("{}{}", to_pfx, &processed_name[from_pfx.len()..]);
113            }
114        }
115
116        // Step 4: Replace suffix if specified
117        if let (Some(ref from_sfx), Some(ref to_sfx)) =
118            (&self.replace_suffix_from, &self.replace_suffix_to)
119        {
120            if processed_name.ends_with(from_sfx) {
121                processed_name = format!(
122                    "{}{}",
123                    &processed_name[..processed_name.len() - from_sfx.len()],
124                    to_sfx
125                );
126            }
127        }
128
129        // Step 5: Apply word filter if provided
130        if let Some(ref filter) = self.word_filter {
131            if !filter.is_match(&processed_name) {
132                return name.to_string(); // Return original if filter doesn't match
133            }
134        }
135
136        // Step 6: Apply case conversion
137        let words = self.from_format.split_words(&processed_name);
138
139        // Step 7: Add prefix/suffix (existing functionality)
140        self.to_format
141            .join_words(&words, &self.prefix, &self.suffix)
142    }
143
144    /// Checks if a file matches the glob pattern
145    fn matches_glob(&self, filepath: &Path, base_path: &Path) -> bool {
146        if let Some(ref pattern) = self.glob_pattern {
147            // Match against the filename
148            if let Some(filename) = filepath.file_name() {
149                if pattern.matches(filename.to_string_lossy().as_ref()) {
150                    return true;
151                }
152            }
153
154            // Also try matching against the full relative path
155            if let Ok(rel_path) = filepath.strip_prefix(base_path) {
156                if pattern.matches_path(rel_path) {
157                    return true;
158                }
159            }
160
161            false
162        } else {
163            true
164        }
165    }
166
167    /// Processes a single file
168    pub fn process_file(&self, filepath: &Path, base_path: &Path) -> crate::Result<()> {
169        // Check file extension
170        let extension = filepath
171            .extension()
172            .and_then(|e| e.to_str())
173            .map(|e| format!(".{}", e));
174
175        if let Some(ext) = extension {
176            if !self.file_extensions.contains(&ext) {
177                return Ok(());
178            }
179        } else {
180            return Ok(());
181        }
182
183        // Check glob pattern
184        if !self.matches_glob(filepath, base_path) {
185            return Ok(());
186        }
187
188        // Read file content
189        let content = fs::read_to_string(filepath)?;
190
191        // Replace all matches of the source pattern
192        let modified_content = self
193            .source_pattern
194            .replace_all(&content, |caps: &regex::Captures| self.convert(&caps[0]));
195
196        if content != modified_content {
197            if self.dry_run {
198                println!("Would convert '{}'", filepath.display());
199            } else {
200                fs::write(filepath, modified_content.as_ref())?;
201                println!("Converted '{}'", filepath.display());
202            }
203        } else if !self.dry_run {
204            println!("No changes needed in '{}'", filepath.display());
205        }
206
207        Ok(())
208    }
209
210    /// Processes a directory or file
211    pub fn process_directory(&self, directory_path: &Path) -> crate::Result<()> {
212        if !directory_path.exists() {
213            eprintln!("Path '{}' does not exist.", directory_path.display());
214            return Ok(());
215        }
216
217        // If it's a single file, process it directly
218        if directory_path.is_file() {
219            if let Some(parent) = directory_path.parent() {
220                self.process_file(directory_path, parent)?;
221            } else {
222                self.process_file(directory_path, Path::new("."))?;
223            }
224            return Ok(());
225        }
226
227        // Otherwise, process directory
228        if !directory_path.is_dir() {
229            eprintln!(
230                "Path '{}' is not a directory or file.",
231                directory_path.display()
232            );
233            return Ok(());
234        }
235
236        if self.recursive {
237            for entry in WalkDir::new(directory_path)
238                .into_iter()
239                .filter_map(|e| e.ok())
240            {
241                if entry.file_type().is_file() {
242                    if let Err(e) = self.process_file(entry.path(), directory_path) {
243                        eprintln!("Error processing file '{}': {}", entry.path().display(), e);
244                    }
245                }
246            }
247        } else {
248            for entry in fs::read_dir(directory_path)? {
249                let entry = entry?;
250                let path = entry.path();
251                if path.is_file() {
252                    if let Err(e) = self.process_file(&path, directory_path) {
253                        eprintln!("Error processing file '{}': {}", path.display(), e);
254                    }
255                }
256            }
257        }
258
259        Ok(())
260    }
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266
267    #[test]
268    fn test_camel_to_snake() {
269        let words = CaseFormat::CamelCase.split_words("firstName");
270        assert_eq!(words, vec!["first", "name"]);
271        assert_eq!(
272            CaseFormat::SnakeCase.join_words(&words, "", ""),
273            "first_name"
274        );
275    }
276
277    #[test]
278    fn test_snake_to_camel() {
279        let words = CaseFormat::SnakeCase.split_words("first_name");
280        assert_eq!(words, vec!["first", "name"]);
281        assert_eq!(
282            CaseFormat::CamelCase.join_words(&words, "", ""),
283            "firstName"
284        );
285    }
286
287    #[test]
288    fn test_pascal_to_kebab() {
289        let words = CaseFormat::PascalCase.split_words("FirstName");
290        assert_eq!(words, vec!["first", "name"]);
291        assert_eq!(
292            CaseFormat::KebabCase.join_words(&words, "", ""),
293            "first-name"
294        );
295    }
296
297    #[test]
298    fn test_kebab_to_screaming_snake() {
299        let words = CaseFormat::KebabCase.split_words("first-name");
300        assert_eq!(words, vec!["first", "name"]);
301        assert_eq!(
302            CaseFormat::ScreamingSnakeCase.join_words(&words, "", ""),
303            "FIRST_NAME"
304        );
305    }
306
307    #[test]
308    fn test_camel_pattern_match() {
309        let pattern = Regex::new(CaseFormat::CamelCase.pattern()).unwrap();
310        assert!(pattern.is_match("firstName"));
311        assert!(pattern.is_match("myVariableName"));
312        assert!(!pattern.is_match("firstname"));
313        assert!(!pattern.is_match("FirstName")); // PascalCase, not camelCase
314    }
315
316    #[test]
317    fn test_pascal_pattern_match() {
318        let pattern = Regex::new(CaseFormat::PascalCase.pattern()).unwrap();
319        assert!(pattern.is_match("FirstName"));
320        assert!(pattern.is_match("MyVariableName"));
321        assert!(!pattern.is_match("firstName")); // camelCase, not PascalCase
322        assert!(!pattern.is_match("FIRSTNAME")); // Not PascalCase
323    }
324
325    #[test]
326    fn test_snake_pattern_match() {
327        let pattern = Regex::new(CaseFormat::SnakeCase.pattern()).unwrap();
328        assert!(pattern.is_match("first_name"));
329        assert!(pattern.is_match("my_variable_name"));
330        assert!(!pattern.is_match("firstname"));
331        assert!(!pattern.is_match("FIRST_NAME")); // SCREAMING_SNAKE_CASE
332    }
333}