use regex::Regex; use serde::{Deserialize, Serialize}; use crate::{NominaError, RenameContext, RenameRule}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RegexRule { pub pattern: String, pub replace_with: String, pub case_insensitive: bool, pub enabled: bool, } impl RegexRule { pub fn new() -> Self { Self { pattern: String::new(), replace_with: String::new(), case_insensitive: false, enabled: true, } } fn build_regex(&self) -> std::result::Result { let pat = if self.case_insensitive { format!("(?i){}", self.pattern) } else { self.pattern.clone() }; Regex::new(&pat).map_err(|e| NominaError::InvalidRegex { pattern: self.pattern.clone(), reason: e.to_string(), }) } } impl RenameRule for RegexRule { fn apply(&self, filename: &str, _context: &RenameContext) -> String { if self.pattern.is_empty() { return filename.to_string(); } match self.build_regex() { Ok(re) => re.replace_all(filename, self.replace_with.as_str()).into_owned(), Err(_) => filename.to_string(), } } fn display_name(&self) -> &str { "Regex" } fn rule_type(&self) -> &str { "regex" } fn is_enabled(&self) -> bool { self.enabled } } #[cfg(test)] mod tests { use super::*; #[test] fn basic_regex() { let rule = RegexRule { pattern: r"(\d+)".into(), replace_with: "NUM".into(), case_insensitive: false, enabled: true, }; let ctx = RenameContext::dummy(0); assert_eq!(rule.apply("file123", &ctx), "fileNUM"); } #[test] fn capture_groups() { let rule = RegexRule { pattern: r"([a-z]+)-([a-z]+)".into(), replace_with: "${2}_${1}".into(), case_insensitive: false, enabled: true, }; let ctx = RenameContext::dummy(0); assert_eq!(rule.apply("hello-world", &ctx), "world_hello"); } #[test] fn empty_pattern() { let rule = RegexRule { pattern: String::new(), replace_with: "x".into(), case_insensitive: false, enabled: true, }; let ctx = RenameContext::dummy(0); assert_eq!(rule.apply("test", &ctx), "test"); } }