1use regex::Regex;
2use std::fs;
3use std::io::{self, Read};
4use std::path::Path;
5
6#[derive(Clone)]
8pub enum ReplaceMode {
9 Regex(Regex),
11 Literal(String),
13}
14
15#[derive(Clone)]
17pub struct ReplacementOperation {
18 mode: ReplaceMode,
19 replacement: String,
20}
21
22impl ReplacementOperation {
23 fn apply(&self, input: &str) -> String {
25 match &self.mode {
26 ReplaceMode::Regex(re) => re.replace_all(input, self.replacement.as_str()).to_string(),
27 ReplaceMode::Literal(search) => input.replace(search, &self.replacement),
28 }
29 }
30}
31
32#[derive(Clone)]
35pub struct TextReplacer {
36 operations: Vec<ReplacementOperation>,
37}
38
39impl TextReplacer {
40 pub fn builder() -> TextReplacerBuilder {
42 TextReplacerBuilder::default()
43 }
44
45 pub fn replace(&self, input: &str) -> String {
47 let mut result = input.to_string();
48
49 for op in &self.operations {
51 result = op.apply(&result);
52 }
53
54 result
55 }
56
57 pub fn replace_file<P: AsRef<Path>>(&self, path: P) -> io::Result<String> {
59 let mut file = fs::File::open(path)?;
60 let mut content = String::new();
61 file.read_to_string(&mut content)?;
62
63 Ok(self.replace(&content))
64 }
65
66 pub fn replace_file_in_place<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
68 let content = self.replace_file(&path)?;
69 fs::write(path, content)?;
70 Ok(())
71 }
72
73 pub fn replace_file_to<P1: AsRef<Path>, P2: AsRef<Path>>(
75 &self,
76 input_path: P1,
77 output_path: P2,
78 ) -> io::Result<()> {
79 let content = self.replace_file(&input_path)?;
80 fs::write(output_path, content)?;
81 Ok(())
82 }
83}
84
85#[derive(Default, Clone)]
87pub struct TextReplacerBuilder {
88 operations: Vec<ReplacementOperation>,
89 pattern: Option<String>,
90 replacement: Option<String>,
91 use_regex: bool,
92 case_insensitive: bool,
93}
94
95impl TextReplacerBuilder {
96 pub fn pattern(mut self, pat: &str) -> Self {
98 self.pattern = Some(pat.to_string());
99 self
100 }
101
102 pub fn replacement(mut self, rep: &str) -> Self {
104 self.replacement = Some(rep.to_string());
105 self
106 }
107
108 pub fn regex(mut self, yes: bool) -> Self {
110 self.use_regex = yes;
111 self
112 }
113
114 pub fn case_insensitive(mut self, yes: bool) -> Self {
116 self.case_insensitive = yes;
117 self
118 }
119
120 pub fn and(mut self) -> Self {
122 self.add_current_operation();
123 self
124 }
125
126 fn add_current_operation(&mut self) -> bool {
128 if let Some(pattern) = self.pattern.take() {
129 let replacement = self.replacement.take().unwrap_or_default();
130 let use_regex = self.use_regex;
131 let case_insensitive = self.case_insensitive;
132
133 self.use_regex = false;
135 self.case_insensitive = false;
136
137 let mode = if use_regex {
139 let mut regex_pattern = pattern;
140
141 if case_insensitive && !regex_pattern.starts_with("(?i)") {
143 regex_pattern = format!("(?i){}", regex_pattern);
144 }
145
146 match Regex::new(®ex_pattern) {
147 Ok(re) => ReplaceMode::Regex(re),
148 Err(_) => return false, }
150 } else {
151 if case_insensitive {
154 return false; }
156 ReplaceMode::Literal(pattern)
157 };
158
159 self.operations
160 .push(ReplacementOperation { mode, replacement });
161
162 true
163 } else {
164 false
165 }
166 }
167
168 pub fn build(mut self) -> Result<TextReplacer, String> {
170 self.add_current_operation();
172
173 if self.operations.is_empty() {
175 return Err("No replacement operations configured".to_string());
176 }
177
178 Ok(TextReplacer {
179 operations: self.operations,
180 })
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187 use std::io::{Seek, SeekFrom, Write};
188 use tempfile::NamedTempFile;
189
190 #[test]
191 fn test_regex_replace() {
192 let replacer = TextReplacer::builder()
193 .pattern(r"\bfoo\b")
194 .replacement("bar")
195 .regex(true)
196 .build()
197 .unwrap();
198
199 let input = "foo bar foo baz";
200 let output = replacer.replace(input);
201
202 assert_eq!(output, "bar bar bar baz");
203 }
204
205 #[test]
206 fn test_literal_replace() {
207 let replacer = TextReplacer::builder()
208 .pattern("foo")
209 .replacement("qux")
210 .regex(false)
211 .build()
212 .unwrap();
213
214 let input = "foo bar foo baz";
215 let output = replacer.replace(input);
216
217 assert_eq!(output, "qux bar qux baz");
218 }
219
220 #[test]
221 fn test_multiple_replacements() {
222 let replacer = TextReplacer::builder()
223 .pattern("foo")
224 .replacement("qux")
225 .and()
226 .pattern("bar")
227 .replacement("baz")
228 .build()
229 .unwrap();
230
231 let input = "foo bar foo";
232 let output = replacer.replace(input);
233
234 assert_eq!(output, "qux baz qux");
235 }
236
237 #[test]
238 fn test_case_insensitive_regex() {
239 let replacer = TextReplacer::builder()
240 .pattern("foo")
241 .replacement("bar")
242 .regex(true)
243 .case_insensitive(true)
244 .build()
245 .unwrap();
246
247 let input = "FOO foo Foo";
248 let output = replacer.replace(input);
249
250 assert_eq!(output, "bar bar bar");
251 }
252
253 #[test]
254 fn test_file_operations() -> io::Result<()> {
255 let mut temp_file = NamedTempFile::new()?;
257 writeln!(temp_file, "foo bar foo baz")?;
258
259 temp_file.as_file_mut().flush()?;
261
262 let replacer = TextReplacer::builder()
263 .pattern("foo")
264 .replacement("qux")
265 .build()
266 .unwrap();
267
268 let result = replacer.replace_file(temp_file.path())?;
270 assert_eq!(result, "qux bar qux baz\n");
271
272 replacer.replace_file_in_place(temp_file.path())?;
274
275 let mut content = String::new();
277 temp_file.as_file_mut().seek(SeekFrom::Start(0))?;
278 temp_file.as_file_mut().read_to_string(&mut content)?;
279 assert_eq!(content, "qux bar qux baz\n");
280
281 let output_file = NamedTempFile::new()?;
283 replacer.replace_file_to(temp_file.path(), output_file.path())?;
284
285 let mut output_content = String::new();
287 fs::File::open(output_file.path())?.read_to_string(&mut output_content)?;
288 assert_eq!(output_content, "qux bar qux baz\n");
289
290 Ok(())
291 }
292}