use briefcase_core::{PiiAnalysis, PiiType, SanitizationJsonResult, SanitizationResult, Sanitizer};
use std::collections::HashMap;
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub struct WasmSanitizer {
inner: Sanitizer,
}
impl Default for WasmSanitizer {
fn default() -> Self {
Self::new()
}
}
#[wasm_bindgen]
impl WasmSanitizer {
#[wasm_bindgen(constructor)]
pub fn new() -> WasmSanitizer {
WasmSanitizer {
inner: Sanitizer::new(),
}
}
#[wasm_bindgen(js_name = disabled)]
pub fn disabled() -> WasmSanitizer {
WasmSanitizer {
inner: Sanitizer::disabled(),
}
}
#[wasm_bindgen(js_name = addPattern)]
pub fn add_pattern(&mut self, name: &str, pattern: &str) -> Result<(), JsValue> {
self.inner
.add_pattern(name, pattern)
.map_err(|e| JsValue::from_str(&e.to_string()))
}
#[wasm_bindgen(js_name = removePattern)]
pub fn remove_pattern(&mut self, pii_type: &str) -> bool {
let pii_type_enum = match pii_type {
"ssn" => PiiType::Ssn,
"credit_card" => PiiType::CreditCard,
"email" => PiiType::Email,
"phone" => PiiType::Phone,
"api_key" => PiiType::ApiKey,
"ip_address" => PiiType::IpAddress,
custom => PiiType::Custom(custom.to_string()),
};
self.inner.remove_pattern(&pii_type_enum)
}
#[wasm_bindgen(js_name = setEnabled)]
pub fn set_enabled(&mut self, enabled: bool) {
self.inner.set_enabled(enabled);
}
#[wasm_bindgen(js_name = sanitize)]
pub fn sanitize(&self, text: &str) -> WasmSanitizationResult {
let result = self.inner.sanitize(text);
WasmSanitizationResult { inner: result }
}
#[wasm_bindgen(js_name = sanitizeJson)]
pub fn sanitize_json(&self, value: &JsValue) -> Result<WasmSanitizationJsonResult, JsValue> {
let json_value: serde_json::Value = serde_wasm_bindgen::from_value(value.clone())
.map_err(|e| JsValue::from_str(&format!("Failed to parse JSON: {}", e)))?;
let result = self.inner.sanitize_json(&json_value);
Ok(WasmSanitizationJsonResult { inner: result })
}
#[wasm_bindgen(js_name = containsPii)]
pub fn contains_pii(&self, text: &str) -> Result<JsValue, JsValue> {
let matches = self.inner.contains_pii(text);
let js_matches: Vec<serde_json::Value> = matches
.iter()
.map(|m| {
serde_json::json!({
"pii_type": pii_type_to_string(&m.pii_type),
"start": m.start,
"end": m.end,
})
})
.collect();
serde_wasm_bindgen::to_value(&js_matches)
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
}
#[wasm_bindgen(js_name = analyze)]
pub fn analyze(&self, text: &str) -> WasmPiiAnalysis {
let analysis = self.inner.analyze(text);
WasmPiiAnalysis { inner: analysis }
}
}
#[wasm_bindgen]
pub struct WasmSanitizationResult {
inner: SanitizationResult,
}
#[wasm_bindgen]
impl WasmSanitizationResult {
#[wasm_bindgen(getter)]
pub fn sanitized(&self) -> String {
self.inner.sanitized.clone()
}
#[wasm_bindgen(getter)]
pub fn redactions(&self) -> Result<JsValue, JsValue> {
let js_redactions: Vec<serde_json::Value> = self
.inner
.redactions
.iter()
.map(|r| {
serde_json::json!({
"pii_type": pii_type_to_string(&r.pii_type),
"original_length": r.original_length,
"start_position": r.start_position,
"end_position": r.end_position,
})
})
.collect();
serde_wasm_bindgen::to_value(&js_redactions)
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
}
#[wasm_bindgen(js_name = toObject)]
pub fn to_object(&self) -> Result<JsValue, JsValue> {
let redactions: Vec<serde_json::Value> = self
.inner
.redactions
.iter()
.map(|r| {
serde_json::json!({
"pii_type": pii_type_to_string(&r.pii_type),
"original_length": r.original_length,
"start_position": r.start_position,
"end_position": r.end_position,
})
})
.collect();
let obj = serde_json::json!({
"sanitized": self.inner.sanitized,
"redactions": redactions,
});
serde_wasm_bindgen::to_value(&obj)
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
}
}
#[wasm_bindgen]
pub struct WasmSanitizationJsonResult {
inner: SanitizationJsonResult,
}
#[wasm_bindgen]
impl WasmSanitizationJsonResult {
#[wasm_bindgen(getter)]
pub fn sanitized(&self) -> Result<JsValue, JsValue> {
serde_wasm_bindgen::to_value(&self.inner.sanitized)
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
}
#[wasm_bindgen(getter)]
pub fn redactions(&self) -> Result<JsValue, JsValue> {
let js_redactions: Vec<serde_json::Value> = self
.inner
.redactions
.iter()
.map(|r| {
serde_json::json!({
"path": r.path,
"pii_type": pii_type_to_string(&r.pii_type),
"original_length": r.original_length,
})
})
.collect();
serde_wasm_bindgen::to_value(&js_redactions)
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
}
#[wasm_bindgen(js_name = toObject)]
pub fn to_object(&self) -> Result<JsValue, JsValue> {
let redactions: Vec<serde_json::Value> = self
.inner
.redactions
.iter()
.map(|r| {
serde_json::json!({
"path": r.path,
"pii_type": pii_type_to_string(&r.pii_type),
"original_length": r.original_length,
})
})
.collect();
let obj = serde_json::json!({
"sanitized": self.inner.sanitized,
"redactions": redactions,
});
serde_wasm_bindgen::to_value(&obj)
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
}
}
#[wasm_bindgen]
pub struct WasmPiiAnalysis {
inner: PiiAnalysis,
}
#[wasm_bindgen]
impl WasmPiiAnalysis {
#[wasm_bindgen(getter)]
pub fn has_pii(&self) -> bool {
self.inner.has_pii
}
#[wasm_bindgen(getter)]
pub fn total_matches(&self) -> u32 {
self.inner.total_matches as u32
}
#[wasm_bindgen(getter)]
pub fn unique_types(&self) -> u32 {
self.inner.unique_types as u32
}
#[wasm_bindgen(getter)]
pub fn type_counts(&self) -> Result<JsValue, JsValue> {
let type_counts: HashMap<String, usize> = self
.inner
.type_counts
.iter()
.map(|(k, v)| (pii_type_to_string(k), *v))
.collect();
serde_wasm_bindgen::to_value(&type_counts)
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
}
#[wasm_bindgen(getter)]
pub fn matches(&self) -> Result<JsValue, JsValue> {
let js_matches: Vec<serde_json::Value> = self
.inner
.matches
.iter()
.map(|m| {
serde_json::json!({
"pii_type": pii_type_to_string(&m.pii_type),
"start": m.start,
"end": m.end,
})
})
.collect();
serde_wasm_bindgen::to_value(&js_matches)
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
}
#[wasm_bindgen(js_name = toObject)]
pub fn to_object(&self) -> Result<JsValue, JsValue> {
let type_counts: HashMap<String, usize> = self
.inner
.type_counts
.iter()
.map(|(k, v)| (pii_type_to_string(k), *v))
.collect();
let matches: Vec<serde_json::Value> = self
.inner
.matches
.iter()
.map(|m| {
serde_json::json!({
"pii_type": pii_type_to_string(&m.pii_type),
"start": m.start,
"end": m.end,
})
})
.collect();
let obj = serde_json::json!({
"has_pii": self.inner.has_pii,
"total_matches": self.inner.total_matches,
"unique_types": self.inner.unique_types,
"type_counts": type_counts,
"matches": matches,
});
serde_wasm_bindgen::to_value(&obj)
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
}
}
fn pii_type_to_string(pii_type: &PiiType) -> String {
match pii_type {
PiiType::Ssn => "ssn".to_string(),
PiiType::CreditCard => "credit_card".to_string(),
PiiType::Email => "email".to_string(),
PiiType::Phone => "phone".to_string(),
PiiType::ApiKey => "api_key".to_string(),
PiiType::IpAddress => "ip_address".to_string(),
PiiType::Custom(name) => format!("custom_{}", name.to_lowercase()),
}
}
#[cfg(test)]
mod tests {
use super::*;
use briefcase_core::PiiType;
#[test]
fn test_pii_type_ssn() {
assert_eq!(pii_type_to_string(&PiiType::Ssn), "ssn");
}
#[test]
fn test_pii_type_credit_card() {
assert_eq!(pii_type_to_string(&PiiType::CreditCard), "credit_card");
}
#[test]
fn test_pii_type_email() {
assert_eq!(pii_type_to_string(&PiiType::Email), "email");
}
#[test]
fn test_pii_type_phone() {
assert_eq!(pii_type_to_string(&PiiType::Phone), "phone");
}
#[test]
fn test_pii_type_api_key() {
assert_eq!(pii_type_to_string(&PiiType::ApiKey), "api_key");
}
#[test]
fn test_pii_type_ip_address() {
assert_eq!(pii_type_to_string(&PiiType::IpAddress), "ip_address");
}
#[test]
fn test_pii_type_custom() {
assert_eq!(
pii_type_to_string(&PiiType::Custom("MyPII".to_string())),
"custom_mypii"
);
}
#[test]
fn test_pii_type_custom_already_lowercase() {
assert_eq!(
pii_type_to_string(&PiiType::Custom("passport".to_string())),
"custom_passport"
);
}
#[test]
fn test_sanitizer_redacts_ssn() {
let sanitizer = Sanitizer::new();
let result = sanitizer.sanitize("My SSN is 123-45-6789");
assert!(!result.sanitized.contains("123-45-6789"));
assert!(!result.redactions.is_empty());
}
#[test]
fn test_sanitizer_redacts_email() {
let sanitizer = Sanitizer::new();
let result = sanitizer.sanitize("Contact me at user@example.com please");
assert!(!result.sanitized.contains("user@example.com"));
}
#[test]
fn test_sanitizer_redacts_credit_card() {
let sanitizer = Sanitizer::new();
let result = sanitizer.sanitize("Card: 4111-1111-1111-1111");
assert!(!result.sanitized.contains("4111-1111-1111-1111"));
}
#[test]
fn test_sanitizer_disabled_passthrough() {
let sanitizer = Sanitizer::disabled();
let text = "SSN: 123-45-6789, email: test@test.com";
let result = sanitizer.sanitize(text);
assert_eq!(result.sanitized, text);
assert!(result.redactions.is_empty());
}
#[test]
fn test_sanitizer_no_pii_clean_text() {
let sanitizer = Sanitizer::new();
let result = sanitizer.sanitize("Hello world, this is clean text.");
assert_eq!(result.sanitized, "Hello world, this is clean text.");
assert!(result.redactions.is_empty());
}
#[test]
fn test_sanitizer_analyze_with_pii() {
let sanitizer = Sanitizer::new();
let analysis = sanitizer.analyze("SSN: 123-45-6789, email: test@example.com");
assert!(analysis.has_pii);
assert!(analysis.total_matches >= 2);
assert!(analysis.unique_types >= 2);
}
#[test]
fn test_sanitizer_analyze_clean_text() {
let sanitizer = Sanitizer::new();
let analysis = sanitizer.analyze("Just a normal sentence.");
assert!(!analysis.has_pii);
assert_eq!(analysis.total_matches, 0);
}
#[test]
fn test_sanitizer_contains_pii_detects() {
let sanitizer = Sanitizer::new();
let matches = sanitizer.contains_pii("My SSN is 123-45-6789");
assert!(!matches.is_empty());
}
#[test]
fn test_sanitizer_json_redaction() {
let sanitizer = Sanitizer::new();
let data = serde_json::json!({
"name": "John",
"ssn": "123-45-6789",
"email": "john@example.com"
});
let result = sanitizer.sanitize_json(&data);
let sanitized_str = result.sanitized.to_string();
assert!(!sanitized_str.contains("123-45-6789"));
assert!(!sanitized_str.contains("john@example.com"));
}
#[test]
fn test_sanitizer_set_enabled_toggle() {
let mut sanitizer = Sanitizer::new();
let text = "SSN: 123-45-6789";
let r1 = sanitizer.sanitize(text);
assert!(!r1.sanitized.contains("123-45-6789"));
sanitizer.set_enabled(false);
let r2 = sanitizer.sanitize(text);
assert_eq!(r2.sanitized, text);
sanitizer.set_enabled(true);
let r3 = sanitizer.sanitize(text);
assert!(!r3.sanitized.contains("123-45-6789"));
}
}