sql_splitter/redactor/strategy/
mod.rs1mod constant;
13mod fake;
14mod hash;
15mod mask;
16mod null;
17mod shuffle;
18mod skip;
19
20pub use constant::ConstantStrategy;
21pub use fake::FakeStrategy;
22pub use hash::HashStrategy;
23pub use mask::MaskStrategy;
24pub use null::NullStrategy;
25#[allow(unused_imports)] pub use shuffle::ShuffleStrategy;
27#[allow(unused_imports)] pub use skip::SkipStrategy;
29
30use serde::{Deserialize, Serialize};
31
32#[derive(Debug, Clone, Default, Serialize, Deserialize)]
34#[serde(tag = "strategy", rename_all = "snake_case")]
35pub enum StrategyKind {
36 Null,
38
39 Constant {
41 value: String,
43 },
44
45 Hash {
47 #[serde(default)]
49 preserve_domain: bool,
50 },
51
52 Mask {
54 pattern: String,
56 },
57
58 Shuffle,
60
61 Fake {
63 generator: String,
65 },
66
67 #[default]
69 Skip,
70}
71
72impl StrategyKind {
73 pub fn validate(&self) -> anyhow::Result<()> {
75 match self {
76 StrategyKind::Null => Ok(()),
77 StrategyKind::Constant { value } => {
78 if value.is_empty() {
79 anyhow::bail!("Constant strategy requires a non-empty value");
80 }
81 Ok(())
82 }
83 StrategyKind::Hash { .. } => Ok(()),
84 StrategyKind::Mask { pattern } => {
85 if pattern.is_empty() {
86 anyhow::bail!("Mask strategy requires a non-empty pattern");
87 }
88 for c in pattern.chars() {
90 if !matches!(c, '*' | 'X' | '#' | '-' | ' ' | '.' | '@' | '(' | ')') {
91 }
93 }
94 Ok(())
95 }
96 StrategyKind::Shuffle => Ok(()),
97 StrategyKind::Fake { generator } => {
98 if !is_valid_generator(generator) {
99 anyhow::bail!("Unknown fake generator: {}. Use: email, name, first_name, last_name, phone, address, city, zip, company, ip, uuid, date, etc.", generator);
100 }
101 Ok(())
102 }
103 StrategyKind::Skip => Ok(()),
104 }
105 }
106}
107
108fn is_valid_generator(name: &str) -> bool {
110 matches!(
111 name.to_lowercase().as_str(),
112 "email"
113 | "safe_email"
114 | "name"
115 | "first_name"
116 | "last_name"
117 | "full_name"
118 | "phone"
119 | "phone_number"
120 | "address"
121 | "street_address"
122 | "city"
123 | "state"
124 | "zip"
125 | "zip_code"
126 | "postal_code"
127 | "country"
128 | "company"
129 | "company_name"
130 | "job_title"
131 | "username"
132 | "user_name"
133 | "url"
134 | "ip"
135 | "ip_address"
136 | "ipv4"
137 | "ipv6"
138 | "uuid"
139 | "date"
140 | "date_time"
141 | "datetime"
142 | "time"
143 | "credit_card"
144 | "iban"
145 | "lorem"
146 | "paragraph"
147 | "sentence"
148 | "word"
149 | "ssn"
150 )
151}
152
153#[derive(Debug, Clone)]
155pub enum RedactValue {
156 Null,
158 String(String),
160 Integer(i64),
162 Bytes(Vec<u8>),
164}
165
166impl RedactValue {
167 pub fn is_null(&self) -> bool {
169 matches!(self, RedactValue::Null)
170 }
171
172 pub fn as_str(&self) -> Option<&str> {
174 match self {
175 RedactValue::String(s) => Some(s),
176 _ => None,
177 }
178 }
179}
180
181pub trait Strategy: Send + Sync {
183 fn apply(&self, value: &RedactValue, rng: &mut dyn rand::RngCore) -> RedactValue;
185
186 fn kind(&self) -> StrategyKind;
188}