use regex::Regex;
use std::fs;
use std::io::{self, Read};
use std::path::Path;
#[derive(Clone)]
pub enum ReplaceMode {
Regex(Regex),
Literal(String),
}
#[derive(Clone)]
pub struct ReplacementOperation {
mode: ReplaceMode,
replacement: String,
}
impl ReplacementOperation {
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),
}
}
}
#[derive(Clone)]
pub struct TextReplacer {
operations: Vec<ReplacementOperation>,
}
impl TextReplacer {
pub fn builder() -> TextReplacerBuilder {
TextReplacerBuilder::default()
}
pub fn replace(&self, input: &str) -> String {
let mut result = input.to_string();
for op in &self.operations {
result = op.apply(&result);
}
result
}
pub fn replace_file<P: AsRef<Path>>(&self, path: P) -> io::Result<String> {
let mut file = fs::File::open(path)?;
let mut content = String::new();
file.read_to_string(&mut content)?;
Ok(self.replace(&content))
}
pub fn replace_file_in_place<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
let content = self.replace_file(&path)?;
fs::write(path, content)?;
Ok(())
}
pub fn replace_file_to<P1: AsRef<Path>, P2: AsRef<Path>>(
&self,
input_path: P1,
output_path: P2,
) -> io::Result<()> {
let content = self.replace_file(&input_path)?;
fs::write(output_path, content)?;
Ok(())
}
}
#[derive(Default, Clone)]
pub struct TextReplacerBuilder {
operations: Vec<ReplacementOperation>,
pattern: Option<String>,
replacement: Option<String>,
use_regex: bool,
case_insensitive: bool,
}
impl TextReplacerBuilder {
pub fn pattern(mut self, pat: &str) -> Self {
self.pattern = Some(pat.to_string());
self
}
pub fn replacement(mut self, rep: &str) -> Self {
self.replacement = Some(rep.to_string());
self
}
pub fn regex(mut self, yes: bool) -> Self {
self.use_regex = yes;
self
}
pub fn case_insensitive(mut self, yes: bool) -> Self {
self.case_insensitive = yes;
self
}
pub fn and(mut self) -> Self {
self.add_current_operation();
self
}
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;
self.use_regex = false;
self.case_insensitive = false;
let mode = if use_regex {
let mut regex_pattern = 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, }
} else {
if case_insensitive {
return false; }
ReplaceMode::Literal(pattern)
};
self.operations
.push(ReplacementOperation { mode, replacement });
true
} else {
false
}
}
pub fn build(mut self) -> Result<TextReplacer, String> {
self.add_current_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<()> {
let mut temp_file = NamedTempFile::new()?;
writeln!(temp_file, "foo bar foo baz")?;
temp_file.as_file_mut().flush()?;
let replacer = TextReplacer::builder()
.pattern("foo")
.replacement("qux")
.build()
.unwrap();
let result = replacer.replace_file(temp_file.path())?;
assert_eq!(result, "qux bar qux baz\n");
replacer.replace_file_in_place(temp_file.path())?;
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");
let output_file = NamedTempFile::new()?;
replacer.replace_file_to(temp_file.path(), output_file.path())?;
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(())
}
}