diff --git a/canvas/Cargo.toml b/canvas/Cargo.toml index aed97f6..4722f81 100644 --- a/canvas/Cargo.toml +++ b/canvas/Cargo.toml @@ -16,7 +16,7 @@ crossterm = { workspace = true, optional = true } anyhow.workspace = true tokio = { workspace = true, optional = true } toml = { workspace = true } -serde = { workspace = true } +serde.workspace = true unicode-width.workspace = true thiserror = { workspace = true } diff --git a/canvas/examples/validation_patterns.rs b/canvas/examples/validation_patterns.rs new file mode 100644 index 0000000..3f3042d --- /dev/null +++ b/canvas/examples/validation_patterns.rs @@ -0,0 +1,290 @@ +// examples/validation_patterns.rs +//! Example demonstrating position-based pattern filtering +//! +//! Run with: cargo run --example validation_patterns --features validation + +use canvas::{ + prelude::*, + validation::{ValidationConfigBuilder, PatternFilters, PositionFilter, PositionRange, CharacterFilter}, +}; + +#[derive(Debug)] +struct DocumentForm { + license_plate: String, + phone_number: String, + credit_card: String, + custom_id: String, +} + +impl DocumentForm { + fn new() -> Self { + Self { + license_plate: String::new(), + phone_number: String::new(), + credit_card: String::new(), + custom_id: String::new(), + } + } +} + +impl DataProvider for DocumentForm { + fn field_count(&self) -> usize { + 4 + } + + fn field_name(&self, index: usize) -> &str { + match index { + 0 => "License Plate", + 1 => "Phone Number", + 2 => "Credit Card", + 3 => "Custom ID", + _ => "", + } + } + + fn field_value(&self, index: usize) -> &str { + match index { + 0 => &self.license_plate, + 1 => &self.phone_number, + 2 => &self.credit_card, + 3 => &self.custom_id, + _ => "", + } + } + + fn set_field_value(&mut self, index: usize, value: String) { + match index { + 0 => self.license_plate = value, + 1 => self.phone_number = value, + 2 => self.credit_card = value, + 3 => self.custom_id = value, + _ => {} + } + } + + fn validation_config(&self, field_index: usize) -> Option { + match field_index { + 0 => { + // License plate: AB123 (2 letters, 3 numbers) - USER DEFINED + let license_plate_pattern = PatternFilters::new() + .add_filter(PositionFilter::new( + PositionRange::Range(0, 1), + CharacterFilter::Alphabetic, + )) + .add_filter(PositionFilter::new( + PositionRange::Range(2, 4), + CharacterFilter::Numeric, + )); + + Some(ValidationConfigBuilder::new() + .with_pattern_filters(license_plate_pattern) + .build()) + } + 1 => { + // Phone number: 123-456-7890 - USER DEFINED + let phone_pattern = PatternFilters::new() + .add_filter(PositionFilter::new( + PositionRange::Multiple(vec![0,1,2,4,5,6,8,9,10,11]), + CharacterFilter::Numeric, + )) + .add_filter(PositionFilter::new( + PositionRange::Multiple(vec![3, 7]), + CharacterFilter::Exact('-'), + )); + + Some(ValidationConfigBuilder::new() + .with_pattern_filters(phone_pattern) + .build()) + } + 2 => { + // Credit card: 1234-5678-9012-3456 - USER DEFINED + let credit_card_pattern = PatternFilters::new() + .add_filter(PositionFilter::new( + PositionRange::Multiple(vec![0,1,2,3,5,6,7,8,10,11,12,13,15,16,17,18]), + CharacterFilter::Numeric, + )) + .add_filter(PositionFilter::new( + PositionRange::Multiple(vec![4, 9, 14]), + CharacterFilter::Exact('-'), + )); + + Some(ValidationConfigBuilder::new() + .with_pattern_filters(credit_card_pattern) + .build()) + } + 3 => { + // Custom ID: First 2 letters, rest alphanumeric - USER DEFINED + let custom_id_pattern = PatternFilters::new() + .add_filter(PositionFilter::new( + PositionRange::Range(0, 1), + CharacterFilter::Alphabetic, + )) + .add_filter(PositionFilter::new( + PositionRange::From(2), + CharacterFilter::Alphanumeric, + )); + + Some(ValidationConfigBuilder::new() + .with_pattern_filters(custom_id_pattern) + .build()) + } + _ => None, + } + } +} + +fn main() -> Result<(), Box> { + println!("🎯 Canvas Pattern Filtering Demo"); + println!("================================="); + println!(); + + let form = DocumentForm::new(); + let mut editor = FormEditor::new(form); + + println!("📋 Form initialized with USER-DEFINED pattern validation rules:"); + for i in 0..editor.data_provider().field_count() { + let field_name = editor.data_provider().field_name(i); + println!(" • {}: Position-based pattern filtering (user-defined)", field_name); + } + println!(); + + // Test License Plate (Field 0) + println!("1. Testing USER-DEFINED License Plate pattern (AB123 - 2 letters, 3 numbers):"); + + // Valid license plate + println!(" Entering valid license plate 'AB123':"); + for ch in "AB123".chars() { + match editor.insert_char(ch) { + Ok(_) => println!(" '{}' ✓ accepted", ch), + Err(e) => println!(" '{}' ✗ rejected: {}", ch, e), + } + } + println!(" Result: '{}'", editor.current_text()); + println!(); + + // Clear and test invalid pattern + editor.clear_current_field(); + println!(" Testing invalid pattern 'A1123':"); + for (i, ch) in "A1123".chars().enumerate() { + match editor.insert_char(ch) { + Ok(_) => println!(" Position {}: '{}' ✓ accepted", i, ch), + Err(e) => println!(" Position {}: '{}' ✗ rejected: {}", i, ch, e), + } + } + println!(" Result: '{}'", editor.current_text()); + println!(); + + // Move to phone number field + editor.move_to_next_field()?; + + // Test Phone Number (Field 1) + println!("2. Testing USER-DEFINED Phone Number pattern (123-456-7890):"); + + // Valid phone number + println!(" Entering valid phone number '123-456-7890':"); + for (i, ch) in "123-456-7890".chars().enumerate() { + match editor.insert_char(ch) { + Ok(_) => println!(" Position {}: '{}' ✓ accepted", i, ch), + Err(e) => println!(" Position {}: '{}' ✗ rejected: {}", i, ch, e), + } + } + println!(" Result: '{}'", editor.current_text()); + println!(); + + // Move to credit card field + editor.move_to_next_field()?; + + // Test Credit Card (Field 2) + println!("3. Testing USER-DEFINED Credit Card pattern (1234-5678-9012-3456):"); + + // Valid credit card (first few characters) + println!(" Entering valid credit card start '1234-56':"); + for (i, ch) in "1234-56".chars().enumerate() { + match editor.insert_char(ch) { + Ok(_) => println!(" Position {}: '{}' ✓ accepted", i, ch), + Err(e) => println!(" Position {}: '{}' ✗ rejected: {}", i, ch, e), + } + } + println!(" Result: '{}'", editor.current_text()); + println!(); + + // Test invalid character at dash position + println!(" Testing invalid character at dash position:"); + editor.clear_current_field(); + for (i, ch) in "1234A56".chars().enumerate() { + match editor.insert_char(ch) { + Ok(_) => println!(" Position {}: '{}' ✓ accepted", i, ch), + Err(e) => println!(" Position {}: '{}' ✗ rejected: {}", i, ch, e), + } + } + println!(" Result: '{}'", editor.current_text()); + println!(); + + // Move to custom ID field + editor.move_to_next_field()?; + + // Test Custom ID (Field 3) + println!("4. Testing USER-DEFINED Custom ID pattern (2 letters + alphanumeric):"); + + // Valid custom ID + println!(" Entering valid custom ID 'AB123def':"); + for (i, ch) in "AB123def".chars().enumerate() { + match editor.insert_char(ch) { + Ok(_) => println!(" Position {}: '{}' ✓ accepted", i, ch), + Err(e) => println!(" Position {}: '{}' ✗ rejected: {}", i, ch, e), + } + } + println!(" Result: '{}'", editor.current_text()); + println!(); + + // Test invalid pattern + editor.clear_current_field(); + println!(" Testing invalid pattern '1B123def' (number in first position):"); + for (i, ch) in "1B123def".chars().enumerate() { + match editor.insert_char(ch) { + Ok(_) => println!(" Position {}: '{}' ✓ accepted", i, ch), + Err(e) => println!(" Position {}: '{}' ✗ rejected: {}", i, ch, e), + } + } + println!(" Result: '{}'", editor.current_text()); + println!(); + + // Show validation summary + println!("📊 Final validation summary:"); + let summary = editor.validation_summary(); + println!(" Total fields with validation: {}", summary.total_fields); + println!(" Validated fields: {}", summary.validated_fields); + println!(" Valid fields: {}", summary.valid_fields); + println!(" Fields with warnings: {}", summary.warning_fields); + println!(" Fields with errors: {}", summary.error_fields); + println!(); + + // Show field-by-field status + println!("📝 Field-by-field validation status:"); + for i in 0..editor.data_provider().field_count() { + let field_name = editor.data_provider().field_name(i); + let field_value = editor.data_provider().field_value(i); + + if let Some(result) = editor.field_validation(i) { + println!(" {} [{}]: {} - {:?}", + field_name, + field_value, + if result.is_acceptable() { "✓" } else { "✗" }, + result + ); + } else { + println!(" {} [{}]: (not validated)", field_name, field_value); + } + } + + println!(); + println!("✨ USER-DEFINED Pattern filtering demo completed!"); + println!("Key Features Demonstrated:"); + println!(" • Position-specific character filtering (USER DEFINES PATTERNS)"); + println!(" • Library provides CharacterFilter: Alphabetic, Numeric, Alphanumeric, Exact, OneOf, Custom"); + println!(" • User defines all patterns using library's building blocks"); + println!(" • Real-time validation during typing"); + println!(" • Flexible position ranges (single, range, from, multiple)"); + + Ok(()) +} diff --git a/canvas/src/lib.rs b/canvas/src/lib.rs index e3356f9..71b380a 100644 --- a/canvas/src/lib.rs +++ b/canvas/src/lib.rs @@ -35,7 +35,7 @@ pub use canvas::actions::{CanvasAction, ActionResult}; pub use validation::{ ValidationConfig, ValidationResult, ValidationError, CharacterLimits, ValidationConfigBuilder, ValidationState, - ValidationSummary, + ValidationSummary, PatternFilters, PositionFilter, PositionRange, CharacterFilter, }; // Theming and GUI diff --git a/canvas/src/validation/config.rs b/canvas/src/validation/config.rs index e37b418..45387f5 100644 --- a/canvas/src/validation/config.rs +++ b/canvas/src/validation/config.rs @@ -1,29 +1,24 @@ // src/validation/config.rs //! Validation configuration types and builders -use crate::validation::CharacterLimits; -use serde::{Deserialize, Serialize}; +use crate::validation::{CharacterLimits, PatternFilters}; /// Main validation configuration for a field -#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[derive(Debug, Clone, Default)] pub struct ValidationConfig { /// Character limit configuration pub character_limits: Option, - /// Future: Predefined patterns - #[serde(skip)] - pub patterns: Option<()>, // Placeholder for future implementation + /// Pattern filtering configuration + pub pattern_filters: Option, /// Future: Reserved characters - #[serde(skip)] pub reserved_chars: Option<()>, // Placeholder for future implementation /// Future: Custom formatting - #[serde(skip)] pub custom_formatting: Option<()>, // Placeholder for future implementation /// Future: External validation - #[serde(skip)] pub external_validation: Option<()>, // Placeholder for future implementation } @@ -45,6 +40,12 @@ impl ValidationConfigBuilder { 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 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)); @@ -114,6 +115,13 @@ impl ValidationConfig { .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 pub fn validate_char_insertion( &self, @@ -130,6 +138,13 @@ 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 @@ -146,6 +161,13 @@ 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 @@ -153,8 +175,7 @@ impl ValidationConfig { /// Check if any validation rules are configured pub fn has_validation(&self) -> bool { - self.character_limits.is_some() - // || self.patterns.is_some() + 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() @@ -232,4 +253,48 @@ mod tests { 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 + 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()); + } } diff --git a/canvas/src/validation/mod.rs b/canvas/src/validation/mod.rs index d1d3261..334d3c7 100644 --- a/canvas/src/validation/mod.rs +++ b/canvas/src/validation/mod.rs @@ -1,13 +1,16 @@ +// src/validation/mod.rs //! Validation module for canvas form fields pub mod config; pub mod limits; pub mod state; +pub mod patterns; // 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}; /// Validation error types #[derive(Debug, Clone, thiserror::Error)] @@ -18,6 +21,9 @@ pub enum ValidationError { #[error("Invalid character '{char}' at position {position}")] InvalidCharacter { char: char, position: usize }, + #[error("Pattern validation failed: {message}")] + PatternValidationFailed { message: String }, + #[error("Validation configuration error: {message}")] ConfigurationError { message: String }, } diff --git a/canvas/src/validation/patterns.rs b/canvas/src/validation/patterns.rs new file mode 100644 index 0000000..5940112 --- /dev/null +++ b/canvas/src/validation/patterns.rs @@ -0,0 +1,299 @@ +// src/validation/patterns.rs +//! Position-based pattern filtering for validation + +use serde::{Deserialize, Serialize}; + +/// A filter that applies to specific character positions in a field +#[derive(Debug, Clone)] +pub struct PositionFilter { + /// Which positions this filter applies to + pub positions: PositionRange, + /// What type of character filter to apply + pub filter: CharacterFilter, +} + +/// Defines which character positions a filter applies to +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum PositionRange { + /// Single position (e.g., position 3 only) + Single(usize), + /// Range of positions (e.g., positions 0-2, inclusive) + Range(usize, usize), + /// From position onwards (e.g., position 4 and beyond) + From(usize), + /// Multiple specific positions (e.g., positions 0, 2, 5) + Multiple(Vec), +} + +/// Types of character filters that can be applied +#[derive(Debug, Clone)] +pub enum CharacterFilter { + /// Allow only alphabetic characters (a-z, A-Z) + Alphabetic, + /// Allow only numeric characters (0-9) + Numeric, + /// Allow alphanumeric characters (a-z, A-Z, 0-9) + Alphanumeric, + /// Allow only exact character match + Exact(char), + /// Allow any character from the provided set + OneOf(Vec), + /// Custom user-defined filter function + Custom(Box bool + Send + Sync>), +} + +impl PositionRange { + /// Check if a position is included in this range + pub fn contains(&self, position: usize) -> bool { + match self { + PositionRange::Single(pos) => position == *pos, + PositionRange::Range(start, end) => position >= *start && position <= *end, + PositionRange::From(start) => position >= *start, + PositionRange::Multiple(positions) => positions.contains(&position), + } + } + + /// Get all positions up to a given length that this range covers + pub fn positions_up_to(&self, max_length: usize) -> Vec { + match self { + PositionRange::Single(pos) => { + if *pos < max_length { vec![*pos] } else { vec![] } + }, + PositionRange::Range(start, end) => { + let actual_end = (*end).min(max_length.saturating_sub(1)); + if *start <= actual_end { + (*start..=actual_end).collect() + } else { + vec![] + } + }, + PositionRange::From(start) => { + if *start < max_length { + (*start..max_length).collect() + } else { + vec![] + } + }, + PositionRange::Multiple(positions) => { + positions.iter() + .filter(|&&pos| pos < max_length) + .copied() + .collect() + }, + } + } +} + +impl CharacterFilter { + /// Test if a character passes this filter + pub fn accepts(&self, ch: char) -> bool { + match self { + CharacterFilter::Alphabetic => ch.is_alphabetic(), + CharacterFilter::Numeric => ch.is_numeric(), + CharacterFilter::Alphanumeric => ch.is_alphanumeric(), + CharacterFilter::Exact(expected) => ch == *expected, + CharacterFilter::OneOf(chars) => chars.contains(&ch), + CharacterFilter::Custom(func) => func(ch), + } + } + + /// Get a human-readable description of this filter + pub fn description(&self) -> String { + match self { + CharacterFilter::Alphabetic => "alphabetic characters (a-z, A-Z)".to_string(), + CharacterFilter::Numeric => "numeric characters (0-9)".to_string(), + CharacterFilter::Alphanumeric => "alphanumeric characters (a-z, A-Z, 0-9)".to_string(), + CharacterFilter::Exact(ch) => format!("exactly '{}'", ch), + CharacterFilter::OneOf(chars) => { + let char_list: String = chars.iter().collect(); + format!("one of: {}", char_list) + }, + CharacterFilter::Custom(_) => "custom filter".to_string(), + } + } +} + +impl PositionFilter { + /// Create a new position filter + pub fn new(positions: PositionRange, filter: CharacterFilter) -> Self { + Self { positions, filter } + } + + /// Validate a character at a specific position + pub fn validate_position(&self, position: usize, character: char) -> bool { + if self.positions.contains(position) { + self.filter.accepts(character) + } else { + true // Position not covered by this filter, allow any character + } + } + + /// Get error message for invalid character at position + pub fn error_message(&self, position: usize, character: char) -> Option { + if self.positions.contains(position) && !self.filter.accepts(character) { + Some(format!( + "Position {} requires {} but got '{}'", + position, + self.filter.description(), + character + )) + } else { + None + } + } +} + +/// A collection of position filters for a field +#[derive(Debug, Clone, Default)] +pub struct PatternFilters { + filters: Vec, +} + +impl PatternFilters { + /// Create empty pattern filters + pub fn new() -> Self { + Self::default() + } + + /// Add a position filter + pub fn add_filter(mut self, filter: PositionFilter) -> Self { + self.filters.push(filter); + self + } + + /// Add multiple filters + pub fn add_filters(mut self, filters: Vec) -> Self { + self.filters.extend(filters); + self + } + + /// Validate a character at a specific position against all applicable filters + pub fn validate_char_at_position(&self, position: usize, character: char) -> Result<(), String> { + for filter in &self.filters { + if let Some(error) = filter.error_message(position, character) { + return Err(error); + } + } + Ok(()) + } + + /// Validate entire text against all filters + pub fn validate_text(&self, text: &str) -> Result<(), String> { + for (position, character) in text.char_indices() { + if let Err(error) = self.validate_char_at_position(position, character) { + return Err(error); + } + } + Ok(()) + } + + /// Check if any filters are configured + pub fn has_filters(&self) -> bool { + !self.filters.is_empty() + } + + /// Get all configured filters + pub fn filters(&self) -> &[PositionFilter] { + &self.filters + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_position_range_contains() { + assert!(PositionRange::Single(3).contains(3)); + assert!(!PositionRange::Single(3).contains(2)); + + assert!(PositionRange::Range(1, 4).contains(3)); + assert!(!PositionRange::Range(1, 4).contains(5)); + + assert!(PositionRange::From(2).contains(5)); + assert!(!PositionRange::From(2).contains(1)); + + assert!(PositionRange::Multiple(vec![0, 2, 5]).contains(2)); + assert!(!PositionRange::Multiple(vec![0, 2, 5]).contains(3)); + } + + #[test] + fn test_position_range_positions_up_to() { + assert_eq!(PositionRange::Single(3).positions_up_to(5), vec![3]); + assert_eq!(PositionRange::Single(5).positions_up_to(3), vec![]); + + assert_eq!(PositionRange::Range(1, 3).positions_up_to(5), vec![1, 2, 3]); + assert_eq!(PositionRange::Range(1, 5).positions_up_to(3), vec![1, 2]); + + assert_eq!(PositionRange::From(2).positions_up_to(5), vec![2, 3, 4]); + + assert_eq!(PositionRange::Multiple(vec![0, 2, 5]).positions_up_to(4), vec![0, 2]); + } + + #[test] + fn test_character_filter_accepts() { + assert!(CharacterFilter::Alphabetic.accepts('a')); + assert!(CharacterFilter::Alphabetic.accepts('Z')); + assert!(!CharacterFilter::Alphabetic.accepts('1')); + + assert!(CharacterFilter::Numeric.accepts('5')); + assert!(!CharacterFilter::Numeric.accepts('a')); + + assert!(CharacterFilter::Alphanumeric.accepts('a')); + assert!(CharacterFilter::Alphanumeric.accepts('5')); + assert!(!CharacterFilter::Alphanumeric.accepts('-')); + + assert!(CharacterFilter::Exact('x').accepts('x')); + assert!(!CharacterFilter::Exact('x').accepts('y')); + + assert!(CharacterFilter::OneOf(vec!['a', 'b', 'c']).accepts('b')); + assert!(!CharacterFilter::OneOf(vec!['a', 'b', 'c']).accepts('d')); + } + + #[test] + fn test_position_filter_validation() { + let filter = PositionFilter::new( + PositionRange::Range(0, 1), + CharacterFilter::Alphabetic, + ); + + assert!(filter.validate_position(0, 'A')); + assert!(filter.validate_position(1, 'b')); + assert!(!filter.validate_position(0, '1')); + assert!(filter.validate_position(2, '1')); // Position 2 not covered, allow anything + } + + #[test] + fn test_pattern_filters_validation() { + let patterns = PatternFilters::new() + .add_filter(PositionFilter::new( + PositionRange::Range(0, 1), + CharacterFilter::Alphabetic, + )) + .add_filter(PositionFilter::new( + PositionRange::Range(2, 4), + CharacterFilter::Numeric, + )); + + // Valid pattern: AB123 + assert!(patterns.validate_text("AB123").is_ok()); + + // Invalid: number in alphabetic position + assert!(patterns.validate_text("A1123").is_err()); + + // Invalid: letter in numeric position + assert!(patterns.validate_text("AB1A3").is_err()); + } + + #[test] + fn test_custom_filter() { + let pattern = PatternFilters::new() + .add_filter(PositionFilter::new( + PositionRange::From(0), + CharacterFilter::Custom(Box::new(|c| c.is_lowercase())), + )); + + assert!(pattern.validate_text("hello").is_ok()); + assert!(pattern.validate_text("Hello").is_err()); // Uppercase not allowed + } +}