use crate::Result;
use crate::types::redaction::{PiiCategory, RedactionStrategy};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "api", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "alef-meta", alef(since = "5.0.0"))]
pub struct RedactionConfig {
#[serde(default)]
#[cfg_attr(feature = "api", schema(value_type = Vec<PiiCategory>))]
pub categories: HashSet<PiiCategory>,
#[serde(default)]
pub strategy: RedactionStrategy,
#[serde(skip_serializing_if = "Option::is_none")]
pub ner: Option<super::ner::NerConfig>,
#[serde(default = "default_preserve_offsets")]
pub preserve_offsets: bool,
#[serde(default)]
pub custom_terms: Vec<RedactionTerm>,
#[serde(default)]
pub custom_patterns: Vec<RedactionPattern>,
}
fn default_preserve_offsets() -> bool {
true
}
fn default_case_sensitive() -> bool {
false
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "api", derive(utoipa::ToSchema))]
pub struct RedactionTerm {
pub label: String,
pub value: String,
#[serde(default = "default_case_sensitive")]
pub case_sensitive: bool,
}
impl RedactionTerm {
pub fn literal(value: impl Into<String>) -> Self {
let v = value.into();
Self {
label: v.clone(),
value: v,
case_sensitive: false,
}
}
pub fn labeled(label: impl Into<String>, value: impl Into<String>) -> Self {
Self {
label: label.into(),
value: value.into(),
case_sensitive: false,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "api", derive(utoipa::ToSchema))]
pub struct RedactionPattern {
pub label: String,
pub pattern: String,
#[serde(default = "default_case_sensitive")]
pub case_sensitive: bool,
}
impl RedactionPattern {
pub fn labeled(label: impl Into<String>, pattern: impl Into<String>) -> Self {
Self {
label: label.into(),
pattern: pattern.into(),
case_sensitive: false,
}
}
}
impl Default for RedactionConfig {
fn default() -> Self {
Self {
categories: HashSet::new(),
strategy: RedactionStrategy::default(),
ner: None,
preserve_offsets: true,
custom_terms: Vec::new(),
custom_patterns: Vec::new(),
}
}
}
impl RedactionConfig {
pub fn validate(&self) -> Result<()> {
for term in &self.custom_terms {
if term.value.is_empty() {
return Err(crate::XbergError::validation(format!(
"RedactionConfig.custom_terms[{}]: value is empty",
term.label
)));
}
}
for pattern in &self.custom_patterns {
if pattern.pattern.is_empty() {
return Err(crate::XbergError::validation(format!(
"RedactionConfig.custom_patterns[{}]: pattern is empty",
pattern.label
)));
}
let compiled = if pattern.case_sensitive {
regex::Regex::new(&pattern.pattern)
} else {
regex::Regex::new(&format!("(?i){}", pattern.pattern))
};
if let Err(err) = compiled {
return Err(crate::XbergError::validation(format!(
"RedactionConfig.custom_patterns[{}]: invalid regex: {err}",
pattern.label
)));
}
}
Ok(())
}
}