1#![doc = include_str!("../examples/percentage.rs")]
7use regex::Regex;
9
10#[derive(Debug)]
11pub enum Kind {
12 Percentage(String, u8, usize, String),
13 Regex(String, Regex, usize, String),
14 Prefix(String, usize, String),
15 All(String, String),
16}
17
18impl Kind {
19 #[must_use]
20 pub fn mask(self) -> String {
21 match self {
22 Self::Percentage(mask_str, percentage, min_chars, mask_char) => {
23 with_percentage(mask_str, percentage, min_chars, mask_char)
24 }
25 Self::Regex(mask_str, re, group, mask_char) => {
26 with_regex(mask_str, re, group, mask_char)
27 }
28 Self::Prefix(mask_str, until, mask_char) => with_prefix(mask_str, until, mask_char),
29 Self::All(mask_str, mask_char) => all(mask_str, mask_char),
30 }
31 }
32}
33
34#[must_use]
49fn with_percentage(text: String, percentage: u8, min_chars: usize, mask_char: String) -> String {
50 #[allow(clippy::cast_sign_loss)]
51 #[allow(clippy::cast_possible_truncation)]
52 #[allow(clippy::cast_precision_loss)]
53 let mask_from = if text.len() > min_chars {
54 text.len() - ((f32::from(percentage) * text.len() as f32) / 100.0).floor() as usize
55 } else {
56 0
58 };
59 mask(text, mask_from, mask_char)
60}
61
62#[must_use]
79fn with_regex(text: String, re: Regex, group: usize, mask_char: String) -> String {
80 let cap_text = re
81 .captures(&text)
82 .and_then(|f| f.get(group))
83 .map_or(text.as_str(), |m| m.as_str());
84
85 text.replace(cap_text, &mask(cap_text.to_string(), 0, mask_char))
86}
87
88#[must_use]
101fn with_prefix(text: String, until: usize, mask_char: String) -> String {
102 let until = if until >= text.len() { 0 } else { until };
103 mask(text, until, mask_char)
104}
105
106#[must_use]
118fn all(text: String, mask_char: String) -> String {
119 mask(text, 0, mask_char)
120}
121
122fn mask(str: String, from: usize, mask_char: String) -> String {
123 str.chars()
124 .enumerate()
125 .map(|(i, c)| {
126 if c as u8 == 0x0d {
127 "\r".to_string()
128 } else if c as u8 == 0x0a {
129 "\n".to_string()
130 } else if i >= from {
131 mask_char.to_string()
132 } else {
133 c.to_string()
134 }
135 })
136 .collect::<String>()
137}
138
139#[cfg(test)]
140mod test_mask {
141
142 use insta::assert_debug_snapshot;
143 use rstest::rstest;
144
145 use super::*;
146
147 macro_rules! set_snapshot_suffix {
148 ($($expr:expr),*) => {
149 let mut settings = insta::Settings::clone_current();
150 settings.set_prepend_module_to_snapshot(false);
151 settings.set_snapshot_suffix(format!($($expr,)*));
152 let _guard = settings.bind_to_scope();
153 }
154 }
155
156 #[rstest]
157 #[case(20, 3, "*")]
158 #[case(80, 3, "*")]
159 #[case(100, 3, "*")]
160 #[case(80, 20, "*")]
161 #[case(80, 3, ".")]
162 fn cat_mask_with_percentage(
163 #[case] percentage: u8,
164 #[case] min_chars: usize,
165 #[case] mask_char: String,
166 ) {
167 let text = "text to mask".to_string();
168
169 set_snapshot_suffix!(
170 "[{}]-[{}]-[{}]",
171 percentage,
172 min_chars,
173 mask_char.replace('*', "asterisk")
174 );
175
176 assert_debug_snapshot!(Kind::Percentage(text, percentage, min_chars, mask_char,).mask());
177 }
178
179 #[rstest]
180 #[case("([a-z].*) (mask) ([a-z].*)", 2, "*")]
181 #[case("([a-z].*) (mask) ([a-z].*)", 3, "*")]
182 #[case("([a-z].*) (mask) ([a-z].*)", 1, ".")]
183 fn cat_mask_with_regex(#[case] re: &str, #[case] group: usize, #[case] mask_char: String) {
184 let text = "text to mask on group".to_string();
185
186 set_snapshot_suffix!(
187 "[{}]-[{}]-[{}]",
188 re.replace('*', "asterisk"),
189 group,
190 mask_char.replace('*', "asterisk")
191 );
192
193 let re = Regex::new(re).unwrap();
194 assert_debug_snapshot!(Kind::Regex(text, re, group, mask_char).mask());
195 }
196
197 #[rstest]
198 #[case(3, "*")]
199 #[case(200, "*")]
200 fn cat_mask_with_prefix(#[case] until: usize, #[case] mask_char: String) {
201 let text = "text to mask".to_string();
202
203 set_snapshot_suffix!("[{}]-[{}]", until, mask_char.replace('*', "asterisk"));
204
205 assert_debug_snapshot!(Kind::Prefix(text, until, mask_char).mask());
206 }
207}