1use crate::case::CaseFormat;
4use regex::Regex;
5use std::fs;
6use std::path::Path;
7use walkdir::WalkDir;
8
9pub 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 #[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 fn convert(&self, name: &str) -> String {
90 let mut processed_name = name.to_string();
91
92 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 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 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 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 if let Some(ref filter) = self.word_filter {
131 if !filter.is_match(&processed_name) {
132 return name.to_string(); }
134 }
135
136 let words = self.from_format.split_words(&processed_name);
138
139 self.to_format
141 .join_words(&words, &self.prefix, &self.suffix)
142 }
143
144 fn matches_glob(&self, filepath: &Path, base_path: &Path) -> bool {
146 if let Some(ref pattern) = self.glob_pattern {
147 if let Some(filename) = filepath.file_name() {
149 if pattern.matches(filename.to_string_lossy().as_ref()) {
150 return true;
151 }
152 }
153
154 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 pub fn process_file(&self, filepath: &Path, base_path: &Path) -> crate::Result<()> {
169 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 if !self.matches_glob(filepath, base_path) {
185 return Ok(());
186 }
187
188 let content = fs::read_to_string(filepath)?;
190
191 let modified_content = self
193 .source_pattern
194 .replace_all(&content, |caps: ®ex::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 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 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 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")); }
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")); assert!(!pattern.is_match("FIRSTNAME")); }
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")); }
333}