use serde::{Deserialize, Serialize};
use serde_json::Value;
pub const PII_MASK: &str = "***";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FieldClassification {
pub pii: bool,
pub safe_for_console: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub retention_days: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub purpose: Option<String>,
}
#[must_use]
pub fn mask_field_value(classification: &FieldClassification, value: &Value) -> String {
if classification.pii || !classification.safe_for_console {
return PII_MASK.to_string();
}
match value {
Value::String(s) => s.clone(),
other => other.to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn classification_roundtrip() {
let c = FieldClassification {
pii: true,
safe_for_console: false,
retention_days: Some(30),
purpose: Some("debug".to_string()),
};
let json = serde_json::to_string(&c).expect("serialize");
let back: FieldClassification = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back, c);
}
#[test]
fn mask_pii_hides_value() {
let c = FieldClassification {
pii: true,
safe_for_console: true,
retention_days: None,
purpose: None,
};
assert_eq!(mask_field_value(&c, &json!("secret")), PII_MASK);
}
#[test]
fn mask_unsafe_console_hides_value() {
let c = FieldClassification {
pii: false,
safe_for_console: false,
retention_days: None,
purpose: None,
};
assert_eq!(mask_field_value(&c, &json!(42)), PII_MASK);
}
#[test]
fn mask_safe_passthrough() {
let c = FieldClassification {
pii: false,
safe_for_console: true,
retention_days: None,
purpose: None,
};
assert_eq!(mask_field_value(&c, &json!("ok")), "ok");
assert_eq!(mask_field_value(&c, &json!(1)), "1");
}
}