use crate::case::CaseFormat;
use regex::Regex;
use std::fs;
use std::path::Path;
use walkdir::WalkDir;
pub struct CaseConverter {
from_format: CaseFormat,
to_format: CaseFormat,
file_extensions: Vec<String>,
recursive: bool,
dry_run: bool,
prefix: String,
suffix: String,
strip_prefix: Option<String>,
strip_suffix: Option<String>,
replace_prefix_from: Option<String>,
replace_prefix_to: Option<String>,
replace_suffix_from: Option<String>,
replace_suffix_to: Option<String>,
glob_pattern: Option<glob::Pattern>,
word_filter: Option<Regex>,
source_pattern: Regex,
}
impl CaseConverter {
#[allow(clippy::too_many_arguments)]
pub fn new(
from_format: CaseFormat,
to_format: CaseFormat,
file_extensions: Option<Vec<String>>,
recursive: bool,
dry_run: bool,
prefix: String,
suffix: String,
strip_prefix: Option<String>,
strip_suffix: Option<String>,
replace_prefix_from: Option<String>,
replace_prefix_to: Option<String>,
replace_suffix_from: Option<String>,
replace_suffix_to: Option<String>,
glob_pattern: Option<String>,
word_filter: Option<String>,
) -> crate::Result<Self> {
let file_extensions = file_extensions.unwrap_or_else(|| {
[
".c", ".h", ".py", ".md", ".js", ".ts", ".java", ".cpp", ".hpp",
]
.iter()
.map(|s| s.to_string())
.collect()
});
let source_pattern = Regex::new(from_format.pattern())?;
let glob_pattern = match glob_pattern {
Some(pattern) => Some(glob::Pattern::new(&pattern)?),
None => None,
};
let word_filter = match word_filter {
Some(pattern) => Some(Regex::new(&pattern)?),
None => None,
};
Ok(CaseConverter {
from_format,
to_format,
file_extensions,
recursive,
dry_run,
prefix,
suffix,
strip_prefix,
strip_suffix,
replace_prefix_from,
replace_prefix_to,
replace_suffix_from,
replace_suffix_to,
glob_pattern,
word_filter,
source_pattern,
})
}
fn convert(&self, name: &str) -> String {
let mut processed_name = name.to_string();
if let Some(ref strip_pfx) = self.strip_prefix {
if processed_name.starts_with(strip_pfx) {
processed_name = processed_name[strip_pfx.len()..].to_string();
}
}
if let Some(ref strip_sfx) = self.strip_suffix {
if processed_name.ends_with(strip_sfx) {
processed_name =
processed_name[..processed_name.len() - strip_sfx.len()].to_string();
}
}
if let (Some(ref from_pfx), Some(ref to_pfx)) =
(&self.replace_prefix_from, &self.replace_prefix_to)
{
if processed_name.starts_with(from_pfx) {
processed_name = format!("{}{}", to_pfx, &processed_name[from_pfx.len()..]);
}
}
if let (Some(ref from_sfx), Some(ref to_sfx)) =
(&self.replace_suffix_from, &self.replace_suffix_to)
{
if processed_name.ends_with(from_sfx) {
processed_name = format!(
"{}{}",
&processed_name[..processed_name.len() - from_sfx.len()],
to_sfx
);
}
}
if let Some(ref filter) = self.word_filter {
if !filter.is_match(&processed_name) {
return name.to_string(); }
}
let words = self.from_format.split_words(&processed_name);
self.to_format
.join_words(&words, &self.prefix, &self.suffix)
}
fn matches_glob(&self, filepath: &Path, base_path: &Path) -> bool {
if let Some(ref pattern) = self.glob_pattern {
if let Some(filename) = filepath.file_name() {
if pattern.matches(filename.to_string_lossy().as_ref()) {
return true;
}
}
if let Ok(rel_path) = filepath.strip_prefix(base_path) {
if pattern.matches_path(rel_path) {
return true;
}
}
false
} else {
true
}
}
pub fn process_file(&self, filepath: &Path, base_path: &Path) -> crate::Result<()> {
let extension = filepath
.extension()
.and_then(|e| e.to_str())
.map(|e| format!(".{}", e));
if let Some(ext) = extension {
if !self.file_extensions.contains(&ext) {
return Ok(());
}
} else {
return Ok(());
}
if !self.matches_glob(filepath, base_path) {
return Ok(());
}
let content = fs::read_to_string(filepath)?;
let modified_content = self
.source_pattern
.replace_all(&content, |caps: ®ex::Captures| self.convert(&caps[0]));
if content != modified_content {
if self.dry_run {
println!("Would convert '{}'", filepath.display());
} else {
fs::write(filepath, modified_content.as_ref())?;
println!("Converted '{}'", filepath.display());
}
} else if !self.dry_run {
println!("No changes needed in '{}'", filepath.display());
}
Ok(())
}
pub fn process_directory(&self, directory_path: &Path) -> crate::Result<()> {
if !directory_path.exists() {
eprintln!("Path '{}' does not exist.", directory_path.display());
return Ok(());
}
if directory_path.is_file() {
if let Some(parent) = directory_path.parent() {
self.process_file(directory_path, parent)?;
} else {
self.process_file(directory_path, Path::new("."))?;
}
return Ok(());
}
if !directory_path.is_dir() {
eprintln!(
"Path '{}' is not a directory or file.",
directory_path.display()
);
return Ok(());
}
if self.recursive {
for entry in WalkDir::new(directory_path)
.into_iter()
.filter_map(|e| e.ok())
{
if entry.file_type().is_file() {
if let Err(e) = self.process_file(entry.path(), directory_path) {
eprintln!("Error processing file '{}': {}", entry.path().display(), e);
}
}
}
} else {
for entry in fs::read_dir(directory_path)? {
let entry = entry?;
let path = entry.path();
if path.is_file() {
if let Err(e) = self.process_file(&path, directory_path) {
eprintln!("Error processing file '{}': {}", path.display(), e);
}
}
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_camel_to_snake() {
let words = CaseFormat::CamelCase.split_words("firstName");
assert_eq!(words, vec!["first", "name"]);
assert_eq!(
CaseFormat::SnakeCase.join_words(&words, "", ""),
"first_name"
);
}
#[test]
fn test_snake_to_camel() {
let words = CaseFormat::SnakeCase.split_words("first_name");
assert_eq!(words, vec!["first", "name"]);
assert_eq!(
CaseFormat::CamelCase.join_words(&words, "", ""),
"firstName"
);
}
#[test]
fn test_pascal_to_kebab() {
let words = CaseFormat::PascalCase.split_words("FirstName");
assert_eq!(words, vec!["first", "name"]);
assert_eq!(
CaseFormat::KebabCase.join_words(&words, "", ""),
"first-name"
);
}
#[test]
fn test_kebab_to_screaming_snake() {
let words = CaseFormat::KebabCase.split_words("first-name");
assert_eq!(words, vec!["first", "name"]);
assert_eq!(
CaseFormat::ScreamingSnakeCase.join_words(&words, "", ""),
"FIRST_NAME"
);
}
#[test]
fn test_camel_pattern_match() {
let pattern = Regex::new(CaseFormat::CamelCase.pattern()).unwrap();
assert!(pattern.is_match("firstName"));
assert!(pattern.is_match("myVariableName"));
assert!(!pattern.is_match("firstname"));
assert!(!pattern.is_match("FirstName")); }
#[test]
fn test_pascal_pattern_match() {
let pattern = Regex::new(CaseFormat::PascalCase.pattern()).unwrap();
assert!(pattern.is_match("FirstName"));
assert!(pattern.is_match("MyVariableName"));
assert!(!pattern.is_match("firstName")); assert!(!pattern.is_match("FIRSTNAME")); }
#[test]
fn test_snake_pattern_match() {
let pattern = Regex::new(CaseFormat::SnakeCase.pattern()).unwrap();
assert!(pattern.is_match("first_name"));
assert!(pattern.is_match("my_variable_name"));
assert!(!pattern.is_match("firstname"));
assert!(!pattern.is_match("FIRST_NAME")); }
}