diff --git a/canvas/examples/validation_2.rs b/canvas/examples/validation_2.rs new file mode 100644 index 0000000..e720149 --- /dev/null +++ b/canvas/examples/validation_2.rs @@ -0,0 +1,724 @@ +// examples/validation_patterns_tui.rs +//! TUI Example demonstrating position-based pattern filtering +//! +//! Run with: cargo run --example validation_patterns_tui --features "validation,gui" + +// REQUIRE validation and gui features - example won't compile without them +#[cfg(not(all(feature = "validation", feature = "gui")))] +compile_error!( + "This example requires the 'validation' and 'gui' features. \ + Run with: cargo run --example validation_patterns_tui --features \"validation,gui\"" +); + +use std::io; +use canvas::ValidationResult; +use crossterm::{ + event::{ + self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers, + }, + execute, + terminal::{ + disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, + }, +}; +use ratatui::{ + backend::{Backend, CrosstermBackend}, + layout::{Constraint, Direction, Layout, Rect}, + style::{Color, Style}, + text::{Line, Span}, + widgets::{Block, Borders, Paragraph, Wrap}, + Frame, Terminal, +}; + +use canvas::{ + canvas::{ + gui::render_canvas_default, + modes::AppMode, + }, + DataProvider, FormEditor, + ValidationConfig, ValidationConfigBuilder, PatternFilters, PositionFilter, PositionRange, CharacterFilter, +}; + +// Enhanced FormEditor for pattern validation demonstration +struct PatternValidationFormEditor { + editor: FormEditor, + debug_message: String, + command_buffer: String, + validation_enabled: bool, + field_switch_blocked: bool, + block_reason: Option, +} + +impl PatternValidationFormEditor { + fn new(data_provider: D) -> Self { + let mut editor = FormEditor::new(data_provider); + + // Enable validation by default + editor.set_validation_enabled(true); + + Self { + editor, + debug_message: "šŸ” Pattern Validation Demo - Try typing in different fields!".to_string(), + command_buffer: String::new(), + validation_enabled: true, + field_switch_blocked: false, + block_reason: None, + } + } + + // === COMMAND BUFFER HANDLING === + fn clear_command_buffer(&mut self) { + self.command_buffer.clear(); + } + + fn add_to_command_buffer(&mut self, ch: char) { + self.command_buffer.push(ch); + } + + fn get_command_buffer(&self) -> &str { + &self.command_buffer + } + + fn has_pending_command(&self) -> bool { + !self.command_buffer.is_empty() + } + + // === VALIDATION CONTROL === + fn toggle_validation(&mut self) { + self.validation_enabled = !self.validation_enabled; + self.editor.set_validation_enabled(self.validation_enabled); + + if self.validation_enabled { + self.debug_message = "āœ… Pattern Validation ENABLED".to_string(); + } else { + self.debug_message = "āŒ Pattern Validation DISABLED".to_string(); + } + } + + // === MOVEMENT === + fn move_left(&mut self) { + self.editor.move_left(); + self.field_switch_blocked = false; + self.block_reason = None; + } + + fn move_right(&mut self) { + self.editor.move_right(); + self.field_switch_blocked = false; + self.block_reason = None; + } + + fn move_up(&mut self) { + match self.editor.move_up() { + Ok(()) => { + self.update_field_validation_status(); + self.field_switch_blocked = false; + self.block_reason = None; + } + Err(e) => { + self.field_switch_blocked = true; + self.block_reason = Some(e.to_string()); + self.debug_message = format!("🚫 Field switch blocked: {}", e); + } + } + } + + fn move_down(&mut self) { + match self.editor.move_down() { + Ok(()) => { + self.update_field_validation_status(); + self.field_switch_blocked = false; + self.block_reason = None; + } + Err(e) => { + self.field_switch_blocked = true; + self.block_reason = Some(e.to_string()); + self.debug_message = format!("🚫 Field switch blocked: {}", e); + } + } + } + + fn move_line_start(&mut self) { + self.editor.move_line_start(); + } + + fn move_line_end(&mut self) { + self.editor.move_line_end(); + } + + // === MODE TRANSITIONS === + fn enter_edit_mode(&mut self) { + self.editor.enter_edit_mode(); + self.debug_message = "āœļø INSERT MODE - Type to test pattern validation".to_string(); + } + + fn enter_append_mode(&mut self) { + self.editor.enter_append_mode(); + self.debug_message = "āœļø INSERT (append) - Pattern validation active".to_string(); + } + + fn exit_edit_mode(&mut self) { + self.editor.exit_edit_mode(); + self.debug_message = "šŸ”’ NORMAL MODE".to_string(); + self.update_field_validation_status(); + } + + fn insert_char(&mut self, ch: char) -> anyhow::Result<()> { + let result = self.editor.insert_char(ch); + if result.is_ok() { + // Show real-time validation feedback + if let Some(validation_result) = self.editor.current_field_validation() { + match validation_result { + ValidationResult::Valid => { + self.debug_message = "āœ… Valid character".to_string(); + } + ValidationResult::Warning { message } => { + self.debug_message = format!("āš ļø Warning: {}", message); + } + ValidationResult::Error { message } => { + self.debug_message = format!("āŒ Error: {}", message); + } + } + } + } + Ok(result?) + } + + // === DELETE OPERATIONS === + fn delete_backward(&mut self) -> anyhow::Result<()> { + let result = self.editor.delete_backward(); + if result.is_ok() { + self.debug_message = "⌫ Deleted character".to_string(); + } + Ok(result?) + } + + fn delete_forward(&mut self) -> anyhow::Result<()> { + let result = self.editor.delete_forward(); + if result.is_ok() { + self.debug_message = "⌦ Deleted character".to_string(); + } + Ok(result?) + } + + // === DELEGATE TO ORIGINAL EDITOR === + fn current_field(&self) -> usize { + self.editor.current_field() + } + + fn cursor_position(&self) -> usize { + self.editor.cursor_position() + } + + fn mode(&self) -> AppMode { + self.editor.mode() + } + + fn current_text(&self) -> &str { + self.editor.current_text() + } + + fn data_provider(&self) -> &D { + self.editor.data_provider() + } + + fn ui_state(&self) -> &canvas::EditorState { + self.editor.ui_state() + } + + fn set_mode(&mut self, mode: AppMode) { + self.editor.set_mode(mode); + } + + fn next_field(&mut self) { + match self.editor.next_field() { + Ok(()) => { + self.update_field_validation_status(); + self.field_switch_blocked = false; + self.block_reason = None; + } + Err(e) => { + self.field_switch_blocked = true; + self.block_reason = Some(e.to_string()); + self.debug_message = format!("🚫 Cannot move to next field: {}", e); + } + } + } + + fn prev_field(&mut self) { + match self.editor.prev_field() { + Ok(()) => { + self.update_field_validation_status(); + self.field_switch_blocked = false; + self.block_reason = None; + } + Err(e) => { + self.field_switch_blocked = true; + self.block_reason = Some(e.to_string()); + self.debug_message = format!("🚫 Cannot move to previous field: {}", e); + } + } + } + + // === STATUS AND DEBUG === + fn set_debug_message(&mut self, msg: String) { + self.debug_message = msg; + } + + fn debug_message(&self) -> &str { + &self.debug_message + } + + fn update_field_validation_status(&mut self) { + if !self.validation_enabled { + return; + } + + if let Some(result) = self.editor.current_field_validation() { + match result { + ValidationResult::Valid => { + self.debug_message = format!("Field {}: āœ… Valid", self.editor.current_field() + 1); + } + ValidationResult::Warning { message } => { + self.debug_message = format!("Field {}: āš ļø {}", self.editor.current_field() + 1, message); + } + ValidationResult::Error { message } => { + self.debug_message = format!("Field {}: āŒ {}", self.editor.current_field() + 1, message); + } + } + } else { + self.debug_message = format!("Field {}: šŸ” Not validated yet", self.editor.current_field() + 1); + } + } + + fn get_validation_status(&self) -> String { + if !self.validation_enabled { + return "āŒ DISABLED".to_string(); + } + + if self.field_switch_blocked { + return "🚫 SWITCH BLOCKED".to_string(); + } + + let summary = self.editor.validation_summary(); + if summary.has_errors() { + format!("āŒ {} ERRORS", summary.error_fields) + } else if summary.has_warnings() { + format!("āš ļø {} WARNINGS", summary.warning_fields) + } else if summary.validated_fields > 0 { + format!("āœ… {} VALID", summary.valid_fields) + } else { + "šŸ” READY".to_string() + } + } +} + +// Demo form with pattern-based validation +struct PatternValidationData { + fields: Vec<(String, String)>, +} + +impl PatternValidationData { + fn new() -> Self { + Self { + fields: vec![ + ("šŸš— License Plate (AB123)".to_string(), "".to_string()), + ("šŸ“ž Phone (123-456-7890)".to_string(), "".to_string()), + ("šŸ’³ Credit Card (1234-5678-9012-3456)".to_string(), "".to_string()), + ("šŸ†” Custom ID (AB123def)".to_string(), "".to_string()), + ], + } + } +} + +impl DataProvider for PatternValidationData { + fn field_count(&self) -> usize { + self.fields.len() + } + + fn field_name(&self, index: usize) -> &str { + &self.fields[index].0 + } + + fn field_value(&self, index: usize) -> &str { + &self.fields[index].1 + } + + fn set_field_value(&mut self, index: usize, value: String) { + self.fields[index].1 = value; + } + + // Pattern validation configuration per field + fn validation_config(&self, field_index: usize) -> Option { + match field_index { + 0 => { + // License plate: AB123 (2 letters, 3 numbers) + 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 + 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 + 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 + 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, + } + } +} + +/// Handle key presses with pattern validation commands +fn handle_key_press( + key: KeyCode, + modifiers: KeyModifiers, + editor: &mut PatternValidationFormEditor, +) -> anyhow::Result { + let mode = editor.mode(); + + // Quit handling + if (key == KeyCode::Char('q') && modifiers.contains(KeyModifiers::CONTROL)) + || (key == KeyCode::Char('c') && modifiers.contains(KeyModifiers::CONTROL)) + || key == KeyCode::F(10) + { + return Ok(false); + } + + match (mode, key, modifiers) { + // === MODE TRANSITIONS === + (AppMode::ReadOnly, KeyCode::Char('i'), _) => { + editor.enter_edit_mode(); + editor.clear_command_buffer(); + } + (AppMode::ReadOnly, KeyCode::Char('a'), _) => { + editor.enter_append_mode(); + editor.clear_command_buffer(); + } + (AppMode::ReadOnly, KeyCode::Char('A'), _) => { + editor.move_line_end(); + editor.enter_edit_mode(); + editor.clear_command_buffer(); + } + + // Escape: Exit edit mode + (_, KeyCode::Esc, _) => { + if mode == AppMode::Edit { + editor.exit_edit_mode(); + } else { + editor.clear_command_buffer(); + } + } + + // === VALIDATION COMMANDS === + (AppMode::ReadOnly, KeyCode::F(1), _) => { + editor.toggle_validation(); + } + + // === MOVEMENT === + (AppMode::ReadOnly, KeyCode::Char('h'), _) | (AppMode::ReadOnly, KeyCode::Left, _) => { + editor.move_left(); + editor.clear_command_buffer(); + } + (AppMode::ReadOnly, KeyCode::Char('l'), _) | (AppMode::ReadOnly, KeyCode::Right, _) => { + editor.move_right(); + editor.clear_command_buffer(); + } + (AppMode::ReadOnly, KeyCode::Char('j'), _) | (AppMode::ReadOnly, KeyCode::Down, _) => { + editor.move_down(); + editor.clear_command_buffer(); + } + (AppMode::ReadOnly, KeyCode::Char('k'), _) | (AppMode::ReadOnly, KeyCode::Up, _) => { + editor.move_up(); + editor.clear_command_buffer(); + } + + // === EDIT MODE MOVEMENT === + (AppMode::Edit, KeyCode::Left, _) => { + editor.move_left(); + } + (AppMode::Edit, KeyCode::Right, _) => { + editor.move_right(); + } + (AppMode::Edit, KeyCode::Up, _) => { + editor.move_up(); + } + (AppMode::Edit, KeyCode::Down, _) => { + editor.move_down(); + } + + // === DELETE OPERATIONS === + (AppMode::Edit, KeyCode::Backspace, _) => { + editor.delete_backward()?; + } + (AppMode::Edit, KeyCode::Delete, _) => { + editor.delete_forward()?; + } + + // === TAB NAVIGATION === + (_, KeyCode::Tab, _) => { + editor.next_field(); + } + (_, KeyCode::BackTab, _) => { + editor.prev_field(); + } + + // === CHARACTER INPUT === + (AppMode::Edit, KeyCode::Char(c), m) if !m.contains(KeyModifiers::CONTROL) => { + editor.insert_char(c)?; + } + + // === DEBUG/INFO COMMANDS === + (AppMode::ReadOnly, KeyCode::Char('?'), _) => { + let summary = editor.editor.validation_summary(); + editor.set_debug_message(format!( + "Field {}/{}, Pos {}, Mode: {:?}, Validation: {} fields configured, {} validated", + editor.current_field() + 1, + editor.data_provider().field_count(), + editor.cursor_position(), + editor.mode(), + summary.total_fields, + summary.validated_fields + )); + } + + _ => { + if editor.has_pending_command() { + editor.clear_command_buffer(); + editor.set_debug_message("Invalid command sequence".to_string()); + } + } + } + + Ok(true) +} + +fn run_app( + terminal: &mut Terminal, + mut editor: PatternValidationFormEditor, +) -> io::Result<()> { + loop { + terminal.draw(|f| ui(f, &editor))?; + + if let Event::Key(key) = event::read()? { + match handle_key_press(key.code, key.modifiers, &mut editor) { + Ok(should_continue) => { + if !should_continue { + break; + } + } + Err(e) => { + editor.set_debug_message(format!("Error: {}", e)); + } + } + } + } + + Ok(()) +} + +fn ui(f: &mut Frame, editor: &PatternValidationFormEditor) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Min(8), Constraint::Length(12)]) + .split(f.area()); + + render_enhanced_canvas(f, chunks[0], editor); + render_validation_status(f, chunks[1], editor); +} + +fn render_enhanced_canvas( + f: &mut Frame, + area: Rect, + editor: &PatternValidationFormEditor, +) { + render_canvas_default(f, area, &editor.editor); +} + +fn render_validation_status( + f: &mut Frame, + area: Rect, + editor: &PatternValidationFormEditor, +) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(3), // Status bar + Constraint::Length(4), // Validation summary + Constraint::Length(5), // Help + ]) + .split(area); + + // Status bar with validation information + let mode_text = match editor.mode() { + AppMode::Edit => "INSERT", + AppMode::ReadOnly => "NORMAL", + _ => "OTHER", + }; + + let validation_status = editor.get_validation_status(); + let status_text = if editor.has_pending_command() { + format!("-- {} -- {} [{}] | Pattern Validation: {}", + mode_text, editor.debug_message(), editor.get_command_buffer(), validation_status) + } else { + format!("-- {} -- {} | Pattern Validation: {}", + mode_text, editor.debug_message(), validation_status) + }; + + let status = Paragraph::new(Line::from(Span::raw(status_text))) + .block(Block::default().borders(Borders::ALL).title("šŸ” Pattern Validation Status")); + + f.render_widget(status, chunks[0]); + + // Validation summary with field switching info + let summary = editor.editor.validation_summary(); + let summary_text = if editor.validation_enabled { + let switch_info = if editor.field_switch_blocked { + format!("\n🚫 Field switching blocked: {}", + editor.block_reason.as_deref().unwrap_or("Unknown reason")) + } else { + "\nāœ… Field switching allowed".to_string() + }; + + format!( + "šŸ“Š Pattern Validation Summary: {} fields configured, {} validated{}\n\ + āœ… Valid: {} āš ļø Warnings: {} āŒ Errors: {} šŸ“ˆ Progress: {:.0}%", + summary.total_fields, + summary.validated_fields, + switch_info, + summary.valid_fields, + summary.warning_fields, + summary.error_fields, + summary.completion_percentage() * 100.0 + ) + } else { + "āŒ Pattern validation is currently DISABLED\nPress F1 to enable validation".to_string() + }; + + let summary_style = if summary.has_errors() { + Style::default().fg(Color::Red) + } else if summary.has_warnings() { + Style::default().fg(Color::Yellow) + } else { + Style::default().fg(Color::Green) + }; + + let validation_summary = Paragraph::new(summary_text) + .block(Block::default().borders(Borders::ALL).title("šŸ“ˆ Pattern Validation Overview")) + .style(summary_style) + .wrap(Wrap { trim: true }); + + f.render_widget(validation_summary, chunks[1]); + + // Pattern-specific help text + let help_text = match editor.mode() { + AppMode::ReadOnly => { + "šŸ” PATTERN VALIDATION DEMO: Each field has specific character patterns!\n\ + License Plate: 2 letters + 3 numbers (AB123)\n\ + Phone: Numbers with dashes at positions 3 and 7 (123-456-7890)\n\ + Credit Card: Number groups separated by dashes (1234-5678-9012-3456)\n\ + Custom ID: 2 letters + alphanumeric (AB123def)\n\ + Movement: hjkl/arrows=move, Tab/Shift+Tab=fields, i/a=insert, F1=toggle validation" + } + AppMode::Edit => { + "āœļø INSERT MODE - Type to test pattern validation!\n\ + Pattern validation will reject characters that don't match the expected pattern\n\ + arrows=move, Backspace/Del=delete, Esc=normal, Tab=next field" + } + _ => "šŸ” Pattern Validation Demo Active!" + }; + + let help = Paragraph::new(help_text) + .block(Block::default().borders(Borders::ALL).title("šŸš€ Pattern Validation Commands")) + .style(Style::default().fg(Color::Gray)) + .wrap(Wrap { trim: true }); + + f.render_widget(help, chunks[2]); +} + +fn main() -> Result<(), Box> { + // Print feature status + println!("šŸ” Canvas Pattern Validation TUI Demo"); + println!("āœ… validation feature: ENABLED"); + println!("āœ… gui feature: ENABLED"); + println!("šŸš€ Pattern-based validation: ACTIVE"); + println!("šŸ“Š Try typing in fields with different patterns!"); + println!(); + + enable_raw_mode()?; + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + + let data = PatternValidationData::new(); + let editor = PatternValidationFormEditor::new(data); + + let res = run_app(&mut terminal, editor); + + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; + + if let Err(err) = res { + println!("{:?}", err); + } + + println!("šŸ” Pattern validation demo completed!"); + Ok(()) +} diff --git a/canvas/examples/validation_patterns.rs b/canvas/examples/validation_patterns.rs deleted file mode 100644 index 3f3042d..0000000 --- a/canvas/examples/validation_patterns.rs +++ /dev/null @@ -1,290 +0,0 @@ -// 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/validation/patterns.rs b/canvas/src/validation/patterns.rs index 5940112..91f07d5 100644 --- a/canvas/src/validation/patterns.rs +++ b/canvas/src/validation/patterns.rs @@ -2,6 +2,7 @@ //! Position-based pattern filtering for validation use serde::{Deserialize, Serialize}; +use std::sync::Arc; /// A filter that applies to specific character positions in a field #[derive(Debug, Clone)] @@ -26,7 +27,6 @@ pub enum PositionRange { } /// Types of character filters that can be applied -#[derive(Debug, Clone)] pub enum CharacterFilter { /// Allow only alphabetic characters (a-z, A-Z) Alphabetic, @@ -39,7 +39,34 @@ pub enum CharacterFilter { /// Allow any character from the provided set OneOf(Vec), /// Custom user-defined filter function - Custom(Box bool + Send + Sync>), + Custom(Arc bool + Send + Sync>), +} + +// Manual implementations for Debug and Clone +impl std::fmt::Debug for CharacterFilter { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + CharacterFilter::Alphabetic => write!(f, "Alphabetic"), + CharacterFilter::Numeric => write!(f, "Numeric"), + CharacterFilter::Alphanumeric => write!(f, "Alphanumeric"), + CharacterFilter::Exact(ch) => write!(f, "Exact('{}')", ch), + CharacterFilter::OneOf(chars) => write!(f, "OneOf({:?})", chars), + CharacterFilter::Custom(_) => write!(f, "Custom()"), + } + } +} + +impl Clone for CharacterFilter { + fn clone(&self) -> Self { + match self { + CharacterFilter::Alphabetic => CharacterFilter::Alphabetic, + CharacterFilter::Numeric => CharacterFilter::Numeric, + CharacterFilter::Alphanumeric => CharacterFilter::Alphanumeric, + CharacterFilter::Exact(ch) => CharacterFilter::Exact(*ch), + CharacterFilter::OneOf(chars) => CharacterFilter::OneOf(chars.clone()), + CharacterFilter::Custom(func) => CharacterFilter::Custom(Arc::clone(func)), + } + } } impl PositionRange { @@ -52,7 +79,7 @@ impl PositionRange { 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 { @@ -96,7 +123,7 @@ impl CharacterFilter { CharacterFilter::Custom(func) => func(ch), } } - + /// Get a human-readable description of this filter pub fn description(&self) -> String { match self { @@ -118,7 +145,7 @@ impl PositionFilter { 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) { @@ -127,7 +154,7 @@ impl PositionFilter { 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) { @@ -154,19 +181,19 @@ impl PatternFilters { 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 { @@ -176,7 +203,7 @@ impl PatternFilters { } Ok(()) } - + /// Validate entire text against all filters pub fn validate_text(&self, text: &str) -> Result<(), String> { for (position, character) in text.char_indices() { @@ -186,12 +213,12 @@ impl PatternFilters { } 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 @@ -206,63 +233,63 @@ mod tests { 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() @@ -274,25 +301,25 @@ mod tests { 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())), + CharacterFilter::Custom(Arc::new(|c| c.is_lowercase())), )); - + assert!(pattern.validate_text("hello").is_ok()); assert!(pattern.validate_text("Hello").is_err()); // Uppercase not allowed }