Skip to main content

redact_core/anonymizers/
replace.rs

1// Copyright 2026 Censgate LLC.
2// Licensed under the Apache License, Version 2.0. See the LICENSE file
3// in the project root for license information.
4
5use super::{apply_anonymization, Anonymizer, AnonymizerConfig};
6use crate::types::{AnonymizedResult, RecognizerResult};
7use anyhow::Result;
8use std::collections::HashMap;
9
10/// Simple replacement anonymizer
11#[derive(Debug, Clone)]
12pub struct ReplaceAnonymizer {
13    custom_replacements: HashMap<String, String>,
14}
15
16impl ReplaceAnonymizer {
17    pub fn new() -> Self {
18        Self {
19            custom_replacements: HashMap::new(),
20        }
21    }
22
23    /// Add a custom replacement for a specific entity type
24    pub fn with_replacement(
25        mut self,
26        entity_type: impl Into<String>,
27        replacement: impl Into<String>,
28    ) -> Self {
29        self.custom_replacements
30            .insert(entity_type.into(), replacement.into());
31        self
32    }
33}
34
35impl Default for ReplaceAnonymizer {
36    fn default() -> Self {
37        Self::new()
38    }
39}
40
41impl Anonymizer for ReplaceAnonymizer {
42    fn name(&self) -> &str {
43        "ReplaceAnonymizer"
44    }
45
46    fn anonymize(
47        &self,
48        text: &str,
49        entities: Vec<RecognizerResult>,
50        _config: &AnonymizerConfig,
51    ) -> Result<AnonymizedResult> {
52        let anonymized_text = apply_anonymization(text, &entities, |entity, _original| {
53            // Check for custom replacement
54            if let Some(replacement) = self.custom_replacements.get(entity.entity_type.as_str()) {
55                replacement.clone()
56            } else {
57                entity.entity_type.default_replacement()
58            }
59        });
60
61        Ok(AnonymizedResult {
62            text: anonymized_text,
63            entities,
64            tokens: None,
65        })
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72    use crate::types::EntityType;
73
74    #[test]
75    fn test_replace_anonymizer() {
76        let anonymizer = ReplaceAnonymizer::new();
77        let text = "Email: john@example.com";
78        let entities = vec![RecognizerResult::new(
79            EntityType::EmailAddress,
80            7,
81            23,
82            0.9,
83            "test",
84        )];
85        let config = AnonymizerConfig::default();
86
87        let result = anonymizer.anonymize(text, entities, &config).unwrap();
88
89        assert_eq!(result.text, "Email: [EMAIL_ADDRESS]");
90    }
91
92    #[test]
93    fn test_replace_with_custom() {
94        let anonymizer =
95            ReplaceAnonymizer::new().with_replacement("EMAIL_ADDRESS", "[REDACTED_EMAIL]");
96
97        let text = "Email: john@example.com";
98        let entities = vec![RecognizerResult::new(
99            EntityType::EmailAddress,
100            7,
101            23,
102            0.9,
103            "test",
104        )];
105        let config = AnonymizerConfig::default();
106
107        let result = anonymizer.anonymize(text, entities, &config).unwrap();
108
109        assert_eq!(result.text, "Email: [REDACTED_EMAIL]");
110    }
111
112    #[test]
113    fn test_replace_multiple() {
114        let anonymizer = ReplaceAnonymizer::new();
115        let text = "Email: john@example.com, Phone: 555-1234";
116        let entities = vec![
117            RecognizerResult::new(EntityType::EmailAddress, 7, 23, 0.9, "test"),
118            RecognizerResult::new(EntityType::PhoneNumber, 32, 40, 0.8, "test"), // Fixed positions
119        ];
120        let config = AnonymizerConfig::default();
121
122        let result = anonymizer.anonymize(text, entities, &config).unwrap();
123
124        assert_eq!(result.text, "Email: [EMAIL_ADDRESS], Phone: [PHONE_NUMBER]");
125    }
126}