use regex::Regex; use std::fs; use std::io::{self, Read}; use std::path::Path; /// Represents the type of replacement to perform. #[derive(Clone)] pub enum ReplaceMode { /// Regex-based replacement using the `regex` crate Regex(Regex), /// Literal substring replacement (non-regex) Literal(String), } /// A single replacement operation with a pattern and replacement text #[derive(Clone)] pub struct ReplacementOperation { mode: ReplaceMode, replacement: String, } impl ReplacementOperation { /// Applies this replacement operation to the input text fn apply(&self, input: &str) -> String { match &self.mode { ReplaceMode::Regex(re) => re.replace_all(input, self.replacement.as_str()).to_string(), ReplaceMode::Literal(search) => input.replace(search, &self.replacement), } } } /// Text replacer that can perform multiple replacement operations /// in a single pass over the input text. #[derive(Clone)] pub struct TextReplacer { operations: Vec, } impl TextReplacer { /// Creates a new builder for configuring a TextReplacer pub fn builder() -> TextReplacerBuilder { TextReplacerBuilder::default() } /// Applies all configured replacement operations to the input text pub fn replace(&self, input: &str) -> String { let mut result = input.to_string(); // Apply each replacement operation in sequence for op in &self.operations { result = op.apply(&result); } result } /// Reads a file, applies all replacements, and returns the result as a string pub fn replace_file>(&self, path: P) -> io::Result { let mut file = fs::File::open(path)?; let mut content = String::new(); file.read_to_string(&mut content)?; Ok(self.replace(&content)) } /// Reads a file, applies all replacements, and writes the result back to the file pub fn replace_file_in_place>(&self, path: P) -> io::Result<()> { let content = self.replace_file(&path)?; fs::write(path, content)?; Ok(()) } /// Reads a file, applies all replacements, and writes the result to a new file pub fn replace_file_to, P2: AsRef>( &self, input_path: P1, output_path: P2, ) -> io::Result<()> { let content = self.replace_file(&input_path)?; fs::write(output_path, content)?; Ok(()) } } /// Builder for the TextReplacer. #[derive(Default, Clone)] pub struct TextReplacerBuilder { operations: Vec, pattern: Option, replacement: Option, use_regex: bool, case_insensitive: bool, } impl TextReplacerBuilder { /// Sets the pattern to search for pub fn pattern(mut self, pat: &str) -> Self { self.pattern = Some(pat.to_string()); self } /// Sets the replacement text pub fn replacement(mut self, rep: &str) -> Self { self.replacement = Some(rep.to_string()); self } /// Sets whether to use regex pub fn regex(mut self, yes: bool) -> Self { self.use_regex = yes; self } /// Sets whether the replacement should be case-insensitive pub fn case_insensitive(mut self, yes: bool) -> Self { self.case_insensitive = yes; self } /// Adds another replacement operation to the chain and resets the builder for a new operation pub fn and(mut self) -> Self { self.add_current_operation(); self } // Helper method to add the current operation to the list fn add_current_operation(&mut self) -> bool { if let Some(pattern) = self.pattern.take() { let replacement = self.replacement.take().unwrap_or_default(); let use_regex = self.use_regex; let case_insensitive = self.case_insensitive; // Reset current settings self.use_regex = false; self.case_insensitive = false; // Create the replacement mode let mode = if use_regex { let mut regex_pattern = pattern; // If case insensitive, add the flag to the regex pattern if case_insensitive && !regex_pattern.starts_with("(?i)") { regex_pattern = format!("(?i){}", regex_pattern); } match Regex::new(®ex_pattern) { Ok(re) => ReplaceMode::Regex(re), Err(_) => return false, // Failed to compile regex } } else { // For literal replacement, we'll handle case insensitivity differently // since String::replace doesn't have a case-insensitive option if case_insensitive { return false; // Case insensitive not supported for literal } ReplaceMode::Literal(pattern) }; self.operations .push(ReplacementOperation { mode, replacement }); true } else { false } } /// Builds the TextReplacer with all configured replacement operations pub fn build(mut self) -> Result { // If there's a pending replacement operation, add it self.add_current_operation(); // Ensure we have at least one replacement operation if self.operations.is_empty() { return Err("No replacement operations configured".to_string()); } Ok(TextReplacer { operations: self.operations, }) } } #[cfg(test)] mod tests { use super::*; use std::io::{Seek, SeekFrom, Write}; use tempfile::NamedTempFile; #[test] fn test_regex_replace() { let replacer = TextReplacer::builder() .pattern(r"\bfoo\b") .replacement("bar") .regex(true) .build() .unwrap(); let input = "foo bar foo baz"; let output = replacer.replace(input); assert_eq!(output, "bar bar bar baz"); } #[test] fn test_literal_replace() { let replacer = TextReplacer::builder() .pattern("foo") .replacement("qux") .regex(false) .build() .unwrap(); let input = "foo bar foo baz"; let output = replacer.replace(input); assert_eq!(output, "qux bar qux baz"); } #[test] fn test_multiple_replacements() { let replacer = TextReplacer::builder() .pattern("foo") .replacement("qux") .and() .pattern("bar") .replacement("baz") .build() .unwrap(); let input = "foo bar foo"; let output = replacer.replace(input); assert_eq!(output, "qux baz qux"); } #[test] fn test_case_insensitive_regex() { let replacer = TextReplacer::builder() .pattern("foo") .replacement("bar") .regex(true) .case_insensitive(true) .build() .unwrap(); let input = "FOO foo Foo"; let output = replacer.replace(input); assert_eq!(output, "bar bar bar"); } #[test] fn test_file_operations() -> io::Result<()> { // Create a temporary file let mut temp_file = NamedTempFile::new()?; writeln!(temp_file, "foo bar foo baz")?; // Flush the file to ensure content is written temp_file.as_file_mut().flush()?; let replacer = TextReplacer::builder() .pattern("foo") .replacement("qux") .build() .unwrap(); // Test replace_file let result = replacer.replace_file(temp_file.path())?; assert_eq!(result, "qux bar qux baz\n"); // Test replace_file_in_place replacer.replace_file_in_place(temp_file.path())?; // Verify the file was updated - need to seek to beginning of file first let mut content = String::new(); temp_file.as_file_mut().seek(SeekFrom::Start(0))?; temp_file.as_file_mut().read_to_string(&mut content)?; assert_eq!(content, "qux bar qux baz\n"); // Test replace_file_to with a new temporary file let output_file = NamedTempFile::new()?; replacer.replace_file_to(temp_file.path(), output_file.path())?; // Verify the output file has the replaced content let mut output_content = String::new(); fs::File::open(output_file.path())?.read_to_string(&mut output_content)?; assert_eq!(output_content, "qux bar qux baz\n"); Ok(()) } }