Skip to main content

redact_api/
models.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 redact_core::AnonymizationStrategy;
6use serde::{Deserialize, Serialize};
7
8/// Request to analyze text for PII entities
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct AnalyzeRequest {
11    /// Text to analyze
12    pub text: String,
13
14    /// Language code (e.g., "en", "es")
15    #[serde(default = "default_language")]
16    pub language: String,
17
18    /// Specific entity types to detect (optional)
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub entities: Option<Vec<String>>,
21
22    /// Minimum confidence threshold
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub min_score: Option<f32>,
25}
26
27fn default_language() -> String {
28    "en".to_string()
29}
30
31/// Response from analyze endpoint
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct AnalyzeResponse {
34    /// Original text (if requested)
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub original_text: Option<String>,
37
38    /// Detected entities
39    pub results: Vec<EntityResult>,
40
41    /// Metadata about the analysis
42    pub metadata: AnalysisMetadata,
43}
44
45/// A detected entity in the text
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct EntityResult {
48    /// Type of entity
49    pub entity_type: String,
50
51    /// Start position
52    pub start: usize,
53
54    /// End position
55    pub end: usize,
56
57    /// Confidence score (0.0 to 1.0)
58    pub score: f32,
59
60    /// The detected text
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub text: Option<String>,
63
64    /// Recognizer that detected this entity
65    pub recognizer_name: String,
66}
67
68/// Metadata about the analysis
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct AnalysisMetadata {
71    /// Number of recognizers used
72    pub recognizers_used: usize,
73
74    /// Processing time in milliseconds
75    pub processing_time_ms: u64,
76
77    /// Language analyzed
78    pub language: String,
79
80    /// Model version (if NER was used)
81    #[serde(skip_serializing_if = "Option::is_none")]
82    pub model_version: Option<String>,
83}
84
85/// Request to anonymize text
86#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct AnonymizeRequest {
88    /// Text to anonymize
89    pub text: String,
90
91    /// Language code
92    #[serde(default = "default_language")]
93    pub language: String,
94
95    /// Anonymization configuration
96    #[serde(default)]
97    pub config: AnonymizationConfig,
98
99    /// Specific entity types to anonymize (optional)
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub entities: Option<Vec<String>>,
102}
103
104/// Anonymization configuration
105#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct AnonymizationConfig {
107    /// Strategy to use
108    #[serde(default)]
109    pub strategy: AnonymizationStrategy,
110
111    /// Masking character (for mask strategy)
112    #[serde(default = "default_mask_char")]
113    pub mask_char: String,
114
115    /// Characters to show at start (for mask strategy)
116    #[serde(default)]
117    pub mask_start_chars: usize,
118
119    /// Characters to show at end (for mask strategy)
120    #[serde(default)]
121    pub mask_end_chars: usize,
122
123    /// Preserve format (for mask strategy)
124    #[serde(default)]
125    pub preserve_format: bool,
126
127    /// Encryption key (for encrypt strategy)
128    #[serde(skip_serializing_if = "Option::is_none")]
129    pub encryption_key: Option<String>,
130
131    /// Hash salt (for hash strategy)
132    #[serde(skip_serializing_if = "Option::is_none")]
133    pub hash_salt: Option<String>,
134}
135
136impl Default for AnonymizationConfig {
137    fn default() -> Self {
138        Self {
139            strategy: AnonymizationStrategy::Replace,
140            mask_char: default_mask_char(),
141            mask_start_chars: 0,
142            mask_end_chars: 0,
143            preserve_format: false,
144            encryption_key: None,
145            hash_salt: None,
146        }
147    }
148}
149
150fn default_mask_char() -> String {
151    "*".to_string()
152}
153
154/// Response from anonymize endpoint
155#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct AnonymizeResponse {
157    /// Anonymized text
158    pub text: String,
159
160    /// Entities that were anonymized
161    pub results: Vec<EntityResult>,
162
163    /// Tokens for reversible anonymization
164    #[serde(skip_serializing_if = "Option::is_none")]
165    pub tokens: Option<Vec<TokenInfo>>,
166
167    /// Metadata
168    pub metadata: AnalysisMetadata,
169}
170
171/// Token information for reversible anonymization
172#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct TokenInfo {
174    /// Token identifier
175    pub token_id: String,
176
177    /// Entity type
178    pub entity_type: String,
179
180    /// Start position in anonymized text
181    pub start: usize,
182
183    /// End position in anonymized text
184    pub end: usize,
185
186    /// Expiration timestamp (if applicable)
187    #[serde(skip_serializing_if = "Option::is_none")]
188    pub expires_at: Option<String>,
189}
190
191/// Health check response
192#[derive(Debug, Clone, Serialize, Deserialize)]
193pub struct HealthResponse {
194    pub status: String,
195    pub version: String,
196    /// Number of recognizer instances (e.g. pattern, NER)
197    pub recognizers: usize,
198    /// Number of entity types supported across all recognizers (e.g. 36+)
199    pub entity_types: usize,
200}
201
202/// Error response
203#[derive(Debug, Clone, Serialize, Deserialize)]
204pub struct ErrorResponse {
205    pub error: String,
206    pub message: String,
207}
208
209impl ErrorResponse {
210    pub fn new(error: impl Into<String>, message: impl Into<String>) -> Self {
211        Self {
212            error: error.into(),
213            message: message.into(),
214        }
215    }
216}
217
218// Conversion helpers
219impl From<redact_core::RecognizerResult> for EntityResult {
220    fn from(result: redact_core::RecognizerResult) -> Self {
221        Self {
222            entity_type: result.entity_type.as_str().to_string(),
223            start: result.start,
224            end: result.end,
225            score: result.score,
226            text: result.text,
227            recognizer_name: result.recognizer_name,
228        }
229    }
230}
231
232impl From<redact_core::AnalysisMetadata> for AnalysisMetadata {
233    fn from(metadata: redact_core::AnalysisMetadata) -> Self {
234        Self {
235            recognizers_used: metadata.recognizers_used,
236            processing_time_ms: metadata.processing_time_ms,
237            language: metadata.language,
238            model_version: metadata.model_version,
239        }
240    }
241}
242
243impl From<redact_core::Token> for TokenInfo {
244    fn from(token: redact_core::Token) -> Self {
245        Self {
246            token_id: token.token_id,
247            entity_type: token.entity_type.as_str().to_string(),
248            start: token.start,
249            end: token.end,
250            expires_at: token.expires_at.map(|dt| dt.to_rfc3339()),
251        }
252    }
253}