use super::masking::MaskRule;
use crate::error::InklogError;
#[derive(Debug, Clone, Default)]
pub struct MaskRuleRegistry {
rules: Vec<MaskRule>,
}
impl MaskRuleRegistry {
pub fn with_builtins() -> Self {
use super::masking::DataMasker;
let masker = DataMasker::new();
Self {
rules: masker.into_rules(),
}
}
pub fn register(&mut self, rule: MaskRule) -> Result<(), InklogError> {
if self.rules.iter().any(|r| r.name() == rule.name()) {
let mut args = fluent_bundle::FluentArgs::new();
args.set("name", rule.name());
return Err(InklogError::ConfigError(crate::i18n::tr_args(
"config-rule_already_registered",
args,
)));
}
self.rules.push(rule);
Ok(())
}
pub fn remove(&mut self, name: &str) -> Option<MaskRule> {
if let Some(pos) = self.rules.iter().position(|r| r.name() == name) {
Some(self.rules.remove(pos))
} else {
None
}
}
pub fn set_enabled(&mut self, name: &str, enabled: bool) -> bool {
if let Some(rule) = self.rules.iter_mut().find(|r| r.name() == name) {
rule.set_enabled(enabled);
true
} else {
false
}
}
pub fn active_rules(&self) -> Vec<&MaskRule> {
let mut active: Vec<&MaskRule> = self.rules.iter().filter(|r| r.is_enabled()).collect();
active.sort_by_key(|r| r.priority());
active
}
pub fn len(&self) -> usize {
self.rules.len()
}
pub fn is_empty(&self) -> bool {
self.rules.is_empty()
}
pub fn load_from_toml(toml_str: &str) -> Result<Vec<MaskRule>, InklogError> {
let parsed: toml::Value = toml::from_str(toml_str).map_err(|e| {
let mut args = fluent_bundle::FluentArgs::new();
args.set("err", e.to_string());
InklogError::ConfigError(crate::i18n::tr_args("config-failed_parse_toml", args))
})?;
let rules_tables = parsed
.get("masking_rules")
.and_then(|v| v.as_array())
.ok_or_else(|| {
InklogError::ConfigError(crate::i18n::tr("config-toml_missing_masking_rules"))
})?;
let mut rules = Vec::new();
for table in rules_tables {
let name = table.get("name").and_then(|v| v.as_str()).ok_or_else(|| {
InklogError::ConfigError(crate::i18n::tr("config-masking_missing_name"))
})?;
let pattern = table
.get("pattern")
.and_then(|v| v.as_str())
.ok_or_else(|| {
let mut args = fluent_bundle::FluentArgs::new();
args.set("name", name);
InklogError::ConfigError(crate::i18n::tr_args(
"config-masking_missing_pattern",
args,
))
})?;
let replacement = table
.get("replacement")
.and_then(|v| v.as_str())
.unwrap_or("***REDACTED***");
let priority_raw = table
.get("priority")
.and_then(|v| v.as_integer())
.unwrap_or(100);
let priority = priority_raw.clamp(i32::MIN as i64, i32::MAX as i64) as i32;
let enabled = table
.get("enabled")
.and_then(|v| v.as_bool())
.unwrap_or(true);
let rule = MaskRule::builder(name)
.pattern(pattern)
.replacement(replacement)
.priority(priority)
.enabled(enabled)
.build()?;
rules.push(rule);
}
Ok(rules)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_with_builtins() {
let registry = MaskRuleRegistry::with_builtins();
assert!(registry.len() >= 21);
assert!(!registry.is_empty());
}
#[test]
fn test_register_and_remove() {
let mut registry = MaskRuleRegistry::default();
let rule = MaskRule::builder("custom")
.pattern(r"\d+")
.replacement("***")
.build()
.unwrap();
assert!(registry.register(rule).is_ok());
assert_eq!(registry.len(), 1);
let dup = MaskRule::builder("custom")
.pattern(r"\w+")
.replacement("###")
.build()
.unwrap();
assert!(registry.register(dup).is_err());
let removed = registry.remove("custom");
assert!(removed.is_some());
assert_eq!(registry.len(), 0);
assert!(registry.remove("nonexistent").is_none());
}
#[test]
fn test_set_enabled() {
let mut registry = MaskRuleRegistry::with_builtins();
let initial_active = registry.active_rules().len();
assert!(registry.set_enabled("email", false));
assert_eq!(registry.active_rules().len(), initial_active - 1);
assert!(registry.set_enabled("email", true));
assert_eq!(registry.active_rules().len(), initial_active);
assert!(!registry.set_enabled("nonexistent", false));
}
#[test]
fn test_active_rules_sorted_by_priority() {
let registry = MaskRuleRegistry::with_builtins();
let active = registry.active_rules();
for window in active.windows(2) {
assert!(window[0].priority() <= window[1].priority());
}
}
#[test]
fn test_load_from_toml() {
let toml_str = r#"
[[masking_rules]]
name = "custom_id"
pattern = "\\bCUSTOM-\\d{6}\\b"
replacement = "***CUSTOM***"
priority = 100
enabled = true
[[masking_rules]]
name = "another_rule"
pattern = "\\bTEST-\\w+\\b"
"#;
let rules = MaskRuleRegistry::load_from_toml(toml_str).unwrap();
assert_eq!(rules.len(), 2);
assert_eq!(rules[0].name(), "custom_id");
assert_eq!(rules[0].priority(), 100);
assert!(rules[0].is_enabled());
assert_eq!(rules[1].name(), "another_rule");
assert_eq!(rules[1].priority(), 100);
assert!(rules[1].is_enabled());
}
#[test]
fn test_load_from_toml_missing_name() {
let toml_str = r#"
[[masking_rules]]
pattern = "\\d+"
"#;
assert!(MaskRuleRegistry::load_from_toml(toml_str).is_err());
}
#[test]
fn test_load_from_toml_invalid_regex() {
let toml_str = r#"
[[masking_rules]]
name = "bad"
pattern = "[invalid"
"#;
assert!(MaskRuleRegistry::load_from_toml(toml_str).is_err());
}
#[test]
fn test_load_from_toml_missing_array() {
let toml_str = "[other]\nkey = \"value\"\n";
assert!(MaskRuleRegistry::load_from_toml(toml_str).is_err());
}
}