1use crate::Error;
26
27pub trait Sanitizer {
33 fn sanitize(&self, error: &Error) -> Error;
41}
42
43pub trait PatternTransform {
48 fn apply(&self, input: &str) -> String;
50}
51
52pub struct SimplePattern {
54 pub pattern: String,
56 pub replacement: String,
58}
59
60impl SimplePattern {
61 pub fn new(pattern: impl Into<String>, replacement: impl Into<String>) -> Self {
62 Self {
63 pattern: pattern.into(),
64 replacement: replacement.into(),
65 }
66 }
67}
68
69impl PatternTransform for SimplePattern {
70 fn apply(&self, input: &str) -> String {
71 input.replace(&self.pattern, &self.replacement)
72 }
73}
74
75pub struct CaseInsensitivePattern {
77 pub pattern: String,
79 pub replacement: String,
81}
82
83impl CaseInsensitivePattern {
84 pub fn new(pattern: impl Into<String>, replacement: impl Into<String>) -> Self {
85 Self {
86 pattern: pattern.into(),
87 replacement: replacement.into(),
88 }
89 }
90}
91
92impl PatternTransform for CaseInsensitivePattern {
93 fn apply(&self, input: &str) -> String {
94 let pattern_lower = self.pattern.to_lowercase();
95 let input_lower = input.to_lowercase();
96
97 if let Some(pos) = input_lower.find(&pattern_lower) {
98 let mut result = input.to_owned();
99 #[allow(clippy::arithmetic_side_effects)]
100 result.replace_range(pos..pos + self.pattern.len(), &self.replacement);
101
102 if result.to_lowercase().contains(&pattern_lower) {
104 return self.apply(&result);
105 }
106 result
107 } else {
108 input.to_owned()
109 }
110 }
111}
112
113pub struct CompositeTransform {
115 transforms: Vec<Box<dyn PatternTransform + Send + Sync>>,
116}
117
118impl CompositeTransform {
119 #[must_use]
120 pub fn new() -> Self {
121 Self {
122 transforms: Vec::new(),
123 }
124 }
125
126 #[must_use]
127 pub fn add_transform<T: PatternTransform + Send + Sync + 'static>(
128 mut self,
129 transform: T,
130 ) -> Self {
131 self.transforms.push(Box::new(transform));
132 self
133 }
134}
135
136impl Default for CompositeTransform {
137 fn default() -> Self {
138 Self::new()
139 }
140}
141
142impl PatternTransform for CompositeTransform {
143 fn apply(&self, input: &str) -> String {
144 self.transforms
145 .iter()
146 .fold(input.to_owned(), |acc, transform| transform.apply(&acc))
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[test]
155 fn test_simple_pattern() {
156 let pattern = SimplePattern::new("password", "[REDACTED]");
157 let result = pattern.apply("The password is secret123");
158 assert!(result.contains("[REDACTED]"));
159 assert!(!result.contains("password"));
160 }
161
162 #[test]
163 fn test_case_insensitive_pattern() {
164 let pattern = CaseInsensitivePattern::new("password", "[REDACTED]");
165 let result = pattern.apply("The PASSWORD is secret123");
166 assert!(result.contains("[REDACTED]"));
167 }
168
169 #[test]
170 fn test_composite_transform() {
171 let composite = CompositeTransform::new()
172 .add_transform(SimplePattern::new("password", "[REDACTED]"))
173 .add_transform(SimplePattern::new("token", "[REDACTED]"));
174
175 let result = composite.apply("password is secret, token is abc123");
176 assert!(result.contains("[REDACTED]"));
177 assert!(!result.contains("password"));
178 assert!(!result.contains("token"));
179 }
180
181 #[test]
182 fn test_simple_pattern_no_match() {
183 let pattern = SimplePattern::new("password", "[REDACTED]");
184 let result = pattern.apply("This text has no sensitive data");
185 assert_eq!(result, "This text has no sensitive data");
186 }
187
188 #[test]
189 fn test_simple_pattern_multiple_occurrences() {
190 let pattern = SimplePattern::new("key", "[REDACTED]");
191 let result = pattern.apply("key1, key2, key3");
192 assert_eq!(result.matches("[REDACTED]").count(), 3);
193 }
194
195 #[test]
196 fn test_case_insensitive_pattern_various_cases() {
197 let pattern = CaseInsensitivePattern::new("secret", "[REDACTED]");
198
199 let result1 = pattern.apply("SECRET value");
200 assert!(result1.contains("[REDACTED]"));
201
202 let result2 = pattern.apply("Secret value");
203 assert!(result2.contains("[REDACTED]"));
204
205 let result3 = pattern.apply("secret value");
206 assert!(result3.contains("[REDACTED]"));
207 }
208
209 #[test]
210 fn test_case_insensitive_pattern_no_match() {
211 let pattern = CaseInsensitivePattern::new("password", "[REDACTED]");
212 let result = pattern.apply("No sensitive data here");
213 assert_eq!(result, "No sensitive data here");
214 }
215
216 #[test]
217 fn test_case_insensitive_pattern_multiple_occurrences() {
218 let pattern = CaseInsensitivePattern::new("pass", "[REDACTED]");
219 let result = pattern.apply("pass PASS Pass");
220 assert!(result.contains("[REDACTED]"));
222 }
223
224 #[test]
225 fn test_composite_transform_empty() {
226 let composite = CompositeTransform::new();
227 let result = composite.apply("test string");
228 assert_eq!(result, "test string");
229 }
230
231 #[test]
232 fn test_composite_transform_default() {
233 let composite = CompositeTransform::default();
234 let result = composite.apply("test");
235 assert_eq!(result, "test");
236 }
237
238 #[test]
239 fn test_composite_transform_single() {
240 let composite = CompositeTransform::new().add_transform(SimplePattern::new("test", "TEST"));
241
242 let result = composite.apply("test string");
243 assert_eq!(result, "TEST string");
244 }
245
246 #[test]
247 fn test_composite_transform_chained() {
248 let composite = CompositeTransform::new()
249 .add_transform(SimplePattern::new("a", "b"))
250 .add_transform(SimplePattern::new("b", "c"))
251 .add_transform(SimplePattern::new("c", "d"));
252
253 let result = composite.apply("a");
254 assert_eq!(result, "d");
255 }
256
257 #[test]
258 fn test_pattern_transform_trait() {
259 let pattern: Box<dyn PatternTransform> = Box::new(SimplePattern::new("test", "replaced"));
260 let result = pattern.apply("test value");
261 assert_eq!(result, "replaced value");
262 }
263}