feature3 with bug, needs a fix immidiately
This commit is contained in:
@@ -1,23 +1,23 @@
|
||||
// src/validation/config.rs
|
||||
//! Validation configuration types and builders
|
||||
|
||||
use crate::validation::{CharacterLimits, PatternFilters};
|
||||
use crate::validation::{CharacterLimits, PatternFilters, DisplayMask};
|
||||
|
||||
/// Main validation configuration for a field
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ValidationConfig {
|
||||
/// Character limit configuration
|
||||
pub character_limits: Option<CharacterLimits>,
|
||||
|
||||
|
||||
/// Pattern filtering configuration
|
||||
pub pattern_filters: Option<PatternFilters>,
|
||||
|
||||
/// Future: Reserved characters
|
||||
pub reserved_chars: Option<()>, // Placeholder for future implementation
|
||||
|
||||
|
||||
/// User-defined display mask for visual formatting
|
||||
pub display_mask: Option<DisplayMask>,
|
||||
|
||||
/// Future: Custom formatting
|
||||
pub custom_formatting: Option<()>, // Placeholder for future implementation
|
||||
|
||||
|
||||
/// Future: External validation
|
||||
pub external_validation: Option<()>, // Placeholder for future implementation
|
||||
}
|
||||
@@ -33,25 +33,57 @@ impl ValidationConfigBuilder {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
|
||||
/// Set character limits for the field
|
||||
pub fn with_character_limits(mut self, limits: CharacterLimits) -> Self {
|
||||
self.config.character_limits = Some(limits);
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
/// Set pattern filters for the field
|
||||
pub fn with_pattern_filters(mut self, filters: PatternFilters) -> Self {
|
||||
self.config.pattern_filters = Some(filters);
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
/// Set user-defined display mask for visual formatting
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use canvas::{ValidationConfigBuilder, DisplayMask};
|
||||
///
|
||||
/// // Phone number with dynamic formatting
|
||||
/// let phone_mask = DisplayMask::new("(###) ###-####", '#');
|
||||
/// let config = ValidationConfigBuilder::new()
|
||||
/// .with_display_mask(phone_mask)
|
||||
/// .build();
|
||||
///
|
||||
/// // Date with template formatting
|
||||
/// let date_mask = DisplayMask::new("##/##/####", '#')
|
||||
/// .with_template('_');
|
||||
/// let config = ValidationConfigBuilder::new()
|
||||
/// .with_display_mask(date_mask)
|
||||
/// .build();
|
||||
///
|
||||
/// // Custom business format
|
||||
/// let employee_id = DisplayMask::new("EMP-####-##", '#')
|
||||
/// .with_template('•');
|
||||
/// let config = ValidationConfigBuilder::new()
|
||||
/// .with_display_mask(employee_id)
|
||||
/// .with_max_length(6) // Only store the 6 digits
|
||||
/// .build();
|
||||
/// ```
|
||||
pub fn with_display_mask(mut self, mask: DisplayMask) -> Self {
|
||||
self.config.display_mask = Some(mask);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set maximum number of characters (convenience method)
|
||||
pub fn with_max_length(mut self, max_length: usize) -> Self {
|
||||
self.config.character_limits = Some(CharacterLimits::new(max_length));
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
/// Build the final validation configuration
|
||||
pub fn build(self) -> ValidationConfig {
|
||||
self.config
|
||||
@@ -63,10 +95,10 @@ impl ValidationConfigBuilder {
|
||||
pub enum ValidationResult {
|
||||
/// Validation passed
|
||||
Valid,
|
||||
|
||||
|
||||
/// Validation failed with warning (input still accepted)
|
||||
Warning { message: String },
|
||||
|
||||
|
||||
/// Validation failed with error (input rejected)
|
||||
Error { message: String },
|
||||
}
|
||||
@@ -76,12 +108,12 @@ impl ValidationResult {
|
||||
pub fn is_acceptable(&self) -> bool {
|
||||
matches!(self, ValidationResult::Valid | ValidationResult::Warning { .. })
|
||||
}
|
||||
|
||||
|
||||
/// Check if the validation result is an error
|
||||
pub fn is_error(&self) -> bool {
|
||||
matches!(self, ValidationResult::Error { .. })
|
||||
}
|
||||
|
||||
|
||||
/// Get the message if there is one
|
||||
pub fn message(&self) -> Option<&str> {
|
||||
match self {
|
||||
@@ -90,12 +122,12 @@ impl ValidationResult {
|
||||
ValidationResult::Error { message } => Some(message),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Create a warning result
|
||||
pub fn warning(message: impl Into<String>) -> Self {
|
||||
ValidationResult::Warning { message: message.into() }
|
||||
}
|
||||
|
||||
|
||||
/// Create an error result
|
||||
pub fn error(message: impl Into<String>) -> Self {
|
||||
ValidationResult::Error { message: message.into() }
|
||||
@@ -107,22 +139,41 @@ impl ValidationConfig {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
|
||||
/// Create a configuration with just character limits
|
||||
pub fn with_max_length(max_length: usize) -> Self {
|
||||
ValidationConfigBuilder::new()
|
||||
.with_max_length(max_length)
|
||||
.build()
|
||||
}
|
||||
|
||||
|
||||
/// Create a configuration with pattern filters
|
||||
pub fn with_patterns(patterns: PatternFilters) -> Self {
|
||||
ValidationConfigBuilder::new()
|
||||
.with_pattern_filters(patterns)
|
||||
.build()
|
||||
}
|
||||
|
||||
/// Validate a character insertion at a specific position
|
||||
|
||||
/// Create a configuration with user-defined display mask
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use canvas::{ValidationConfig, DisplayMask};
|
||||
///
|
||||
/// let phone_mask = DisplayMask::new("(###) ###-####", '#');
|
||||
/// let config = ValidationConfig::with_mask(phone_mask);
|
||||
/// ```
|
||||
pub fn with_mask(mask: DisplayMask) -> Self {
|
||||
ValidationConfigBuilder::new()
|
||||
.with_display_mask(mask)
|
||||
.build()
|
||||
}
|
||||
|
||||
/// Validate a character insertion at a specific position (raw text space).
|
||||
///
|
||||
/// Note: Display masks are visual-only and do not participate in validation.
|
||||
/// Editor logic is responsible for skipping mask separator positions; here we
|
||||
/// only validate the raw insertion against limits and patterns.
|
||||
pub fn validate_char_insertion(
|
||||
&self,
|
||||
current_text: &str,
|
||||
@@ -137,20 +188,20 @@ impl ValidationConfig {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Pattern filters validation
|
||||
if let Some(ref patterns) = self.pattern_filters {
|
||||
if let Err(message) = patterns.validate_char_at_position(position, character) {
|
||||
return ValidationResult::error(message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Future: Add other validation types here
|
||||
|
||||
|
||||
ValidationResult::Valid
|
||||
}
|
||||
|
||||
/// Validate the current text content
|
||||
|
||||
/// Validate the current text content (raw text space)
|
||||
pub fn validate_content(&self, text: &str) -> ValidationResult {
|
||||
// Character limits validation
|
||||
if let Some(ref limits) = self.character_limits {
|
||||
@@ -160,26 +211,26 @@ impl ValidationConfig {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Pattern filters validation
|
||||
if let Some(ref patterns) = self.pattern_filters {
|
||||
if let Err(message) = patterns.validate_text(text) {
|
||||
return ValidationResult::error(message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Future: Add other validation types here
|
||||
|
||||
|
||||
ValidationResult::Valid
|
||||
}
|
||||
|
||||
|
||||
/// Check if any validation rules are configured
|
||||
pub fn has_validation(&self) -> bool {
|
||||
self.character_limits.is_some() || self.pattern_filters.is_some()
|
||||
// || self.reserved_chars.is_some()
|
||||
// || self.custom_formatting.is_some()
|
||||
// || self.external_validation.is_some()
|
||||
self.character_limits.is_some()
|
||||
|| self.pattern_filters.is_some()
|
||||
|| self.display_mask.is_some()
|
||||
}
|
||||
|
||||
pub fn allows_field_switch(&self, text: &str) -> bool {
|
||||
// Character limits validation
|
||||
if let Some(ref limits) = self.character_limits {
|
||||
@@ -187,12 +238,12 @@ impl ValidationConfig {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Future: Add other validation types here
|
||||
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
|
||||
/// Get reason why field switching is blocked (if any)
|
||||
pub fn field_switch_block_reason(&self, text: &str) -> Option<String> {
|
||||
// Character limits validation
|
||||
@@ -201,9 +252,9 @@ impl ValidationConfig {
|
||||
return Some(reason);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Future: Add other validation types here
|
||||
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -212,89 +263,99 @@ impl ValidationConfig {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_config_with_user_defined_mask() {
|
||||
// User creates their own phone mask
|
||||
let phone_mask = DisplayMask::new("(###) ###-####", '#');
|
||||
let config = ValidationConfig::with_mask(phone_mask);
|
||||
|
||||
// has_validation should be true because mask is configured
|
||||
assert!(config.has_validation());
|
||||
|
||||
// Display mask is visual only; validation still focuses on raw content
|
||||
let result = config.validate_char_insertion("123", 3, '4');
|
||||
assert!(result.is_acceptable());
|
||||
|
||||
// Content validation unaffected by mask
|
||||
let result = config.validate_content("1234567890");
|
||||
assert!(result.is_acceptable());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validation_config_builder() {
|
||||
let config = ValidationConfigBuilder::new()
|
||||
.with_max_length(10)
|
||||
.build();
|
||||
|
||||
|
||||
assert!(config.character_limits.is_some());
|
||||
assert_eq!(config.character_limits.unwrap().max_length(), Some(10));
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_config_builder_with_user_mask() {
|
||||
// User defines custom format
|
||||
let custom_mask = DisplayMask::new("##-##-##", '#').with_template('_');
|
||||
let config = ValidationConfigBuilder::new()
|
||||
.with_display_mask(custom_mask)
|
||||
.with_max_length(6)
|
||||
.build();
|
||||
|
||||
assert!(config.has_validation());
|
||||
assert!(config.character_limits.is_some());
|
||||
assert!(config.display_mask.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validation_result() {
|
||||
let valid = ValidationResult::Valid;
|
||||
assert!(valid.is_acceptable());
|
||||
assert!(!valid.is_error());
|
||||
assert_eq!(valid.message(), None);
|
||||
|
||||
|
||||
let warning = ValidationResult::warning("Too long");
|
||||
assert!(warning.is_acceptable());
|
||||
assert!(!warning.is_error());
|
||||
assert_eq!(warning.message(), Some("Too long"));
|
||||
|
||||
|
||||
let error = ValidationResult::error("Invalid");
|
||||
assert!(!error.is_acceptable());
|
||||
assert!(error.is_error());
|
||||
assert_eq!(error.message(), Some("Invalid"));
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_config_with_max_length() {
|
||||
let config = ValidationConfig::with_max_length(5);
|
||||
assert!(config.has_validation());
|
||||
|
||||
|
||||
// Test valid insertion
|
||||
let result = config.validate_char_insertion("test", 4, 'x');
|
||||
assert!(result.is_acceptable());
|
||||
|
||||
|
||||
// Test invalid insertion (would exceed limit)
|
||||
let result = config.validate_char_insertion("tests", 5, 'x');
|
||||
assert!(!result.is_acceptable());
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_config_with_patterns() {
|
||||
use crate::validation::{PatternFilters, PositionFilter, PositionRange, CharacterFilter};
|
||||
|
||||
|
||||
let patterns = PatternFilters::new()
|
||||
.add_filter(PositionFilter::new(
|
||||
PositionRange::Range(0, 1),
|
||||
CharacterFilter::Alphabetic,
|
||||
));
|
||||
|
||||
|
||||
let config = ValidationConfig::with_patterns(patterns);
|
||||
assert!(config.has_validation());
|
||||
|
||||
|
||||
// Test valid pattern insertion
|
||||
let result = config.validate_char_insertion("", 0, 'A');
|
||||
assert!(result.is_acceptable());
|
||||
|
||||
// Test invalid pattern insertion
|
||||
|
||||
// Test invalid pattern insertion
|
||||
let result = config.validate_char_insertion("", 0, '1');
|
||||
assert!(!result.is_acceptable());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_builder_with_patterns() {
|
||||
use crate::validation::{PatternFilters, PositionFilter, PositionRange, CharacterFilter};
|
||||
|
||||
let patterns = PatternFilters::license_plate();
|
||||
let config = ValidationConfigBuilder::new()
|
||||
.with_pattern_filters(patterns)
|
||||
.with_max_length(5)
|
||||
.build();
|
||||
|
||||
assert!(config.has_validation());
|
||||
assert!(config.character_limits.is_some());
|
||||
assert!(config.pattern_filters.is_some());
|
||||
|
||||
// Test pattern validation
|
||||
let result = config.validate_content("AB123");
|
||||
assert!(result.is_acceptable());
|
||||
|
||||
let result = config.validate_content("A1123");
|
||||
assert!(!result.is_acceptable());
|
||||
}
|
||||
}
|
||||
|
||||
333
canvas/src/validation/mask.rs
Normal file
333
canvas/src/validation/mask.rs
Normal file
@@ -0,0 +1,333 @@
|
||||
// src/validation/mask.rs
|
||||
//! Pure display mask system - user-defined patterns only
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum MaskDisplayMode {
|
||||
/// Only show separators as user types
|
||||
/// Example: "" → "", "123" → "123", "12345" → "(123) 45"
|
||||
Dynamic,
|
||||
|
||||
/// Show full template with placeholders from start
|
||||
/// Example: "" → "(___) ___-____", "123" → "(123) ___-____"
|
||||
Template {
|
||||
/// Character to use as placeholder for empty input positions
|
||||
placeholder: char
|
||||
},
|
||||
}
|
||||
|
||||
impl Default for MaskDisplayMode {
|
||||
fn default() -> Self {
|
||||
MaskDisplayMode::Dynamic
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct DisplayMask {
|
||||
/// Mask pattern like "##-##-####" where # = input position, others are visual separators
|
||||
pattern: String,
|
||||
/// Character used to represent input positions (usually '#')
|
||||
input_char: char,
|
||||
/// How to display the mask (dynamic vs template)
|
||||
display_mode: MaskDisplayMode,
|
||||
}
|
||||
|
||||
impl DisplayMask {
|
||||
/// Create a new display mask with dynamic mode (current behavior)
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `pattern` - The mask pattern (e.g., "##-##-####", "(###) ###-####")
|
||||
/// * `input_char` - Character representing input positions (usually '#')
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// // Phone number format
|
||||
/// let phone_mask = DisplayMask::new("(###) ###-####", '#');
|
||||
///
|
||||
/// // Date format
|
||||
/// let date_mask = DisplayMask::new("##/##/####", '#');
|
||||
///
|
||||
/// // Custom business format
|
||||
/// let employee_id = DisplayMask::new("EMP-####-##", '#');
|
||||
/// ```
|
||||
pub fn new(pattern: impl Into<String>, input_char: char) -> Self {
|
||||
Self {
|
||||
pattern: pattern.into(),
|
||||
input_char,
|
||||
display_mode: MaskDisplayMode::Dynamic,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the display mode for this mask
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// let dynamic_mask = DisplayMask::new("##-##", '#')
|
||||
/// .with_mode(MaskDisplayMode::Dynamic);
|
||||
///
|
||||
/// let template_mask = DisplayMask::new("##-##", '#')
|
||||
/// .with_mode(MaskDisplayMode::Template { placeholder: '_' });
|
||||
/// ```
|
||||
pub fn with_mode(mut self, mode: MaskDisplayMode) -> Self {
|
||||
self.display_mode = mode;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set template mode with custom placeholder
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// let phone_template = DisplayMask::new("(###) ###-####", '#')
|
||||
/// .with_template('_'); // Shows "(___) ___-____" when empty
|
||||
///
|
||||
/// let date_dots = DisplayMask::new("##/##/####", '#')
|
||||
/// .with_template('•'); // Shows "••/••/••••" when empty
|
||||
/// ```
|
||||
pub fn with_template(self, placeholder: char) -> Self {
|
||||
self.with_mode(MaskDisplayMode::Template { placeholder })
|
||||
}
|
||||
|
||||
/// Apply mask to raw input, showing visual separators and handling display mode
|
||||
pub fn apply_to_display(&self, raw_input: &str) -> String {
|
||||
match &self.display_mode {
|
||||
MaskDisplayMode::Dynamic => self.apply_dynamic(raw_input),
|
||||
MaskDisplayMode::Template { placeholder } => self.apply_template(raw_input, *placeholder),
|
||||
}
|
||||
}
|
||||
|
||||
/// Dynamic mode - only show separators as user types
|
||||
fn apply_dynamic(&self, raw_input: &str) -> String {
|
||||
let mut result = String::new();
|
||||
let mut raw_chars = raw_input.chars();
|
||||
|
||||
for pattern_char in self.pattern.chars() {
|
||||
if pattern_char == self.input_char {
|
||||
// Input position - take from raw input
|
||||
if let Some(input_char) = raw_chars.next() {
|
||||
result.push(input_char);
|
||||
} else {
|
||||
// No more input - stop here in dynamic mode
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// Visual separator - always show
|
||||
result.push(pattern_char);
|
||||
}
|
||||
}
|
||||
|
||||
// Append any remaining raw characters that don't fit the pattern
|
||||
for remaining_char in raw_chars {
|
||||
result.push(remaining_char);
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Template mode - show full pattern with placeholders
|
||||
fn apply_template(&self, raw_input: &str, placeholder: char) -> String {
|
||||
let mut result = String::new();
|
||||
let mut raw_chars = raw_input.chars().peekable();
|
||||
|
||||
for pattern_char in self.pattern.chars() {
|
||||
if pattern_char == self.input_char {
|
||||
// Input position - take from raw input or use placeholder
|
||||
if let Some(input_char) = raw_chars.next() {
|
||||
result.push(input_char);
|
||||
} else {
|
||||
// No more input - use placeholder to show template
|
||||
result.push(placeholder);
|
||||
}
|
||||
} else {
|
||||
// Visual separator - always show in template mode
|
||||
result.push(pattern_char);
|
||||
}
|
||||
}
|
||||
|
||||
// In template mode, we don't append extra characters beyond the pattern
|
||||
// This keeps the template consistent
|
||||
result
|
||||
}
|
||||
|
||||
/// Check if a display position should accept cursor/input
|
||||
pub fn is_input_position(&self, display_position: usize) -> bool {
|
||||
self.pattern.chars()
|
||||
.nth(display_position)
|
||||
.map(|c| c == self.input_char)
|
||||
.unwrap_or(true) // Beyond pattern = accept input
|
||||
}
|
||||
|
||||
/// Map display position to raw position
|
||||
pub fn display_pos_to_raw_pos(&self, display_pos: usize) -> usize {
|
||||
let mut raw_pos = 0;
|
||||
|
||||
for (i, pattern_char) in self.pattern.chars().enumerate() {
|
||||
if i >= display_pos {
|
||||
break;
|
||||
}
|
||||
if pattern_char == self.input_char {
|
||||
raw_pos += 1;
|
||||
}
|
||||
}
|
||||
|
||||
raw_pos
|
||||
}
|
||||
|
||||
/// Map raw position to display position
|
||||
pub fn raw_pos_to_display_pos(&self, raw_pos: usize) -> usize {
|
||||
let mut input_positions_seen = 0;
|
||||
|
||||
for (display_pos, pattern_char) in self.pattern.chars().enumerate() {
|
||||
if pattern_char == self.input_char {
|
||||
if input_positions_seen == raw_pos {
|
||||
return display_pos;
|
||||
}
|
||||
input_positions_seen += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Beyond pattern, return position after pattern
|
||||
self.pattern.len() + (raw_pos - input_positions_seen)
|
||||
}
|
||||
|
||||
/// Find next input position at or after the given display position
|
||||
pub fn next_input_position(&self, display_pos: usize) -> usize {
|
||||
for (i, pattern_char) in self.pattern.chars().enumerate().skip(display_pos) {
|
||||
if pattern_char == self.input_char {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
// Beyond pattern = all positions are input positions
|
||||
display_pos.max(self.pattern.len())
|
||||
}
|
||||
|
||||
/// Find previous input position at or before the given display position
|
||||
pub fn prev_input_position(&self, display_pos: usize) -> Option<usize> {
|
||||
// Collect pattern chars with indices first, then search backwards
|
||||
let pattern_chars: Vec<(usize, char)> = self.pattern.chars().enumerate().collect();
|
||||
|
||||
// Search backwards from display_pos
|
||||
for &(i, pattern_char) in pattern_chars.iter().rev() {
|
||||
if i <= display_pos && pattern_char == self.input_char {
|
||||
return Some(i);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Get the display mode
|
||||
pub fn display_mode(&self) -> &MaskDisplayMode {
|
||||
&self.display_mode
|
||||
}
|
||||
|
||||
/// Check if this mask uses template mode
|
||||
pub fn is_template_mode(&self) -> bool {
|
||||
matches!(self.display_mode, MaskDisplayMode::Template { .. })
|
||||
}
|
||||
|
||||
/// Get the pattern string
|
||||
pub fn pattern(&self) -> &str {
|
||||
&self.pattern
|
||||
}
|
||||
|
||||
/// Get the position of the first input character in the pattern
|
||||
pub fn first_input_position(&self) -> usize {
|
||||
for (pos, ch) in self.pattern.chars().enumerate() {
|
||||
if ch == self.input_char {
|
||||
return pos;
|
||||
}
|
||||
}
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DisplayMask {
|
||||
fn default() -> Self {
|
||||
Self::new("", '#')
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_user_defined_phone_mask() {
|
||||
// User creates their own phone mask
|
||||
let dynamic = DisplayMask::new("(###) ###-####", '#');
|
||||
let template = DisplayMask::new("(###) ###-####", '#').with_template('_');
|
||||
|
||||
// Dynamic mode
|
||||
assert_eq!(dynamic.apply_to_display(""), "");
|
||||
assert_eq!(dynamic.apply_to_display("1234567890"), "(123) 456-7890");
|
||||
|
||||
// Template mode
|
||||
assert_eq!(template.apply_to_display(""), "(___) ___-____");
|
||||
assert_eq!(template.apply_to_display("123"), "(123) ___-____");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_user_defined_date_mask() {
|
||||
// User creates their own date formats
|
||||
let us_date = DisplayMask::new("##/##/####", '#');
|
||||
let eu_date = DisplayMask::new("##.##.####", '#');
|
||||
let iso_date = DisplayMask::new("####-##-##", '#');
|
||||
|
||||
assert_eq!(us_date.apply_to_display("12252024"), "12/25/2024");
|
||||
assert_eq!(eu_date.apply_to_display("25122024"), "25.12.2024");
|
||||
assert_eq!(iso_date.apply_to_display("20241225"), "2024-12-25");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_user_defined_business_formats() {
|
||||
// User creates custom business formats
|
||||
let employee_id = DisplayMask::new("EMP-####-##", '#');
|
||||
let product_code = DisplayMask::new("###-###-###", '#');
|
||||
let invoice = DisplayMask::new("INV####/##", '#');
|
||||
|
||||
assert_eq!(employee_id.apply_to_display("123456"), "EMP-1234-56");
|
||||
assert_eq!(product_code.apply_to_display("123456789"), "123-456-789");
|
||||
assert_eq!(invoice.apply_to_display("123456"), "INV1234/56");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_custom_input_characters() {
|
||||
// User can define their own input character
|
||||
let mask_with_x = DisplayMask::new("XXX-XX-XXXX", 'X');
|
||||
let mask_with_hash = DisplayMask::new("###-##-####", '#');
|
||||
let mask_with_n = DisplayMask::new("NNN-NN-NNNN", 'N');
|
||||
|
||||
assert_eq!(mask_with_x.apply_to_display("123456789"), "123-45-6789");
|
||||
assert_eq!(mask_with_hash.apply_to_display("123456789"), "123-45-6789");
|
||||
assert_eq!(mask_with_n.apply_to_display("123456789"), "123-45-6789");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_custom_placeholders() {
|
||||
// User can define custom placeholder characters
|
||||
let underscores = DisplayMask::new("##-##", '#').with_template('_');
|
||||
let dots = DisplayMask::new("##-##", '#').with_template('•');
|
||||
let dashes = DisplayMask::new("##-##", '#').with_template('-');
|
||||
|
||||
assert_eq!(underscores.apply_to_display(""), "__-__");
|
||||
assert_eq!(dots.apply_to_display(""), "••-••");
|
||||
assert_eq!(dashes.apply_to_display(""), "---"); // Note: dashes blend with separator
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_position_mapping_user_patterns() {
|
||||
let custom = DisplayMask::new("ABC-###-XYZ", '#');
|
||||
|
||||
// Position mapping should work correctly with any pattern
|
||||
assert_eq!(custom.raw_pos_to_display_pos(0), 4); // First # at position 4
|
||||
assert_eq!(custom.raw_pos_to_display_pos(1), 5); // Second # at position 5
|
||||
assert_eq!(custom.raw_pos_to_display_pos(2), 6); // Third # at position 6
|
||||
|
||||
assert_eq!(custom.display_pos_to_raw_pos(4), 0); // Position 4 -> first input
|
||||
assert_eq!(custom.display_pos_to_raw_pos(5), 1); // Position 5 -> second input
|
||||
assert_eq!(custom.display_pos_to_raw_pos(6), 2); // Position 6 -> third input
|
||||
|
||||
assert!(!custom.is_input_position(0)); // A
|
||||
assert!(!custom.is_input_position(3)); // -
|
||||
assert!(custom.is_input_position(4)); // #
|
||||
assert!(!custom.is_input_position(8)); // Y
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,28 @@
|
||||
// src/validation/mod.rs
|
||||
//! Validation module for canvas form fields
|
||||
|
||||
// Core validation modules
|
||||
pub mod config;
|
||||
pub mod limits;
|
||||
pub mod state;
|
||||
pub mod patterns;
|
||||
pub mod mask; // Simple display mask instead of complex reserved chars
|
||||
|
||||
// Re-export main types
|
||||
pub use config::{ValidationConfig, ValidationResult, ValidationConfigBuilder};
|
||||
pub use limits::{CharacterLimits, LimitCheckResult};
|
||||
pub use state::{ValidationState, ValidationSummary};
|
||||
pub use patterns::{PatternFilters, PositionFilter, PositionRange, CharacterFilter};
|
||||
pub use mask::DisplayMask; // Simple mask instead of ReservedCharacters
|
||||
|
||||
/// Validation error types
|
||||
#[derive(Debug, Clone, thiserror::Error)]
|
||||
pub enum ValidationError {
|
||||
#[error("Character limit exceeded: {current}/{max}")]
|
||||
CharacterLimitExceeded { current: usize, max: usize },
|
||||
|
||||
#[error("Invalid character '{char}' at position {position}")]
|
||||
InvalidCharacter { char: char, position: usize },
|
||||
#[error("Character limit exceeded: {message}")]
|
||||
LimitExceeded { message: String },
|
||||
|
||||
#[error("Pattern validation failed: {message}")]
|
||||
PatternValidationFailed { message: String },
|
||||
PatternFailed { message: String },
|
||||
|
||||
#[error("Validation configuration error: {message}")]
|
||||
ConfigurationError { message: String },
|
||||
#[error("Custom validation failed: {message}")]
|
||||
CustomFailed { message: String },
|
||||
}
|
||||
|
||||
/// Result type for validation operations
|
||||
pub type Result<T> = std::result::Result<T, ValidationError>;
|
||||
|
||||
Reference in New Issue
Block a user