use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::Path;
use log::{debug, info}; use std::fmt;
pub const MAX_PATTERN_LENGTH: usize = 500;
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub struct RedactionRule {
pub name: String,
pub pattern: String,
pub replace_with: String,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub multiline: bool,
#[serde(default)]
pub dot_matches_new_line: bool,
#[serde(default)]
pub opt_in: bool,
#[serde(default)]
pub programmatic_validation: bool,
}
#[derive(Debug, Default, Deserialize, Serialize, Clone)]
pub struct RedactionConfig {
pub rules: Vec<RedactionRule>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RedactionSummaryItem {
pub rule_name: String,
pub occurrences: usize,
pub original_texts: Vec<String>,
pub sanitized_texts: Vec<String>,
}
#[derive(Debug)]
pub struct RuleConfigNotFoundError {
pub config_name: String,
}
impl fmt::Display for RuleConfigNotFoundError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Rule configuration '{}' not found.", self.config_name)
}
}
impl std::error::Error for RuleConfigNotFoundError {}
impl RedactionConfig {
pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
let path = path.as_ref();
info!("Loading custom rules from: {}", path.display());
let text = std::fs::read_to_string(path)
.with_context(|| format!("Failed to read config file {}", path.display()))?;
let config: RedactionConfig = serde_yaml::from_str(&text)
.with_context(|| format!("Failed to parse config file {}", path.display()))?;
debug!("[config.rs] Loaded {} rules from file {}.", config.rules.len(), path.display());
for rule in &config.rules {
debug!("[config.rs] File Rule - Name: {}, Opt_in: {}", rule.name, rule.opt_in);
}
Ok(config)
}
pub fn load_default_rules() -> Result<Self> {
debug!("[config.rs] Loading default rules from embedded string...");
let default_yaml = include_str!("../config/default_rules.yaml");
let config: RedactionConfig = serde_yaml::from_str(default_yaml).context("Failed to parse default rules")?;
debug!("[config.rs] Loaded {} default rules.", config.rules.len());
for rule in &config.rules {
debug!("[config.rs] Default Rule - Name: {}, Opt_in: {}", rule.name, rule.opt_in);
}
Ok(config)
}
pub fn set_active_rules_config(&mut self, config_name: &str) -> Result<()> {
debug!("[config.rs] Setting active rules configuration to: '{}'", config_name);
match config_name {
"default" => {
debug!("[config.rs] 'default' config applied. All rules loaded from this config will be passed to compilation.");
}
"strict" => {
debug!("[config.rs] 'strict' config applied. All rules ({} total) are active for compilation.", self.rules.len());
}
_ => {
return Err(RuleConfigNotFoundError { config_name: config_name.to_string() }.into());
}
}
Ok(())
}
}
pub fn merge_rules(
mut default_config: RedactionConfig,
user_config: Option<RedactionConfig>,
) -> RedactionConfig {
let initial_default_count = default_config.rules.len();
debug!("[config.rs] merge_rules called. Initial default rules count: {}", initial_default_count);
if let Some(user_cfg) = user_config {
debug!("[config.rs] User config provided. Merging {} user rules.", user_cfg.rules.len());
let user_rules_map: HashMap<String, RedactionRule> = user_cfg
.rules.clone()
.into_iter()
.map(|rule| {
debug!("[config.rs] User rule to merge: '{}', Opt_in: {}", rule.name, rule.opt_in);
(rule.name.clone(), rule)
})
.collect();
let mut overridden_count = 0;
default_config.rules.retain(|default_rule| {
if user_rules_map.contains_key(&default_rule.name) {
debug!("Default rule '{}' overridden by user configuration. Skipping default.", default_rule.name);
overridden_count += 1;
false
} else {
debug!("[config.rs] Keeping default rule: '{}', Opt_in: {}", default_rule.name, default_rule.opt_in);
true
}
});
let added_user_rules_count = user_rules_map.len() - overridden_count;
default_config.rules.extend(user_rules_map.into_values());
debug!(
"Merged rules summary: {} default rules initially, {} user rules processed. Overrode {} defaults, added {} new user rules. Final total rules: {}",
initial_default_count,
user_cfg.rules.len(),
overridden_count,
added_user_rules_count,
default_config.rules.len()
);
debug!("[config.rs] Final merged rules count: {}", default_config.rules.len());
for rule in &default_config.rules {
debug!("[config.rs] Final Merged Rule - Name: {}, Opt_in: {}", rule.name, rule.opt_in);
}
} else {
debug!(
"No user configuration provided. Using {} default rules.",
default_config.rules.len()
);
debug!("[config.rs] No user configuration to merge. Final rules count: {}", default_config.rules.len());
for rule in &default_config.rules {
debug!("[config.rs] Final Merged Rule (no user config) - Name: {}, Opt_in: {}", rule.name, rule.opt_in);
}
}
default_config
}