Skip to main content

validation_core/rules/
display_mask.rs

1// src/validation/mask.rs
2//! Pure display mask system - user-defined patterns only
3
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
7pub enum MaskDisplayMode {
8    /// Only show separators as user types
9    /// Example: "" → "", "123" → "123", "12345" → "(123) 45"
10    #[default]
11    Dynamic,
12
13    /// Show full template with placeholders from start
14    /// Example: "" → "(___) ___-____", "123" → "(123) ___-____"
15    Template {
16        /// Character to use as placeholder for empty input positions
17        placeholder: char,
18    },
19}
20
21#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
22pub struct DisplayMask {
23    /// Mask pattern like "##-##-####" where # = input position, others are visual separators
24    pattern: String,
25    /// Character used to represent input positions (usually '#')
26    input_char: char,
27    /// How to display the mask (dynamic vs template)
28    display_mode: MaskDisplayMode,
29}
30
31impl DisplayMask {
32    /// Create a new display mask with dynamic mode (current behavior)
33    ///
34    /// # Arguments
35    /// * `pattern` - The mask pattern (e.g., "##-##-####", "(###) ###-####")  
36    /// * `input_char` - Character representing input positions (usually '#')
37    ///
38    /// # Examples
39    /// ```
40    /// use validation_core::DisplayMask;
41    ///
42    /// // Phone number format
43    /// let phone_mask = DisplayMask::new("(###) ###-####", '#');
44    ///
45    /// // Date format  
46    /// let date_mask = DisplayMask::new("##/##/####", '#');
47    ///
48    /// // Custom business format
49    /// let employee_id = DisplayMask::new("EMP-####-##", '#');
50    /// ```
51    pub fn new(pattern: impl Into<String>, input_char: char) -> Self {
52        Self {
53            pattern: pattern.into(),
54            input_char,
55            display_mode: MaskDisplayMode::Dynamic,
56        }
57    }
58
59    /// Set the display mode for this mask
60    ///
61    /// # Examples
62    /// ```
63    /// use validation_core::{DisplayMask, MaskDisplayMode};
64    ///
65    /// let dynamic_mask = DisplayMask::new("##-##", '#')
66    ///     .with_mode(MaskDisplayMode::Dynamic);
67    ///     
68    /// let template_mask = DisplayMask::new("##-##", '#')
69    ///     .with_mode(MaskDisplayMode::Template { placeholder: '_' });
70    /// ```
71    pub fn with_mode(mut self, mode: MaskDisplayMode) -> Self {
72        self.display_mode = mode;
73        self
74    }
75
76    /// Set template mode with custom placeholder
77    ///
78    /// # Examples
79    /// ```
80    /// use validation_core::DisplayMask;
81    ///
82    /// let phone_template = DisplayMask::new("(###) ###-####", '#')
83    ///     .with_template('_');  // Shows "(___) ___-____" when empty
84    ///     
85    /// let date_dots = DisplayMask::new("##/##/####", '#')
86    ///     .with_template('•');  // Shows "••/••/••••" when empty
87    /// ```
88    pub fn with_template(self, placeholder: char) -> Self {
89        self.with_mode(MaskDisplayMode::Template { placeholder })
90    }
91
92    /// Apply mask to raw input, showing visual separators and handling display mode
93    pub fn apply_to_display(&self, raw_input: &str) -> String {
94        match &self.display_mode {
95            MaskDisplayMode::Dynamic => self.apply_dynamic(raw_input),
96            MaskDisplayMode::Template { placeholder } => {
97                self.apply_template(raw_input, *placeholder)
98            }
99        }
100    }
101
102    /// Dynamic mode - only show separators as user types
103    fn apply_dynamic(&self, raw_input: &str) -> String {
104        if raw_input.is_empty() {
105            return String::new();
106        }
107
108        let mut result = String::new();
109        let mut raw_chars = raw_input.chars();
110
111        for pattern_char in self.pattern.chars() {
112            if pattern_char == self.input_char {
113                // Input position - take from raw input
114                if let Some(input_char) = raw_chars.next() {
115                    result.push(input_char);
116                } else {
117                    // No more input - stop here in dynamic mode
118                    break;
119                }
120            } else {
121                // Visual separator - always show
122                result.push(pattern_char);
123            }
124        }
125
126        // Append any remaining raw characters that don't fit the pattern
127        for remaining_char in raw_chars {
128            result.push(remaining_char);
129        }
130
131        result
132    }
133
134    /// Template mode - show full pattern with placeholders
135    fn apply_template(&self, raw_input: &str, placeholder: char) -> String {
136        let mut result = String::new();
137        let mut raw_chars = raw_input.chars().peekable();
138
139        for pattern_char in self.pattern.chars() {
140            if pattern_char == self.input_char {
141                // Input position - take from raw input or use placeholder
142                if let Some(input_char) = raw_chars.next() {
143                    result.push(input_char);
144                } else {
145                    // No more input - use placeholder to show template
146                    result.push(placeholder);
147                }
148            } else {
149                // Visual separator - always show in template mode
150                result.push(pattern_char);
151            }
152        }
153
154        // In template mode, we don't append extra characters beyond the pattern
155        // This keeps the template consistent
156        result
157    }
158
159    /// Check if a display position should accept cursor/input
160    pub fn is_input_position(&self, display_position: usize) -> bool {
161        self.pattern
162            .chars()
163            .nth(display_position)
164            .map(|c| c == self.input_char)
165            .unwrap_or(true) // Beyond pattern = accept input
166    }
167
168    /// Map display position to raw position
169    pub fn display_pos_to_raw_pos(&self, display_pos: usize) -> usize {
170        let mut raw_pos = 0;
171
172        for (i, pattern_char) in self.pattern.chars().enumerate() {
173            if i >= display_pos {
174                break;
175            }
176            if pattern_char == self.input_char {
177                raw_pos += 1;
178            }
179        }
180
181        raw_pos
182    }
183
184    /// Map raw position to display position
185    pub fn raw_pos_to_display_pos(&self, raw_pos: usize) -> usize {
186        let mut input_positions_seen = 0;
187
188        for (display_pos, pattern_char) in self.pattern.chars().enumerate() {
189            if pattern_char == self.input_char {
190                if input_positions_seen == raw_pos {
191                    return display_pos;
192                }
193                input_positions_seen += 1;
194            }
195        }
196
197        // Beyond pattern, return position after pattern
198        self.pattern.len() + (raw_pos - input_positions_seen)
199    }
200
201    /// Find next input position at or after the given display position
202    pub fn next_input_position(&self, display_pos: usize) -> usize {
203        for (i, pattern_char) in self.pattern.chars().enumerate().skip(display_pos) {
204            if pattern_char == self.input_char {
205                return i;
206            }
207        }
208        // Beyond pattern = all positions are input positions
209        display_pos.max(self.pattern.len())
210    }
211
212    /// Find previous input position at or before the given display position
213    pub fn prev_input_position(&self, display_pos: usize) -> Option<usize> {
214        // Collect pattern chars with indices first, then search backwards
215        let pattern_chars: Vec<(usize, char)> = self.pattern.chars().enumerate().collect();
216
217        // Search backwards from display_pos
218        for &(i, pattern_char) in pattern_chars.iter().rev() {
219            if i <= display_pos && pattern_char == self.input_char {
220                return Some(i);
221            }
222        }
223        None
224    }
225
226    /// Get the display mode
227    pub fn display_mode(&self) -> &MaskDisplayMode {
228        &self.display_mode
229    }
230
231    /// Check if this mask uses template mode
232    pub fn is_template_mode(&self) -> bool {
233        matches!(self.display_mode, MaskDisplayMode::Template { .. })
234    }
235
236    /// Get the pattern string
237    pub fn pattern(&self) -> &str {
238        &self.pattern
239    }
240
241    /// Get the input placeholder character
242    pub fn input_char(&self) -> char {
243        self.input_char
244    }
245
246    /// Get the position of the first input character in the pattern
247    pub fn first_input_position(&self) -> usize {
248        for (pos, ch) in self.pattern.chars().enumerate() {
249            if ch == self.input_char {
250                return pos;
251            }
252        }
253        0
254    }
255}
256
257impl Default for DisplayMask {
258    fn default() -> Self {
259        Self::new("", '#')
260    }
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266
267    #[test]
268    fn test_user_defined_phone_mask() {
269        // User creates their own phone mask
270        let dynamic = DisplayMask::new("(###) ###-####", '#');
271        let template = DisplayMask::new("(###) ###-####", '#').with_template('_');
272
273        // Dynamic mode
274        assert_eq!(dynamic.apply_to_display(""), "");
275        assert_eq!(dynamic.apply_to_display("1234567890"), "(123) 456-7890");
276
277        // Template mode
278        assert_eq!(template.apply_to_display(""), "(___) ___-____");
279        assert_eq!(template.apply_to_display("123"), "(123) ___-____");
280    }
281
282    #[test]
283    fn test_user_defined_date_mask() {
284        // User creates their own date formats
285        let us_date = DisplayMask::new("##/##/####", '#');
286        let eu_date = DisplayMask::new("##.##.####", '#');
287        let iso_date = DisplayMask::new("####-##-##", '#');
288
289        assert_eq!(us_date.apply_to_display("12252024"), "12/25/2024");
290        assert_eq!(eu_date.apply_to_display("25122024"), "25.12.2024");
291        assert_eq!(iso_date.apply_to_display("20241225"), "2024-12-25");
292    }
293
294    #[test]
295    fn test_user_defined_business_formats() {
296        // User creates custom business formats
297        let employee_id = DisplayMask::new("EMP-####-##", '#');
298        let product_code = DisplayMask::new("###-###-###", '#');
299        let invoice = DisplayMask::new("INV####/##", '#');
300
301        assert_eq!(employee_id.apply_to_display("123456"), "EMP-1234-56");
302        assert_eq!(product_code.apply_to_display("123456789"), "123-456-789");
303        assert_eq!(invoice.apply_to_display("123456"), "INV1234/56");
304    }
305
306    #[test]
307    fn test_custom_input_characters() {
308        // User can define their own input character
309        let mask_with_x = DisplayMask::new("XXX-XX-XXXX", 'X');
310        let mask_with_hash = DisplayMask::new("###-##-####", '#');
311        let mask_with_n = DisplayMask::new("NNN-NN-NNNN", 'N');
312
313        assert_eq!(mask_with_x.apply_to_display("123456789"), "123-45-6789");
314        assert_eq!(mask_with_hash.apply_to_display("123456789"), "123-45-6789");
315        assert_eq!(mask_with_n.apply_to_display("123456789"), "123-45-6789");
316    }
317
318    #[test]
319    fn test_custom_placeholders() {
320        // User can define custom placeholder characters
321        let underscores = DisplayMask::new("##-##", '#').with_template('_');
322        let dots = DisplayMask::new("##-##", '#').with_template('•');
323        let dashes = DisplayMask::new("##-##", '#').with_template('-');
324
325        assert_eq!(underscores.apply_to_display(""), "__-__");
326        assert_eq!(dots.apply_to_display(""), "••-••");
327        assert_eq!(dashes.apply_to_display(""), "-----"); // Note: dashes blend with separator
328    }
329
330    #[test]
331    fn test_position_mapping_user_patterns() {
332        let custom = DisplayMask::new("ABC-###-XYZ", '#');
333
334        // Position mapping should work correctly with any pattern
335        assert_eq!(custom.raw_pos_to_display_pos(0), 4); // First # at position 4
336        assert_eq!(custom.raw_pos_to_display_pos(1), 5); // Second # at position 5
337        assert_eq!(custom.raw_pos_to_display_pos(2), 6); // Third # at position 6
338
339        assert_eq!(custom.display_pos_to_raw_pos(4), 0); // Position 4 -> first input
340        assert_eq!(custom.display_pos_to_raw_pos(5), 1); // Position 5 -> second input
341        assert_eq!(custom.display_pos_to_raw_pos(6), 2); // Position 6 -> third input
342
343        assert!(!custom.is_input_position(0)); // A
344        assert!(!custom.is_input_position(3)); // -
345        assert!(custom.is_input_position(4)); // #
346        assert!(!custom.is_input_position(8)); // Y
347    }
348}