From 96cde3ca0d586e6c5304a50396cb10b135deaac0 Mon Sep 17 00:00:00 2001 From: Priec Date: Thu, 7 Aug 2025 00:23:45 +0200 Subject: [PATCH] working examples 4 and 5 --- canvas/examples/validation_4.rs | 1061 ++++++++++++++--------------- canvas/examples/validation_5.rs | 1129 ++++++++++++++++++++++++------- 2 files changed, 1391 insertions(+), 799 deletions(-) diff --git a/canvas/examples/validation_4.rs b/canvas/examples/validation_4.rs index 3459577..13a9f2f 100644 --- a/canvas/examples/validation_4.rs +++ b/canvas/examples/validation_4.rs @@ -1,16 +1,14 @@ /* examples/validation_4.rs - Demonstrates Feature 4: Custom parsing/formatting provided by the app, - displayed by the library while keeping raw input authoritative. + Enhanced Feature 4 Demo: Multiple custom formatters with comprehensive edge cases + + Demonstrates: + - Multiple formatter types: PSC, Phone, Credit Card, Date + - Edge case handling: incomplete input, invalid chars, overflow + - Real-time validation feedback and format preview + - Advanced cursor position mapping + - Raw vs formatted data separation + - Error handling and fallback behavior - Use-case: PSC (postal code) typed as "01001" should display as "010 01". - - Raw input: "01001" - - Display: "010 01" - - Cursor mapping is handled by the library via PositionMapper - - Validation still applies to raw text (if configured) - - Formatting is optional and only active when feature "validation" is enabled - - Run with: - cargo run --example validation_4 --features "gui,validation" */ #![allow(clippy::needless_return)] @@ -40,564 +38,502 @@ use ratatui::{ Frame, Terminal, }; -// Bring library types use canvas::{ canvas::{gui::render_canvas_default, modes::AppMode}, DataProvider, FormEditor, ValidationConfig, ValidationConfigBuilder, - // Feature 4 exports CustomFormatter, FormattingResult, }; -/// PSC custom formatter -/// -/// Formats a raw 5-digit PSC as "XXX XX". -/// Examples: -/// - "" -> "" -/// - "0" -> "0" -/// - "01" -> "01" -/// - "010" -> "010" -/// - "0100" -> "010 0" -/// - "01001" -> "010 01" -/// Any extra chars are appended after the space (simple behavior). +/// PSC (Postal Code) Formatter: "01001" -> "010 01" struct PSCFormatter; impl CustomFormatter for PSCFormatter { fn format(&self, raw: &str) -> FormattingResult { - let mut out = String::new(); - - for (i, ch) in raw.chars().enumerate() { - // Insert space after 3rd character for PSC visual grouping - if i == 3 { - out.push(' '); - } - out.push(ch); + if raw.is_empty() { + return FormattingResult::success(""); + } + + // Validate: only digits allowed + if !raw.chars().all(|c| c.is_ascii_digit()) { + return FormattingResult::error("PSC must contain only digits"); + } + + let len = raw.chars().count(); + match len { + 0 => FormattingResult::success(""), + 1..=3 => FormattingResult::success(raw), + 4 => FormattingResult::warning( + format!("{} ", &raw[..3]), + "PSC incomplete (4/5 digits)" + ), + 5 => { + let formatted = format!("{} {}", &raw[..3], &raw[3..]); + if raw == "00000" { + FormattingResult::warning(formatted, "Invalid PSC: 00000") + } else { + FormattingResult::success(formatted) + } + }, + _ => FormattingResult::error("PSC too long (max 5 digits)"), } - - // Use default position mapper which treats non-alphanumeric as separators - FormattingResult::success(out) } } -// Demo editor wrapper for custom formatter demonstration (mirror UX from validation_3) -struct PscDemoFormEditor { - editor: FormEditor, - debug_message: String, - command_buffer: String, - validation_enabled: bool, - show_raw_data: bool, +/// Phone Number Formatter: "1234567890" -> "(123) 456-7890" +struct PhoneFormatter; + +impl CustomFormatter for PhoneFormatter { + fn format(&self, raw: &str) -> FormattingResult { + if raw.is_empty() { + return FormattingResult::success(""); + } + + // Only digits allowed + if !raw.chars().all(|c| c.is_ascii_digit()) { + return FormattingResult::error("Phone must contain only digits"); + } + + let len = raw.chars().count(); + match len { + 0 => FormattingResult::success(""), + 1..=3 => FormattingResult::success(format!("({})", raw)), + 4..=6 => FormattingResult::success(format!("({}) {}", &raw[..3], &raw[3..])), + 7..=10 => FormattingResult::success(format!("({}) {}-{}", &raw[..3], &raw[3..6], &raw[6..])), + 10 => { + let formatted = format!("({}) {}-{}", &raw[..3], &raw[3..6], &raw[6..]); + FormattingResult::success(formatted) + }, + _ => FormattingResult::warning( + format!("({}) {}-{}", &raw[..3], &raw[3..6], &raw[6..10]), + "Phone too long (extra digits ignored)" + ), + } + } } -impl PscDemoFormEditor { +/// Credit Card Formatter: "1234567890123456" -> "1234 5678 9012 3456" +struct CreditCardFormatter; + +impl CustomFormatter for CreditCardFormatter { + fn format(&self, raw: &str) -> FormattingResult { + if raw.is_empty() { + return FormattingResult::success(""); + } + + if !raw.chars().all(|c| c.is_ascii_digit()) { + return FormattingResult::error("Card number must contain only digits"); + } + + let mut formatted = String::new(); + for (i, ch) in raw.chars().enumerate() { + if i > 0 && i % 4 == 0 { + formatted.push(' '); + } + formatted.push(ch); + } + + let len = raw.chars().count(); + match len { + 0..=15 => FormattingResult::warning(formatted, format!("Card incomplete ({}/16 digits)", len)), + 16 => FormattingResult::success(formatted), + _ => FormattingResult::warning(formatted, "Card too long (extra digits shown)"), + } + } +} + + + + + + +/// Date Formatter: "12012024" -> "12/01/2024" +struct DateFormatter; + +impl CustomFormatter for DateFormatter { + fn format(&self, raw: &str) -> FormattingResult { + if raw.is_empty() { + return FormattingResult::success(""); + } + + if !raw.chars().all(|c| c.is_ascii_digit()) { + return FormattingResult::error("Date must contain only digits"); + } + + let len = raw.len(); + match len { + 0 => FormattingResult::success(""), + 1..=2 => FormattingResult::success(raw.to_string()), + 3..=4 => FormattingResult::success(format!("{}/{}", &raw[..2], &raw[2..])), + 5..=8 => FormattingResult::success(format!("{}/{}/{}", &raw[..2], &raw[2..4], &raw[4..])), + 8 => { + let month = &raw[..2]; + let day = &raw[2..4]; + let year = &raw[4..]; + + // Basic validation + let m: u32 = month.parse().unwrap_or(0); + let d: u32 = day.parse().unwrap_or(0); + + if m == 0 || m > 12 { + FormattingResult::warning( + format!("{}/{}/{}", month, day, year), + "Invalid month (01-12)" + ) + } else if d == 0 || d > 31 { + FormattingResult::warning( + format!("{}/{}/{}", month, day, year), + "Invalid day (01-31)" + ) + } else { + FormattingResult::success(format!("{}/{}/{}", month, day, year)) + } + }, + _ => FormattingResult::error("Date too long (MMDDYYYY format)"), + } + } +} + +// Enhanced demo data with multiple formatter types +struct MultiFormatterDemoData { + fields: Vec<(String, String)>, +} + +impl MultiFormatterDemoData { + fn new() -> Self { + Self { + fields: vec![ + ("🏁 PSC (01001)".to_string(), "".to_string()), + ("πŸ“ž Phone (1234567890)".to_string(), "".to_string()), + ("πŸ’³ Credit Card (16 digits)".to_string(), "".to_string()), + ("πŸ“… Date (12012024)".to_string(), "".to_string()), + ("πŸ“ Plain Text".to_string(), "".to_string()), + ], + } + } +} + +impl DataProvider for MultiFormatterDemoData { + 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; + } + + #[cfg(feature = "validation")] + fn validation_config(&self, field_index: usize) -> Option { + match field_index { + 0 => Some(ValidationConfigBuilder::new() + .with_custom_formatter(Arc::new(PSCFormatter)) + .with_max_length(5) + .build()), + 1 => Some(ValidationConfigBuilder::new() + .with_custom_formatter(Arc::new(PhoneFormatter)) + .with_max_length(12) + .build()), + 2 => Some(ValidationConfigBuilder::new() + .with_custom_formatter(Arc::new(CreditCardFormatter)) + .with_max_length(20) + .build()), + 3 => Some(ValidationConfigBuilder::new() + .with_custom_formatter(Arc::new(DateFormatter)) + .with_max_length(8) + .build()), + 4 => Some(ValidationConfigBuilder::new() + .with_custom_formatter(Arc::new(DateFormatter)) + .with_max_length(8) + .build()), + _ => None, // Plain text field - no formatter + } + } +} + +// Enhanced demo editor with comprehensive status tracking +struct EnhancedDemoEditor { + editor: FormEditor, + debug_message: String, + validation_enabled: bool, + show_raw_data: bool, + show_cursor_details: bool, + example_mode: usize, +} + +impl EnhancedDemoEditor { fn new(data_provider: D) -> Self { let mut editor = FormEditor::new(data_provider); editor.set_validation_enabled(true); Self { editor, - debug_message: - "🧩 Custom Formatter Demo - App-defined parsing with library-managed display!".to_string(), - command_buffer: String::new(), + debug_message: "🧩 Enhanced Custom Formatter Demo - Multiple formatters with rich edge cases!".to_string(), validation_enabled: true, show_raw_data: false, + show_cursor_details: false, + example_mode: 0, } } - // === PSC HELPERS (conditional formatting policy) === - fn is_psc_field(&self) -> bool { - self.editor.current_field() == 0 - } - fn psc_raw(&self) -> &str { - if self.is_psc_field() { self.editor.current_text() } else { "" } - } - fn psc_is_valid(&self) -> bool { - let raw = self.psc_raw(); - raw.chars().count() == 5 && raw.chars().all(|c| c.is_ascii_digit()) - } - fn psc_should_format_for_display(&self) -> bool { - // Apply formatting only when NOT editing, on PSC field, and valid 5 digits - self.mode() != AppMode::Edit && self.is_psc_field() && self.psc_is_valid() - } - fn psc_filter_input(&self, ch: char) -> bool { - if !self.is_psc_field() { - return true; + // Field type detection + fn current_field_type(&self) -> &'static str { + match self.editor.current_field() { + 0 => "PSC", + 1 => "Phone", + 2 => "Credit Card", + 3 => "Date", + _ => "Plain Text", } - // Only allow digits, enforce max 5 - if !ch.is_ascii_digit() { - return false; - } - self.psc_raw().chars().count() < 5 } - // === 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() + fn has_formatter(&self) -> bool { + self.editor.current_field() < 5 // First 5 fields have formatters } - // === FORMATTER CONTROL === + fn get_input_rules(&self) -> &'static str { + match self.editor.current_field() { + 0 => "5 digits only (PSC format)", + 1 => "10+ digits (US phone format)", + 2 => "16+ digits (credit card)", + 3 => "Digits as cents (12345 = $123.45)", + 4 => "8 digits MMDDYYYY (date format)", + _ => "Any text (no formatting)", + } + } + + fn cycle_example_data(&mut self) { + let examples = [ + // PSC examples + vec!["01001", "1234567890", "1234567890123456", "12345", "12012024", "Plain text here"], + // Incomplete examples + vec!["010", "123", "1234", "123", "1201", "More text"], + // Invalid examples (will show error handling) + vec!["0abc1", "12a45", "123abc", "abc", "ab01cd", "Special chars!"], + // Edge cases + vec!["00000", "0000000000", "0000000000000000", "99", "13012024", ""], + ]; + + self.example_mode = (self.example_mode + 1) % examples.len(); + let current_examples = &examples[self.example_mode]; + + for (i, example) in current_examples.iter().enumerate() { + if i < self.editor.data_provider().field_count() { + self.editor.data_provider_mut().set_field_value(i, example.to_string()); + } + } + + let mode_names = ["Valid Examples", "Incomplete Input", "Invalid Characters", "Edge Cases"]; + self.debug_message = format!("πŸ“‹ Loaded: {}", mode_names[self.example_mode]); + } + + // Enhanced status methods 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 = "βœ… Custom Formatter ENABLED - Library displays app-formatted output!".to_string(); + self.debug_message = if self.validation_enabled { + "βœ… Custom Formatters ENABLED".to_string() } else { - self.debug_message = "❌ Custom Formatter DISABLED - Raw text only".to_string(); - } + "❌ Custom Formatters DISABLED".to_string() + }; } fn toggle_raw_data_view(&mut self) { self.show_raw_data = !self.show_raw_data; - if self.show_raw_data { - self.debug_message = - "πŸ‘οΈ Showing RAW business data (what's actually stored)".to_string(); + self.debug_message = if self.show_raw_data { + "πŸ‘οΈ Showing RAW data focus".to_string() } else { - self.debug_message = - "✨ Showing FORMATTED display (provided by your app, rendered by library)".to_string(); - } + "✨ Showing FORMATTED display focus".to_string() + }; } - fn get_current_field_info(&self) -> (String, String, String) { - let field_index = self.editor.current_field(); - let raw_data = self.editor.current_text(); + fn toggle_cursor_details(&mut self) { + self.show_cursor_details = !self.show_cursor_details; + self.debug_message = if self.show_cursor_details { + "πŸ“ Detailed cursor mapping info ON".to_string() + } else { + "πŸ“ Detailed cursor mapping info OFF".to_string() + }; + } - // Conditional display policy: - // - If editing PSC: show raw (no formatting) - // - Else if PSC valid and PSC field: show formatted - // - Else: show raw - let display_data = if self.is_psc_field() { - if self.mode() == AppMode::Edit { - raw_data.to_string() - } else if self.psc_is_valid() { - self.editor.current_display_text() + fn get_current_field_analysis(&self) -> (String, String, String, Option) { + let raw = self.editor.current_text(); + let display = self.editor.current_display_text(); + + let status = if raw == display { + if self.has_formatter() { + if self.mode() == AppMode::Edit { + "Raw (editing)".to_string() + } else { + "No formatting needed".to_string() + } } else { - raw_data.to_string() + "No formatter".to_string() } } else { - // Non-PSC field: show raw in this demo - raw_data.to_string() + "Custom formatted".to_string() }; - let fmt_info = if self.is_psc_field() { - if self.psc_is_valid() { - "CustomFormatter: PSC β€˜XXX XX’ (active)".to_string() + let warning = if self.validation_enabled && self.has_formatter() { + // Check if there are any formatting warnings + if raw.len() > 0 { + match self.editor.current_field() { + 0 if raw.len() < 5 => Some(format!("PSC incomplete: {}/5", raw.len())), + 1 if raw.len() < 10 => Some(format!("Phone incomplete: {}/10", raw.len())), + 2 if raw.len() < 16 => Some(format!("Card incomplete: {}/16", raw.len())), + 4 if raw.len() < 8 => Some(format!("Date incomplete: {}/8", raw.len())), + _ => None, + } } else { - "CustomFormatter: PSC β€˜XXX XX’ (waiting for 5 digits)".to_string() + None } } else { - "No formatter".to_string() + None }; - (raw_data.to_string(), display_data, fmt_info) + (raw.to_string(), display, status, warning) } - // === ENHANCED MOVEMENT === - fn move_left(&mut self) { - self.editor.move_left(); - self.update_cursor_info(); - } - - fn move_right(&mut self) { - self.editor.move_right(); - self.update_cursor_info(); - } - - fn move_up(&mut self) { - match self.editor.move_up() { - Ok(()) => { - self.update_field_info(); - } - Err(e) => { - self.debug_message = format!("🚫 Field switch blocked: {}", e); - } - } - } - - fn move_down(&mut self) { - match self.editor.move_down() { - Ok(()) => { - self.update_field_info(); - } - Err(e) => { - self.debug_message = format!("🚫 Field switch blocked: {}", e); - } - } - } - - fn move_line_start(&mut self) { - self.editor.move_line_start(); - self.update_cursor_info(); - } - - fn move_line_end(&mut self) { - self.editor.move_line_end(); - self.update_cursor_info(); - } - - fn update_cursor_info(&mut self) { - if self.validation_enabled { - let raw_pos = self.editor.cursor_position(); - let display_pos = self.editor.display_cursor_position(); - if raw_pos != display_pos { - self.debug_message = format!( - "πŸ“ Cursor: Raw pos {} β†’ Display pos {} (custom formatting active)", - raw_pos, display_pos - ); - } else { - self.debug_message = format!("πŸ“ Cursor at position {} (no display offset)", raw_pos); - } - } - } - - fn update_field_info(&mut self) { - let field_name = self - .editor - .data_provider() - .field_name(self.editor.current_field()); - self.debug_message = format!("πŸ“ Switched to: {}", field_name); - } - - // === MODE TRANSITIONS === + // Delegate methods with enhanced feedback fn enter_edit_mode(&mut self) { self.editor.enter_edit_mode(); - self.debug_message = - "✏️ INSERT MODE - Type to see custom formatting applied in real-time".to_string(); - } - - fn enter_append_mode(&mut self) { - self.editor.enter_append_mode(); - self.debug_message = "✏️ INSERT (append) - Custom formatting active".to_string(); + let field_type = self.current_field_type(); + let rules = self.get_input_rules(); + self.debug_message = format!("✏️ EDITING {} - {}", field_type, rules); } fn exit_edit_mode(&mut self) { self.editor.exit_edit_mode(); - self.debug_message = "πŸ”’ NORMAL MODE - Press 'r' to see raw data".to_string(); + let (raw, display, _, warning) = self.get_current_field_analysis(); + if let Some(warn) = warning { + self.debug_message = format!("πŸ”’ NORMAL - {} | ⚠️ {}", self.current_field_type(), warn); + } else if raw != display { + self.debug_message = format!("πŸ”’ NORMAL - {} formatted successfully", self.current_field_type()); + } else { + self.debug_message = "πŸ”’ NORMAL MODE".to_string(); + } } fn insert_char(&mut self, ch: char) -> anyhow::Result<()> { - // Enforce PSC typing rules on PSC field: - // - Only digits - // - Max 5 characters - if self.is_psc_field() && !self.psc_filter_input(ch) { - self.debug_message = "🚦 PSC: only digits, max 5".to_string(); - return Ok(()); - } - let result = self.editor.insert_char(ch); if result.is_ok() { - // In edit mode we always show raw - let raw = self.editor.current_text().to_string(); - let display = if self.psc_should_format_for_display() { - self.editor.current_display_text() + let (raw, display, _, _) = self.get_current_field_analysis(); + if raw != display && self.validation_enabled { + self.debug_message = format!("✏️ '{}' added - Real-time formatting active", ch); } else { - raw.clone() - }; - if raw != display { - self.debug_message = - format!("✏️ Added '{}': Raw='{}' Display='{}'", ch, raw, display); - } else { - self.debug_message = format!("✏️ Added '{}': '{}'", ch, raw); + self.debug_message = format!("✏️ '{}' added", ch); } } - Ok(result?) + result } - // === DELETE OPERATIONS === - fn delete_backward(&mut self) -> anyhow::Result<()> { - let result = self.editor.delete_backward(); - if result.is_ok() { - // In edit mode, we revert to raw view; debug info reflects that - self.debug_message = "⌫ Character deleted".to_string(); - self.update_cursor_info(); + // Position mapping demo + fn show_position_mapping(&mut self) { + if !self.has_formatter() { + self.debug_message = "πŸ“ No position mapping (plain text field)".to_string(); + return; } - Ok(result?) - } - fn delete_forward(&mut self) -> anyhow::Result<()> { - let result = self.editor.delete_forward(); - if result.is_ok() { - // In edit mode, we revert to raw view; debug info reflects that - self.debug_message = "⌦ Character deleted".to_string(); - self.update_cursor_info(); - } - 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_info(); - } - Err(e) => { - self.debug_message = format!("🚫 Cannot move to next field: {}", e); - } + let raw_pos = self.editor.cursor_position(); + let display_pos = self.editor.display_cursor_position(); + let raw = self.editor.current_text(); + let display = self.editor.current_display_text(); + + if raw_pos != display_pos { + self.debug_message = format!( + "πŸ—ΊοΈ Position mapping: Raw[{}]='{}' ↔ Display[{}]='{}'", + raw_pos, + raw.chars().nth(raw_pos).unwrap_or('βˆ…'), + display_pos, + display.chars().nth(display_pos).unwrap_or('βˆ…') + ); + } else { + self.debug_message = format!("πŸ“ Cursor at position {} (no mapping needed)", raw_pos); } } - fn prev_field(&mut self) { - match self.editor.prev_field() { - Ok(()) => { - self.update_field_info(); - } - Err(e) => { - 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 show_formatter_details(&mut self) { - let (raw, display, fmt_info) = self.get_current_field_info(); - self.debug_message = format!( - "πŸ” Field {}: {} | Raw: '{}' Display: '{}'", - self.current_field() + 1, - fmt_info, - raw, - display - ); - } - - fn get_formatter_status(&self) -> String { - if !self.validation_enabled { - return "❌ DISABLED".to_string(); - } - - // Count fields with validation config (for demo parity) - let field_count = self.editor.data_provider().field_count(); - let mut cfg_count = 0; - for i in 0..field_count { - if self.editor.validation_state().get_field_config(i).is_some() { - cfg_count += 1; - } - } - - format!("🧩 {} FORMATTERS", cfg_count) - } + // Delegate remaining methods + fn mode(&self) -> AppMode { self.editor.mode() } + fn current_field(&self) -> usize { self.editor.current_field() } + fn cursor_position(&self) -> usize { self.editor.cursor_position() } + fn data_provider(&self) -> &D { self.editor.data_provider() } + fn data_provider_mut(&mut self) -> &mut D { self.editor.data_provider_mut() } + fn ui_state(&self) -> &canvas::EditorState { self.editor.ui_state() } + + fn move_up(&mut self) { let _ = self.editor.move_up(); } + fn move_down(&mut self) { let _ = self.editor.move_down(); } + fn move_left(&mut self) { let _ = self.editor.move_left(); } + fn move_right(&mut self) { let _ = self.editor.move_right(); } + fn delete_backward(&mut self) -> anyhow::Result<()> { self.editor.delete_backward() } + fn delete_forward(&mut self) -> anyhow::Result<()> { self.editor.delete_forward() } + fn next_field(&mut self) { let _ = self.editor.next_field(); } + fn prev_field(&mut self) { let _ = self.editor.prev_field(); } } -// Demo data with a PSC field configured with a custom formatter -struct PscDemoData { - fields: Vec<(String, String)>, -} - -impl PscDemoData { - fn new() -> Self { - Self { - fields: vec![ - ("🏁 PSC (type 01001)".to_string(), "".to_string()), - ("πŸ“ Notes (raw)".to_string(), "".to_string()), - ], - } - } -} - -impl DataProvider for PscDemoData { - 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; - } - - // Provide validation config with custom formatter for field 0 - #[cfg(feature = "validation")] - fn validation_config(&self, field_index: usize) -> Option { - match field_index { - 0 => { - // PSC 5 digits displayed as "XXX XX". Raw value remains unmodified. - let cfg = ValidationConfigBuilder::new() - .with_custom_formatter(Arc::new(PSCFormatter)) - // Optional: add character limits or patterns for raw value - // .with_max_length(5) - .build(); - Some(cfg) - } - _ => None, - } - } -} - -// Enhanced key handling with custom formatter specific commands +// Enhanced key handling fn handle_key_press( key: KeyCode, modifiers: KeyModifiers, - editor: &mut PscDemoFormEditor, + editor: &mut EnhancedDemoEditor, ) -> 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) - { + // Quit + if matches!(key, KeyCode::F(10)) || + (key == KeyCode::Char('q') && modifiers.contains(KeyModifiers::CONTROL)) || + (key == KeyCode::Char('c') && modifiers.contains(KeyModifiers::CONTROL)) { return Ok(false); } match (mode, key, modifiers) { - // === MODE TRANSITIONS === - (AppMode::ReadOnly, KeyCode::Char('i'), _) => { - editor.enter_edit_mode(); - editor.clear_command_buffer(); - } + // Mode transitions + (AppMode::ReadOnly, KeyCode::Char('i'), _) => editor.enter_edit_mode(), (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(); - } + editor.editor.enter_append_mode(); + editor.debug_message = format!("✏️ APPEND {} - {}", editor.current_field_type(), editor.get_input_rules()); + }, + (_, KeyCode::Esc, _) => editor.exit_edit_mode(), - // Escape: Exit edit mode - (_, KeyCode::Esc, _) => { - if mode == AppMode::Edit { - editor.exit_edit_mode(); - } else { - editor.clear_command_buffer(); - } - } + // Enhanced demo features + (AppMode::ReadOnly, KeyCode::Char('e'), _) => editor.cycle_example_data(), + (AppMode::ReadOnly, KeyCode::Char('r'), _) => editor.toggle_raw_data_view(), + (AppMode::ReadOnly, KeyCode::Char('c'), _) => editor.toggle_cursor_details(), + (AppMode::ReadOnly, KeyCode::Char('m'), _) => editor.show_position_mapping(), + (AppMode::ReadOnly, KeyCode::F(1), _) => editor.toggle_validation(), - // === FORMATTER-SPECIFIC COMMANDS === - (AppMode::ReadOnly, KeyCode::Char('m'), _) => { - editor.show_formatter_details(); - editor.clear_command_buffer(); - } - (AppMode::ReadOnly, KeyCode::Char('r'), _) => { - editor.toggle_raw_data_view(); - editor.clear_command_buffer(); - } - (AppMode::ReadOnly, KeyCode::F(1), _) => { - editor.toggle_validation(); - } + // Movement + (_, KeyCode::Up, _) | (AppMode::ReadOnly, KeyCode::Char('k'), _) => editor.move_up(), + (_, KeyCode::Down, _) | (AppMode::ReadOnly, KeyCode::Char('j'), _) => editor.move_down(), + (_, KeyCode::Left, _) | (AppMode::ReadOnly, KeyCode::Char('h'), _) => editor.move_left(), + (_, KeyCode::Right, _) | (AppMode::ReadOnly, KeyCode::Char('l'), _) => editor.move_right(), + (_, KeyCode::Tab, _) => editor.next_field(), + (_, KeyCode::BackTab, _) => editor.prev_field(), - // === 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(); - } - - // Line movement - (AppMode::ReadOnly, KeyCode::Char('0'), _) => { - editor.move_line_start(); - editor.clear_command_buffer(); - } - (AppMode::ReadOnly, KeyCode::Char('$'), _) => { - editor.move_line_end(); - 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 === + // Editing (AppMode::Edit, KeyCode::Char(c), m) if !m.contains(KeyModifiers::CONTROL) => { editor.insert_char(c)?; - } + }, + (AppMode::Edit, KeyCode::Backspace, _) => { editor.delete_backward()?; }, + (AppMode::Edit, KeyCode::Delete, _) => { editor.delete_forward()?; }, - // === DEBUG/INFO COMMANDS === + // Field analysis (AppMode::ReadOnly, KeyCode::Char('?'), _) => { - let (raw, display, fmt_info) = editor.get_current_field_info(); - editor.set_debug_message(format!( - "Field {}/{}, Cursor {}, {}, Raw: '{}', Display: '{}'", - editor.current_field() + 1, - editor.data_provider().field_count(), - editor.cursor_position(), - fmt_info, - raw, - display - )); - } + let (raw, display, status, warning) = editor.get_current_field_analysis(); + let warning_text = warning.map(|w| format!(" ⚠️ {}", w)).unwrap_or_default(); + editor.debug_message = format!( + "πŸ” Field {}: {} | Raw: '{}' | Display: '{}'{}", + editor.current_field() + 1, status, raw, display, warning_text + ); + }, - _ => { - if editor.has_pending_command() { - editor.clear_command_buffer(); - editor.set_debug_message("Invalid command sequence".to_string()); - } - } + _ => {} } Ok(true) @@ -605,7 +541,7 @@ fn handle_key_press( fn run_app( terminal: &mut Terminal, - mut editor: PscDemoFormEditor, + mut editor: EnhancedDemoEditor, ) -> io::Result<()> { loop { terminal.draw(|f| ui(f, &editor))?; @@ -618,132 +554,143 @@ fn run_app( } } Err(e) => { - editor.set_debug_message(format!("Error: {}", e)); + editor.debug_message = format!("❌ Error: {}", e); } } } } - Ok(()) } -fn ui(f: &mut Frame, editor: &PscDemoFormEditor) { +fn ui(f: &mut Frame, editor: &EnhancedDemoEditor) { let chunks = Layout::default() .direction(Direction::Vertical) - .constraints([Constraint::Min(8), Constraint::Length(16)]) + .constraints([Constraint::Min(8), Constraint::Length(18)]) .split(f.area()); - render_enhanced_canvas(f, chunks[0], editor); - render_formatter_status(f, chunks[1], editor); + render_canvas_default(f, chunks[0], &editor.editor); + render_enhanced_status(f, chunks[1], editor); } -fn render_enhanced_canvas(f: &mut Frame, area: Rect, editor: &PscDemoFormEditor) { - render_canvas_default(f, area, &editor.editor); -} - -fn render_formatter_status( +fn render_enhanced_status( f: &mut Frame, area: Rect, - editor: &PscDemoFormEditor, + editor: &EnhancedDemoEditor, ) { let chunks = Layout::default() .direction(Direction::Vertical) .constraints([ - Constraint::Length(3), // Status bar - Constraint::Length(6), // Data comparison - Constraint::Length(7), // Help + Constraint::Length(3), // Status bar + Constraint::Length(6), // Current field analysis + Constraint::Length(9), // Help ]) .split(area); - // Status bar with formatter information + // Status bar let mode_text = match editor.mode() { AppMode::Edit => "INSERT", AppMode::ReadOnly => "NORMAL", _ => "OTHER", }; - let fmt_status = editor.get_formatter_status(); + let formatter_count = (0..editor.data_provider().field_count()) + .filter(|&i| editor.data_provider().validation_config(i).is_some()) + .count(); + let status_text = format!( - "-- {} -- {} | Formatters: {} | View: {}", + "-- {} -- {} | Formatters: {}/{} active | View: {}{}", mode_text, - editor.debug_message(), - fmt_status, - if editor.show_raw_data { "RAW" } else { "FORMATTED" } + editor.debug_message, + formatter_count, + editor.data_provider().field_count(), + if editor.show_raw_data { "RAW" } else { "DISPLAY" }, + if editor.show_cursor_details { " | CURSOR+" } else { "" } ); - let status = - Paragraph::new(Line::from(Span::raw(status_text))) - .block(Block::default().borders(Borders::ALL).title("🧩 Custom Formatter Demo")); + let status = Paragraph::new(Line::from(Span::raw(status_text))) + .block(Block::default().borders(Borders::ALL).title("🧩 Enhanced Custom Formatter Demo")); f.render_widget(status, chunks[0]); - // Data comparison showing raw vs display - let (raw_data, display_data, fmt_info) = editor.get_current_field_info(); + // Current field analysis + let (raw, display, status, warning) = editor.get_current_field_analysis(); let field_name = editor.data_provider().field_name(editor.current_field()); + let field_type = editor.current_field_type(); + + let mut analysis_lines = vec![ + format!("πŸ“ Current: {} ({})", field_name, field_type), + format!("πŸ”§ Status: {}", status), + ]; - let comparison_text = format!( - "πŸ“ Current Field: {}\n\ - πŸ”§ Formatter Config: {}\n\ - \n\ - πŸ’Ύ Raw Business Data: '{}' ← What's actually stored in your database\n\ - ✨ Formatted Display: '{}' ← What users see in the interface\n\ - πŸ“ Cursor: Raw pos {} β†’ Display pos {}", - field_name, - fmt_info, - raw_data, - display_data, - editor.cursor_position(), - editor.editor.display_cursor_position() - ); - - let comparison_style = if raw_data != display_data { - Style::default().fg(Color::Green) // Green when formatting is active + if editor.show_raw_data || editor.mode() == AppMode::Edit { + analysis_lines.push(format!("πŸ’Ύ Raw Data: '{}'", raw)); + analysis_lines.push(format!("✨ Display: '{}'", display)); } else { - Style::default().fg(Color::Gray) // Gray when no formatting + analysis_lines.push(format!("✨ User Sees: '{}'", display)); + analysis_lines.push(format!("πŸ’Ύ Stored As: '{}'", raw)); + } + + if editor.show_cursor_details { + analysis_lines.push(format!( + "πŸ“ Cursor: Raw[{}] β†’ Display[{}]", + editor.cursor_position(), + editor.editor.display_cursor_position() + )); + } + + if let Some(ref warn) = warning { + analysis_lines.push(format!("⚠️ Warning: {}", warn)); + } + + let analysis_color = if warning.is_some() { + Color::Yellow + } else if raw != display && editor.validation_enabled { + Color::Green + } else { + Color::Gray }; - let data_comparison = Paragraph::new(comparison_text) - .block( - Block::default() - .borders(Borders::ALL) - .title("πŸ“Š Raw Data vs App-Provided Formatting"), - ) - .style(comparison_style) + let analysis = Paragraph::new(analysis_lines.join("\n")) + .block(Block::default().borders(Borders::ALL).title("πŸ” Field Analysis")) + .style(Style::default().fg(analysis_color)) .wrap(Wrap { trim: true }); - f.render_widget(data_comparison, chunks[1]); + f.render_widget(analysis, chunks[1]); - // Help text + // Enhanced help let help_text = match editor.mode() { AppMode::ReadOnly => { - "🧩 CUSTOM FORMATTER DEMO: App provides parsing/formatting; library displays and maps cursor!\n\ + "🧩 ENHANCED CUSTOM FORMATTER DEMO\n\ \n\ - Try the PSC field:\n\ - β€’ Type: 01001 β†’ Display: 010 01\n\ - β€’ Raw data stays unmodified: '01001'\n\ + Try these formatters: + β€’ PSC: 01001 β†’ 010 01 | Phone: 1234567890 β†’ (123) 456-7890 | Card: 1234567890123456 β†’ 1234 5678 9012 3456 + β€’ Date: 12012024 β†’ 12/01/2024 | Plain: no formatting \n\ - Commands: i/a=insert, m=formatter details, r=toggle raw/display view\n\ - Movement: hjkl/arrows=move, 0/$ line start/end, Tab=next field, F1=toggle formatting\n\ - ?=detailed info, Ctrl+C=quit" + Commands: i=insert, e=cycle examples, r=toggle raw/display, c=cursor details, m=position mapping\n\ + Movement: hjkl/arrows, Tab=next field, ?=analyze current field, F1=toggle formatters\n\ + Ctrl+C/F10=quit" } AppMode::Edit => { - "✏️ INSERT MODE - Type to see real-time custom formatter output!\n\ + "✏️ INSERT MODE - Real-time formatting as you type!\n\ \n\ - Key Points:\n\ - β€’ Your app formats; library displays and maps cursor\n\ - β€’ Raw input is authoritative for validation and storage\n\ + Current field rules: {}\n\ + β€’ Raw input is authoritative (what gets stored)\n\ + β€’ Display formatting updates in real-time (what users see)\n\ + β€’ Cursor position is mapped between raw and display\n\ \n\ - arrows=move, Backspace/Del=delete, Esc=normal, Tab=next field" + Esc=normal mode, arrows=navigate, Backspace/Del=delete" } - _ => "🧩 Custom Formatter Demo Active!" + _ => "🧩 Enhanced Custom Formatter Demo" }; - let help = Paragraph::new(help_text) - .block( - Block::default() - .borders(Borders::ALL) - .title("πŸš€ Formatter Features & Commands"), - ) + let formatted_help = if editor.mode() == AppMode::Edit { + help_text.replace("{}", editor.get_input_rules()) + } else { + help_text.to_string() + }; + + let help = Paragraph::new(formatted_help) + .block(Block::default().borders(Borders::ALL).title("πŸš€ Enhanced Features & Commands")) .style(Style::default().fg(Color::Gray)) .wrap(Wrap { trim: true }); @@ -751,15 +698,15 @@ fn render_formatter_status( } fn main() -> Result<(), Box> { - // Print feature status - println!("🧩 Canvas Custom Formatter Demo (Feature 4)"); + println!("🧩 Enhanced Canvas Custom Formatter Demo (Feature 4)"); println!("βœ… validation feature: ENABLED"); println!("βœ… gui feature: ENABLED"); - println!("🧩 Custom formatting: ACTIVE"); - println!("πŸ”₯ Key Benefits Demonstrated:"); - println!(" β€’ App decides how to display values (e.g., PSC '01001' β†’ '010 01')"); - println!(" β€’ Library handles display + cursor mapping automatically"); - println!(" β€’ Raw input remains authoritative for validation/storage"); + println!("🧩 Enhanced features:"); + println!(" β€’ 5 different custom formatters with edge cases"); + println!(" β€’ Real-time format preview and validation"); + println!(" β€’ Advanced cursor position mapping"); + println!(" β€’ Comprehensive error handling and warnings"); + println!(" β€’ Raw vs formatted data separation demos"); println!(); enable_raw_mode()?; @@ -768,8 +715,8 @@ fn main() -> Result<(), Box> { let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; - let data = PscDemoData::new(); - let editor = PscDemoFormEditor::new(data); + let data = MultiFormatterDemoData::new(); + let editor = EnhancedDemoEditor::new(data); let res = run_app(&mut terminal, editor); @@ -781,7 +728,11 @@ fn main() -> Result<(), Box> { println!("{:?}", err); } - println!("🧩 Custom formatter demo completed!"); - println!("πŸ† You saw how app-defined formatting integrates seamlessly with the library!"); + println!("🧩 Enhanced custom formatter demo completed!"); + println!("πŸ† You experienced comprehensive custom formatting with:"); + println!(" β€’ Multiple formatter types (PSC, Phone, Credit Card, Date)"); + println!(" β€’ Edge case handling (incomplete, invalid, overflow)"); + println!(" β€’ Real-time format preview and cursor mapping"); + println!(" β€’ Clear separation between raw business data and display formatting"); Ok(()) -} \ No newline at end of file +} diff --git a/canvas/examples/validation_5.rs b/canvas/examples/validation_5.rs index 2c06e33..b8b1d94 100644 --- a/canvas/examples/validation_5.rs +++ b/canvas/examples/validation_5.rs @@ -1,21 +1,29 @@ // examples/validation_5.rs -//! Feature 5: External validation (UI-only) demo with Feature 4 (custom formatter) +//! Enhanced Feature 5: Comprehensive external validation (UI-only) demo with Feature 4 integration //! -//! Behavior: -//! - Field 1 (PSC) uses a custom formatter (Feature 4) and external validation (Feature 5). -//! β€’ While editing PSC: raw text, capped at 5 digits -//! β€’ When not editing PSC (moved focus or Esc) and raw is 5 digits: shows formatted ("XXX XX") -//! β€’ After leaving PSC (or pressing 'v'), external validation kicks in: -//! - Validating -> Valid/Invalid/Warning based on simple rules (LSP-like simulation) -//! - Field 2 (Notes) is plain text, no formatter, no external validation. +//! Demonstrates: +//! - Multiple external validation types: PSC lookup, email domain check, username availability, +//! API key validation, credit card verification +//! - Async validation simulation with realistic delays +//! - Validation caching and debouncing +//! - Progressive validation (local β†’ remote) +//! - Validation history and performance metrics +//! - Different validation triggers (blur, manual, auto) +//! - Error handling and retry mechanisms +//! - Complex validation states with detailed feedback //! //! Controls: //! - i/a: insert/append -//! - Esc: exit edit mode (triggers PSC validation if enabled) -//! - Tab/Shift+Tab: next/prev field (triggers PSC validation if enabled) +//! - Esc: exit edit mode (triggers validation on configured fields) +//! - Tab/Shift+Tab: next/prev field (triggers validation) //! - v: manually trigger validation of current field +//! - V: validate all fields //! - c: clear external validation state for current field -//! - r: toggle raw/format view flag in panel (visual only; canvas follows library rules) +//! - C: clear all validation states +//! - r: toggle validation history view +//! - e: cycle through example datasets +//! - h: show validation help and field rules +//! - F1: toggle external validation globally //! - F10/Ctrl+C: quit //! //! Run: cargo run --example validation_5 --features "gui,validation" @@ -30,6 +38,8 @@ compile_error!( use std::io; use std::sync::Arc; +use std::collections::HashMap; +use std::time::{Instant, Duration}; use crossterm::{ event::{ @@ -44,7 +54,7 @@ use ratatui::{ layout::{Constraint, Direction, Layout, Rect}, style::{Color, Style}, text::{Line, Span}, - widgets::{Block, Borders, Paragraph, Wrap}, + widgets::{Block, Borders, Paragraph, Wrap, List, ListItem}, Frame, Terminal, }; @@ -55,39 +65,452 @@ use canvas::{ validation::ExternalValidationState, }; -/// PSC custom formatter for display ("XXX XX"). +/// Enhanced external validation state with timing and context +#[derive(Debug, Clone)] +struct ValidationResult { + state: ExternalValidationState, + started_at: Instant, + completed_at: Option, + validation_type: String, + cached: bool, +} + +impl ValidationResult { + fn new(validation_type: String) -> Self { + Self { + state: ExternalValidationState::Validating, + started_at: Instant::now(), + completed_at: None, + validation_type, + cached: false, + } + } + + fn complete(mut self, state: ExternalValidationState) -> Self { + self.state = state; + self.completed_at = Some(Instant::now()); + self + } + + fn from_cache(state: ExternalValidationState, validation_type: String) -> Self { + Self { + state, + started_at: Instant::now(), + completed_at: Some(Instant::now()), + validation_type, + cached: true, + } + } + + fn duration(&self) -> Duration { + self.completed_at.unwrap_or_else(Instant::now).duration_since(self.started_at) + } +} + +/// PSC Formatter with enhanced validation context struct PSCFormatter; impl CustomFormatter for PSCFormatter { fn format(&self, raw: &str) -> FormattingResult { - let mut out = String::new(); - for (i, ch) in raw.chars().enumerate() { - if i == 3 { - out.push(' '); - } - out.push(ch); + if raw.is_empty() { + return FormattingResult::success(""); + } + + if !raw.chars().all(|c| c.is_ascii_digit()) { + return FormattingResult::error("PSC must contain only digits"); + } + + match raw.len() { + 0 => FormattingResult::success(""), + 1..=3 => FormattingResult::success(raw.to_string()), + 4 => FormattingResult::warning( + format!("{} ", &raw[..3]), + "PSC incomplete - external validation pending" + ), + 5 => FormattingResult::success(format!("{} {}", &raw[..3], &raw[3..])), + _ => FormattingResult::error("PSC too long (max 5 digits)"), } - FormattingResult::success(out) } } -// Demo data provider: PSC + Notes -struct DemoData { +/// Credit Card Formatter for external validation demo +struct CreditCardFormatter; + +impl CustomFormatter for CreditCardFormatter { + fn format(&self, raw: &str) -> FormattingResult { + if raw.is_empty() { + return FormattingResult::success(""); + } + + if !raw.chars().all(|c| c.is_ascii_digit()) { + return FormattingResult::error("Card number must contain only digits"); + } + + let mut formatted = String::new(); + for (i, ch) in raw.chars().enumerate() { + if i > 0 && i % 4 == 0 { + formatted.push(' '); + } + formatted.push(ch); + } + + match raw.len() { + 0..=15 => FormattingResult::warning(formatted, "Card incomplete - validation pending"), + 16 => FormattingResult::success(formatted), + _ => FormattingResult::warning(formatted, "Card too long - validation may fail"), + } + } +} + +/// Comprehensive validation cache +struct ValidationCache { + results: HashMap, +} + +impl ValidationCache { + fn new() -> Self { + Self { + results: HashMap::new(), + } + } + + fn get(&self, key: &str) -> Option<&ExternalValidationState> { + self.results.get(key) + } + + fn set(&mut self, key: String, result: ExternalValidationState) { + self.results.insert(key, result); + } + + fn clear(&mut self) { + self.results.clear(); + } +} + +/// Simulated external validation services +struct ValidationServices { + cache: ValidationCache, +} + +impl ValidationServices { + fn new() -> Self { + Self { + cache: ValidationCache::new(), + } + } + + /// PSC validation: simulates postal service API lookup + fn validate_psc(&mut self, psc: &str) -> ExternalValidationState { + let cache_key = format!("psc:{}", psc); + if let Some(cached) = self.cache.get(&cache_key) { + return cached.clone(); + } + + if psc.is_empty() { + return ExternalValidationState::NotValidated; + } + + if !psc.chars().all(|c| c.is_ascii_digit()) || psc.len() != 5 { + let result = ExternalValidationState::Invalid { + message: "Invalid PSC format".to_string(), + suggestion: Some("Enter 5 digits".to_string()) + }; + self.cache.set(cache_key, result.clone()); + return result; + } + + // Simulate realistic PSC validation scenarios + let result = match psc { + "00000" | "99999" => ExternalValidationState::Invalid { + message: "PSC does not exist".to_string(), + suggestion: Some("Check postal code".to_string()) + }, + "01001" => ExternalValidationState::Valid(Some("Prague 1 - verified".to_string())), + "10000" => ExternalValidationState::Valid(Some("Bratislava - verified".to_string())), + "12345" => ExternalValidationState::Warning { + message: "PSC region deprecated - still valid".to_string() + }, + "50000" => ExternalValidationState::Invalid { + message: "PSC temporarily unavailable".to_string(), + suggestion: Some("Try again later".to_string()) + }, + _ => { + // Most PSCs are valid with generic info + let region = match &psc[..2] { + "01" | "02" | "03" => "Prague region", + "10" | "11" | "12" => "Bratislava region", + "20" | "21" => "Brno region", + _ => "Valid postal region" + }; + ExternalValidationState::Valid(Some(format!("{} - verified", region))) + } + }; + + self.cache.set(cache_key, result.clone()); + result + } + + /// Email validation: simulates domain checking + fn validate_email(&mut self, email: &str) -> ExternalValidationState { + let cache_key = format!("email:{}", email); + if let Some(cached) = self.cache.get(&cache_key) { + return cached.clone(); + } + + if email.is_empty() { + return ExternalValidationState::NotValidated; + } + + if !email.contains('@') { + let result = ExternalValidationState::Invalid { + message: "Email must contain @".to_string(), + suggestion: Some("Format: user@domain.com".to_string()) + }; + self.cache.set(cache_key, result.clone()); + return result; + } + + let parts: Vec<&str> = email.split('@').collect(); + if parts.len() != 2 || parts[0].is_empty() || parts[1].is_empty() { + let result = ExternalValidationState::Invalid { + message: "Invalid email format".to_string(), + suggestion: Some("Format: user@domain.com".to_string()) + }; + self.cache.set(cache_key, result.clone()); + return result; + } + + let domain = parts[1]; + let result = match domain { + "gmail.com" | "outlook.com" | "yahoo.com" => { + ExternalValidationState::Valid(Some("Popular email provider - verified".to_string())) + }, + "example.com" | "test.com" => { + ExternalValidationState::Warning { + message: "Test domain - email may not be deliverable".to_string() + } + }, + "blocked.com" | "spam.com" => { + ExternalValidationState::Invalid { + message: "Domain blocked".to_string(), + suggestion: Some("Use different email provider".to_string()) + } + }, + _ if domain.contains('.') => { + ExternalValidationState::Valid(Some("Domain appears valid - not verified".to_string())) + }, + _ => { + ExternalValidationState::Invalid { + message: "Invalid domain format".to_string(), + suggestion: Some("Domain must contain '.'".to_string()) + } + } + }; + + self.cache.set(cache_key, result.clone()); + result + } + + /// Username validation: simulates availability checking + fn validate_username(&mut self, username: &str) -> ExternalValidationState { + let cache_key = format!("username:{}", username); + if let Some(cached) = self.cache.get(&cache_key) { + return cached.clone(); + } + + if username.is_empty() { + return ExternalValidationState::NotValidated; + } + + if username.len() < 3 { + let result = ExternalValidationState::Invalid { + message: "Username too short".to_string(), + suggestion: Some("Minimum 3 characters".to_string()) + }; + self.cache.set(cache_key, result.clone()); + return result; + } + + if !username.chars().all(|c| c.is_alphanumeric() || c == '_') { + let result = ExternalValidationState::Invalid { + message: "Invalid characters".to_string(), + suggestion: Some("Use letters, numbers, underscore only".to_string()) + }; + self.cache.set(cache_key, result.clone()); + return result; + } + + let result = match username { + "admin" | "root" | "user" | "test" => { + ExternalValidationState::Invalid { + message: "Username reserved".to_string(), + suggestion: Some("Choose different username".to_string()) + } + }, + "john123" | "alice_dev" => { + ExternalValidationState::Invalid { + message: "Username already taken".to_string(), + suggestion: Some("Try variations or add numbers".to_string()) + } + }, + username if username.starts_with("temp_") => { + ExternalValidationState::Warning { + message: "Temporary username pattern - are you sure?".to_string() + } + }, + _ => { + ExternalValidationState::Valid(Some("Username available - good choice!".to_string())) + } + }; + + self.cache.set(cache_key, result.clone()); + result + } + + /// API Key validation: simulates authentication service + fn validate_api_key(&mut self, key: &str) -> ExternalValidationState { + let cache_key = format!("apikey:{}", key); + if let Some(cached) = self.cache.get(&cache_key) { + return cached.clone(); + } + + if key.is_empty() { + return ExternalValidationState::NotValidated; + } + + if key.len() < 20 { + let result = ExternalValidationState::Invalid { + message: "API key too short".to_string(), + suggestion: Some("Valid keys are 32+ characters".to_string()) + }; + self.cache.set(cache_key, result.clone()); + return result; + } + + let result = match key { + "invalid_key_12345678901" => { + ExternalValidationState::Invalid { + message: "API key not found".to_string(), + suggestion: Some("Check key and permissions".to_string()) + } + }, + "expired_key_12345678901" => { + ExternalValidationState::Invalid { + message: "API key expired".to_string(), + suggestion: Some("Generate new key".to_string()) + } + }, + "limited_key_12345678901" => { + ExternalValidationState::Warning { + message: "API key has limited permissions".to_string() + } + }, + key if key.starts_with("test_") => { + ExternalValidationState::Warning { + message: "Test API key - limited functionality".to_string() + } + }, + _ if key.len() >= 32 => { + ExternalValidationState::Valid(Some("API key authenticated - full access".to_string())) + }, + _ => { + ExternalValidationState::Invalid { + message: "Invalid API key format".to_string(), + suggestion: Some("Keys should be 32+ alphanumeric characters".to_string()) + } + } + }; + + self.cache.set(cache_key, result.clone()); + result + } + + /// Credit Card validation: simulates bank verification + fn validate_credit_card(&mut self, card: &str) -> ExternalValidationState { + let cache_key = format!("card:{}", card); + if let Some(cached) = self.cache.get(&cache_key) { + return cached.clone(); + } + + if card.is_empty() { + return ExternalValidationState::NotValidated; + } + + if !card.chars().all(|c| c.is_ascii_digit()) || card.len() != 16 { + let result = ExternalValidationState::Invalid { + message: "Invalid card format".to_string(), + suggestion: Some("Enter 16 digits".to_string()) + }; + self.cache.set(cache_key, result.clone()); + return result; + } + + // Basic Luhn algorithm check (simplified) + let sum: u32 = card.chars() + .filter_map(|c| c.to_digit(10)) + .enumerate() + .map(|(i, digit)| { + if i % 2 == 0 { + let doubled = digit * 2; + if doubled > 9 { doubled - 9 } else { doubled } + } else { + digit + } + }) + .sum(); + + if sum % 10 != 0 { + let result = ExternalValidationState::Invalid { + message: "Invalid card number (failed checksum)".to_string(), + suggestion: Some("Check card number".to_string()) + }; + self.cache.set(cache_key, result.clone()); + return result; + } + + let result = match &card[..4] { + "4000" => ExternalValidationState::Valid(Some("Visa - card verified".to_string())), + "5555" => ExternalValidationState::Valid(Some("Mastercard - card verified".to_string())), + "4111" => ExternalValidationState::Warning { + message: "Test card number - not for real transactions".to_string() + }, + "0000" => ExternalValidationState::Invalid { + message: "Card declined by issuer".to_string(), + suggestion: Some("Contact your bank".to_string()) + }, + _ => ExternalValidationState::Valid(Some("Card number valid - bank not verified".to_string())) + }; + + self.cache.set(cache_key, result.clone()); + result + } + + fn clear_cache(&mut self) { + self.cache.clear(); + } +} + +/// Rich demo data with multiple validation types +struct ValidationDemoData { fields: Vec<(String, String)>, } -impl DemoData { +impl ValidationDemoData { fn new() -> Self { Self { fields: vec![ - ("🏁 PSC (5 digits)".to_string(), "".to_string()), - ("πŸ“ Notes".to_string(), "".to_string()), + ("🏁 PSC (01001)".to_string(), "".to_string()), + ("πŸ“§ Email (user@domain.com)".to_string(), "".to_string()), + ("πŸ‘€ Username (3+ chars)".to_string(), "".to_string()), + ("πŸ”‘ API Key (32+ chars)".to_string(), "".to_string()), + ("πŸ’³ Credit Card (16 digits)".to_string(), "".to_string()), + ("πŸ“ Notes (no validation)".to_string(), "".to_string()), ], } } } -impl DataProvider for DemoData { +impl DataProvider for ValidationDemoData { 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 } @@ -96,187 +519,300 @@ impl DataProvider for DemoData { #[cfg(feature = "validation")] fn validation_config(&self, field_index: usize) -> Option { match field_index { - 0 => { - // PSC: Feature 4 + Feature 5 + max length cap - Some( - ValidationConfigBuilder::new() - .with_custom_formatter(Arc::new(PSCFormatter)) - .with_max_length(5) // Enforce raw max length during typing - .with_external_validation_enabled(true) // Show external validation indicator - .build() - ) - } + 0 => Some(ValidationConfigBuilder::new() + .with_custom_formatter(Arc::new(PSCFormatter)) + .with_max_length(5) + .with_external_validation_enabled(true) + .build()), + 1 => Some(ValidationConfigBuilder::new() + .with_max_length(50) + .with_external_validation_enabled(true) + .build()), + 2 => Some(ValidationConfigBuilder::new() + .with_max_length(20) + .with_external_validation_enabled(true) + .build()), + 3 => Some(ValidationConfigBuilder::new() + .with_max_length(50) + .with_external_validation_enabled(true) + .build()), + 4 => Some(ValidationConfigBuilder::new() + .with_custom_formatter(Arc::new(CreditCardFormatter)) + .with_max_length(16) + .with_external_validation_enabled(true) + .build()), _ => None, } } } -/// Simulated LSP-like validator: -/// - Receives the raw PSC and returns a state (Validating -> Valid/Warn/Invalid). -/// - This is called by our "frontend" workflow when we leave PSC (or press 'v'). -/// In real apps, this would be an async backend call; we just simulate results. -fn simulate_external_psc_validation(raw_psc: &str) -> ExternalValidationState { - // Edge cases and outcomes: - // - Empty -> NotValidated (frontend shouldn't call on empty, but handle gracefully) - // - Non-digit -> Invalid - // - Length < 5 -> Warning (incomplete) - // - Exactly 5 digits: - // * "00000" -> Invalid (nonsensical) - // * "12345" -> Warning (pretend partial region) - // * "01001" -> Valid(Some("Known district")) - // * otherwise Valid(None) - if raw_psc.is_empty() { - return ExternalValidationState::NotValidated; - } - if !raw_psc.chars().all(|c| c.is_ascii_digit()) { - return ExternalValidationState::Invalid { message: "PSC must be digits".to_string(), suggestion: Some("Only 0-9".to_string()) }; - } - let len = raw_psc.chars().count(); - if len < 5 { - return ExternalValidationState::Warning { message: format!("PSC incomplete: {}/5", len) }; - } - // len == 5 - match raw_psc { - "00000" => ExternalValidationState::Invalid { message: "PSC cannot be 00000".to_string(), suggestion: Some("Try 01001".to_string()) }, - "12345" => ExternalValidationState::Warning { message: "Unrecognized region: verify".to_string() }, - "01001" => ExternalValidationState::Valid(Some("Known district".to_string())), - _ => ExternalValidationState::Valid(None), - } -} - -// Editor wrapper to manage external validation workflow and show UI info -struct DemoEditor { +/// Enhanced editor with comprehensive external validation management +struct ValidationDemoEditor { editor: FormEditor, + services: ValidationServices, + validation_history: Vec<(usize, String, ValidationResult)>, debug_message: String, - show_raw_hint: bool, // panel-only toggle, does not affect canvas + show_history: bool, + example_mode: usize, + validation_enabled: bool, + auto_validate: bool, + validation_stats: HashMap, // field -> (count, total_time) } -impl DemoEditor { +impl ValidationDemoEditor { fn new(data_provider: D) -> Self { let mut editor = FormEditor::new(data_provider); editor.set_validation_enabled(true); Self { editor, - debug_message: "πŸ§ͺ External validation demo (Feature 5) + Custom formatting (Feature 4)".to_string(), - show_raw_hint: false, + services: ValidationServices::new(), + validation_history: Vec::new(), + debug_message: "πŸ§ͺ Enhanced External Validation Demo - Multiple validation types with rich scenarios!".to_string(), + show_history: false, + example_mode: 0, + validation_enabled: true, + auto_validate: true, + validation_stats: HashMap::new(), } } fn current_field(&self) -> usize { self.editor.current_field() } fn mode(&self) -> AppMode { self.editor.mode() } fn data_provider(&self) -> &D { self.editor.data_provider() } - fn cursor_position(&self) -> usize { self.editor.cursor_position() } fn ui_state(&self) -> &canvas::EditorState { self.editor.ui_state() } - // Minimal pass-through editing controls - fn enter_edit_mode(&mut self) { - self.editor.enter_edit_mode(); - self.debug_message = "✏️ Edit PSC or Notes. PSC: raw while editing, formatted when not editing".to_string(); - } - fn enter_append_mode(&mut self) { - self.editor.enter_append_mode(); - self.debug_message = "✏️ Append mode active".to_string(); - } - fn exit_edit_mode(&mut self) { - self.editor.exit_edit_mode(); - self.debug_message = "πŸ”’ Normal mode".to_string(); - // Trigger external validation upon exiting the field (Feature 5 timing) - self.trigger_field_validation(self.editor.current_field()); + fn field_type(&self) -> &'static str { + match self.current_field() { + 0 => "PSC", + 1 => "Email", + 2 => "Username", + 3 => "API Key", + 4 => "Credit Card", + _ => "Plain Text", + } } - fn next_field(&mut self) { - match self.editor.next_field() { - Ok(()) => { - self.debug_message = "➑ Switched to next field".to_string(); - // Validate field we just left (Feature 5 timing) - let prev = self.editor.current_field().saturating_sub(1); - self.trigger_field_validation(prev); + fn field_validation_rules(&self) -> &'static str { + match self.current_field() { + 0 => "5 digits - checks postal service database", + 1 => "email@domain.com - verifies domain", + 2 => "3+ chars, alphanumeric + _ - checks availability", + 3 => "32+ chars - authenticates with service", + 4 => "16 digits - verifies with bank", + _ => "No external validation", + } + } + + fn has_external_validation(&self) -> bool { + self.current_field() < 5 + } + + /// Trigger external validation for specific field + fn validate_field(&mut self, field_index: usize) { + if !self.validation_enabled || field_index >= 5 { + return; + } + + let raw_value = self.editor.data_provider().field_value(field_index).to_string(); + if raw_value.is_empty() { + self.editor.clear_external_validation(field_index); + return; + } + + // Set to validating state first + self.editor.set_external_validation(field_index, ExternalValidationState::Validating); + + let validation_type = match field_index { + 0 => "PSC Lookup", + 1 => "Email Domain Check", + 2 => "Username Availability", + 3 => "API Key Auth", + 4 => "Credit Card Verify", + _ => "Unknown", + }.to_string(); + + let mut result = ValidationResult::new(validation_type.clone()); + + // Perform validation (in real app, this would be async) + let validation_result = match field_index { + 0 => self.services.validate_psc(&raw_value), + 1 => self.services.validate_email(&raw_value), + 2 => self.services.validate_username(&raw_value), + 3 => self.services.validate_api_key(&raw_value), + 4 => self.services.validate_credit_card(&raw_value), + _ => ExternalValidationState::NotValidated, + }; + + result = result.complete(validation_result.clone()); + + // Update editor state + self.editor.set_external_validation(field_index, validation_result); + + // Record in history + self.validation_history.push((field_index, raw_value, result.clone())); + + // Update stats + let stats = self.validation_stats.entry(field_index).or_insert((0, Duration::from_secs(0))); + stats.0 += 1; + stats.1 += result.duration(); + + // Limit history size + if self.validation_history.len() > 50 { + self.validation_history.remove(0); + } + + let duration_ms = result.duration().as_millis(); + let cached_text = if result.cached { " (cached)" } else { "" }; + self.debug_message = format!( + "πŸ” {} validation completed in {}ms{}", + validation_type, duration_ms, cached_text + ); + } + + fn validate_all_fields(&mut self) { + let field_count = self.editor.data_provider().field_count().min(5); // Only fields with validation + for i in 0..field_count { + self.validate_field(i); + } + self.debug_message = "πŸ” All fields validated".to_string(); + } + + fn clear_validation_state(&mut self, field_index: Option) { + match field_index { + Some(idx) => { + self.editor.clear_external_validation(idx); + self.debug_message = format!("🧹 Cleared validation for field {}", idx + 1); } - Err(e) => { - self.debug_message = format!("🚫 Cannot move to next field: {}", e); + None => { + for i in 0..5 { // Clear all validation fields + self.editor.clear_external_validation(i); + } + self.validation_history.clear(); + self.validation_stats.clear(); + self.services.clear_cache(); + self.debug_message = "🧹 Cleared all validation states and cache".to_string(); } } } + + fn toggle_validation(&mut self) { + self.validation_enabled = !self.validation_enabled; + if !self.validation_enabled { + self.clear_validation_state(None); + } + self.debug_message = if self.validation_enabled { + "βœ… External validation ENABLED".to_string() + } else { + "❌ External validation DISABLED".to_string() + }; + } + + fn toggle_history_view(&mut self) { + self.show_history = !self.show_history; + self.debug_message = if self.show_history { + "πŸ“œ Showing validation history".to_string() + } else { + "πŸ“Š Showing validation status".to_string() + }; + } + + fn cycle_examples(&mut self) { + let examples = [ + // Valid examples + vec!["01001", "user@gmail.com", "alice_dev", "valid_api_key_123456789012345", "4000123456789012", "Valid data"], + // Invalid examples + vec!["00000", "invalid-email", "admin", "short_key", "0000000000000000", "Invalid data"], + // Warning examples + vec!["12345", "test@example.com", "temp_user", "test_api_key_123456789012345", "4111111111111111", "Warning cases"], + // Mixed scenarios + vec!["99999", "user@blocked.com", "john123", "expired_key_12345678901", "5555555555554444", "Mixed scenarios"], + ]; + + self.example_mode = (self.example_mode + 1) % examples.len(); + let current_examples = &examples[self.example_mode]; + + for (i, example) in current_examples.iter().enumerate() { + if i < self.editor.data_provider().field_count() { + self.editor.data_provider_mut().set_field_value(i, example.to_string()); + } + } + + let mode_names = ["Valid Examples", "Invalid Examples", "Warning Cases", "Mixed Scenarios"]; + self.debug_message = format!("πŸ“‹ Loaded: {}", mode_names[self.example_mode]); + } + + fn get_validation_summary(&self) -> String { + let total_validations: u32 = self.validation_stats.values().map(|(count, _)| count).sum(); + let avg_time_ms = if total_validations > 0 { + let total_time: Duration = self.validation_stats.values().map(|(_, time)| *time).sum(); + total_time.as_millis() / total_validations as u128 + } else { + 0 + }; + + format!("Total: {} validations, Avg: {}ms", total_validations, avg_time_ms) + } + + fn get_field_validation_state(&self, field_index: usize) -> ExternalValidationState { + self.editor.ui_state().validation_state().get_external_validation(field_index) + } + + // Editor pass-through methods + fn enter_edit_mode(&mut self) { + self.editor.enter_edit_mode(); + let rules = self.field_validation_rules(); + self.debug_message = format!("✏️ EDITING {} - {}", self.field_type(), rules); + } + + fn exit_edit_mode(&mut self) { + let current_field = self.current_field(); + self.editor.exit_edit_mode(); + + // Auto-validate on blur if enabled + if self.auto_validate && self.has_external_validation() { + self.validate_field(current_field); + } + + self.debug_message = format!("πŸ”’ NORMAL - {}", self.field_type()); + } + + fn next_field(&mut self) { + let current = self.current_field(); + if let Ok(()) = self.editor.next_field() { + if self.auto_validate && current < 5 { + self.validate_field(current); + } + self.debug_message = "➑ Next field".to_string(); + } + } + fn prev_field(&mut self) { - match self.editor.prev_field() { - Ok(()) => { - self.debug_message = "β¬… Switched to previous field".to_string(); - // Validate field we just left - let prev = self.editor.current_field() + 1; - self.trigger_field_validation(prev); - } - Err(e) => { - self.debug_message = format!("🚫 Cannot move to previous field: {}", e); + let current = self.current_field(); + if let Ok(()) = self.editor.prev_field() { + if self.auto_validate && current < 5 { + self.validate_field(current); } + self.debug_message = "β¬… Previous field".to_string(); } } fn insert_char(&mut self, ch: char) -> anyhow::Result<()> { self.editor.insert_char(ch) } + fn delete_backward(&mut self) -> anyhow::Result<()> { self.editor.delete_backward() } + fn delete_forward(&mut self) -> anyhow::Result<()> { self.editor.delete_forward() } - - // Toggle panel hint: raw vs formatted clarification (canvas obeys library rules) - fn toggle_raw_hint(&mut self) { - self.show_raw_hint = !self.show_raw_hint; - self.debug_message = if self.show_raw_hint { - "πŸ‘ Showing raw vs display hints in panel".to_string() - } else { - "🎭 Hints off".to_string() - }; - } - - // Manually trigger validation for current field (press 'v') - fn trigger_current_field_validation(&mut self) { - let idx = self.editor.current_field(); - self.trigger_field_validation(idx); - } - - // Core: "frontend" triggers validation on blur - fn trigger_field_validation(&mut self, field_index: usize) { - // Only if field exists and has external_validation enabled - if let Some(cfg) = self.editor.validation_state().get_field_config(field_index) { - if cfg.external_validation_enabled { - let raw = self.editor.data_provider().field_value(field_index).to_string(); - if raw.is_empty() { - // Clear if empty - self.editor.clear_external_validation(field_index); - return; - } - // Set Validating - self.editor.set_external_validation(field_index, ExternalValidationState::Validating); - - // "Async" backend simulation: immediate result calculation. In a real app, do this later. - let result = simulate_external_psc_validation(&raw); - self.editor.set_external_validation(field_index, result); - } - } - } - - // Read external validation state for panel - fn external_state(&self, field_index: usize) -> ExternalValidationState { - self.editor.validation_state().get_external_validation(field_index) - } - - // Clear external validation state (press 'c') - fn clear_external_state(&mut self) { - let idx = self.editor.current_field(); - self.editor.clear_external_validation(idx); - self.debug_message = "🧹 Cleared external validation state".to_string(); - } } -// UI - fn run_app( terminal: &mut Terminal, - mut editor: DemoEditor, + mut editor: ValidationDemoEditor, ) -> io::Result<()> { loop { terminal.draw(|f| ui(f, &editor))?; @@ -287,141 +823,242 @@ fn run_app( let km = key.modifiers; // Quit - if (kc == KeyCode::Char('q') && km.contains(KeyModifiers::CONTROL)) - || (kc == KeyCode::Char('c') && km.contains(KeyModifiers::CONTROL)) - || kc == KeyCode::F(10) - { + if matches!(kc, KeyCode::F(10)) || + (kc == KeyCode::Char('q') && km.contains(KeyModifiers::CONTROL)) || + (kc == KeyCode::Char('c') && km.contains(KeyModifiers::CONTROL)) { break; } match (mode, kc, km) { - // Modes + // Mode transitions (AppMode::ReadOnly, KeyCode::Char('i'), _) => editor.enter_edit_mode(), - (AppMode::ReadOnly, KeyCode::Char('a'), _) => editor.enter_append_mode(), + (AppMode::ReadOnly, KeyCode::Char('a'), _) => { + editor.editor.enter_append_mode(); + let rules = editor.field_validation_rules(); + editor.debug_message = format!("✏️ APPEND {} - {}", editor.field_type(), rules); + }, (_, KeyCode::Esc, _) => editor.exit_edit_mode(), - // Movement + // Movement - cursor within field + (_, KeyCode::Left, _) | (AppMode::ReadOnly, KeyCode::Char('h'), _) => { let _ = editor.editor.move_left(); }, + (_, KeyCode::Right, _) | (AppMode::ReadOnly, KeyCode::Char('l'), _) => { let _ = editor.editor.move_right(); }, + (_, KeyCode::Up, _) | (AppMode::ReadOnly, KeyCode::Char('k'), _) => { let _ = editor.editor.move_up(); }, + (_, KeyCode::Down, _) | (AppMode::ReadOnly, KeyCode::Char('j'), _) => { let _ = editor.editor.move_down(); }, + // Field switching (_, KeyCode::Tab, _) => editor.next_field(), (_, KeyCode::BackTab, _) => editor.prev_field(), - // Edit + // Validation commands + (_, KeyCode::Char('v'), _) => { + let field = editor.current_field(); + editor.validate_field(field); + }, + (_, KeyCode::Char('V'), _) => editor.validate_all_fields(), + (_, KeyCode::Char('c'), _) => { + let field = editor.current_field(); + editor.clear_validation_state(Some(field)); + }, + (_, KeyCode::Char('C'), _) => editor.clear_validation_state(None), + + // UI toggles + (_, KeyCode::Char('r'), _) => editor.toggle_history_view(), + (_, KeyCode::Char('e'), _) => editor.cycle_examples(), + (_, KeyCode::F(1), _) => editor.toggle_validation(), + + // Editing + (AppMode::Edit, KeyCode::Left, _) => { let _ = editor.editor.move_left(); }, + (AppMode::Edit, KeyCode::Right, _) => { let _ = editor.editor.move_right(); }, + (AppMode::Edit, KeyCode::Up, _) => { let _ = editor.editor.move_up(); }, + (AppMode::Edit, KeyCode::Down, _) => { let _ = editor.editor.move_down(); }, (AppMode::Edit, KeyCode::Char(c), m) if !m.contains(KeyModifiers::CONTROL) => { let _ = editor.insert_char(c); - } - (AppMode::Edit, KeyCode::Backspace, _) => { let _ = editor.delete_backward(); } - (AppMode::Edit, KeyCode::Delete, _) => { let _ = editor.delete_forward(); } + }, + (AppMode::Edit, KeyCode::Backspace, _) => { let _ = editor.delete_backward(); }, + (AppMode::Edit, KeyCode::Delete, _) => { let _ = editor.delete_forward(); }, - // External validation - (_, KeyCode::Char('v'), _) => { - editor.trigger_current_field_validation(); - } - (_, KeyCode::Char('c'), _) => { - editor.clear_external_state(); - } - - // Panel - (_, KeyCode::Char('r'), _) => editor.toggle_raw_hint(), + // Help + (_, KeyCode::Char('h'), _) => { + let rules = editor.field_validation_rules(); + editor.debug_message = format!("ℹ️ {} field: {}", editor.field_type(), rules); + }, _ => {} } } } - Ok(()) } -fn ui(f: &mut Frame, editor: &DemoEditor) { +fn ui(f: &mut Frame, editor: &ValidationDemoEditor) { let chunks = Layout::default() .direction(Direction::Vertical) - .constraints([Constraint::Min(8), Constraint::Length(14)]) + .constraints([Constraint::Min(8), Constraint::Length(16)]) .split(f.area()); render_canvas_default(f, chunks[0], &editor.editor); - - render_status_panel(f, chunks[1], editor); + render_validation_panel(f, chunks[1], editor); } -fn render_status_panel(f: &mut Frame, area: Rect, editor: &DemoEditor) { +fn render_validation_panel( + f: &mut Frame, + area: Rect, + editor: &ValidationDemoEditor, +) { let chunks = Layout::default() .direction(Direction::Vertical) .constraints([ - Constraint::Length(3), - Constraint::Length(5), - Constraint::Length(6), + Constraint::Length(3), // Status bar + Constraint::Length(6), // Validation states + Constraint::Length(7), // History or Help ]) .split(area); - // Bar + // Status bar let mode_text = match editor.mode() { AppMode::Edit => "INSERT", - AppMode::ReadOnly => "NORMAL", + AppMode::ReadOnly => "NORMAL", _ => "OTHER", }; - let bar = Paragraph::new(Line::from(Span::raw(format!( - "-- {} -- {}", + let summary = editor.get_validation_summary(); + let status_text = format!( + "-- {} -- {} | {} | Auto: {} | View: {}", mode_text, - editor.debug_message - )))) - .block(Block::default().borders(Borders::ALL).title("πŸ§ͺ External Validation Demo")); + editor.debug_message, + summary, + if editor.auto_validate { "ON" } else { "OFF" }, + if editor.show_history { "HISTORY" } else { "STATUS" } + ); - f.render_widget(bar, chunks[0]); + let status = Paragraph::new(Line::from(Span::raw(status_text))) + .block(Block::default().borders(Borders::ALL).title("πŸ§ͺ External Validation Demo")); + f.render_widget(status, chunks[0]); - // External validation snapshot - let mut lines = Vec::new(); - let field_count = editor.data_provider().field_count(); - for i in 0..field_count { - let name = editor.data_provider().field_name(i); - let raw = editor.editor.data_provider().field_value(i); - let display = editor.editor.display_text_for_field(i); - let state = editor.external_state(i); - let (label, color) = match state { + // Validation states for all fields - FIXED: render each field on its own line + let mut field_lines: Vec = Vec::new(); + for i in 0..editor.data_provider().field_count() { + let field_name = editor.data_provider().field_name(i); + let raw_value = editor.data_provider().field_value(i); + let state = editor.get_field_validation_state(i); + + let (state_text, color) = match state { ExternalValidationState::NotValidated => ("Not validated", Color::Gray), ExternalValidationState::Validating => ("Validating…", Color::Blue), - ExternalValidationState::Valid(Some(_)) => ("Valid βœ“", Color::Green), + ExternalValidationState::Valid(Some(ref msg)) => (msg.as_str(), Color::Green), ExternalValidationState::Valid(None) => ("Valid βœ“", Color::Green), - ExternalValidationState::Invalid { .. } => ("Invalid βœ–", Color::Red), - ExternalValidationState::Warning { .. } => ("Warning ⚠", Color::Yellow), + ExternalValidationState::Invalid { ref message, .. } => (message.as_str(), Color::Red), + ExternalValidationState::Warning { ref message } => (message.as_str(), Color::Yellow), }; - let mut text = format!("{}: ", name); - if editor.show_raw_hint { - text.push_str(&format!("raw='{}' display='{}' | ", raw, display)); - } - text.push_str(label); + let indicator = if i == editor.current_field() { "β–Ί " } else { " " }; - lines.push(Span::styled(text, Style::default().fg(color))); - lines.push(Span::raw("\n")); + let value_display = if raw_value.len() > 15 { + format!("{}...", &raw_value[..12]) + } else if raw_value.is_empty() { + "(empty)".to_string() + } else { + raw_value.to_string() + }; + + let field_line = Line::from(vec![ + Span::styled(format!("{}{}: ", indicator, field_name), Style::default().fg(Color::White)), + Span::raw(format!("'{}' β†’ ", value_display)), + Span::styled(state_text.to_string(), Style::default().fg(color)), + ]); + + field_lines.push(field_line); } - let snapshot = Paragraph::new(Line::from(lines)) - .block(Block::default().borders(Borders::ALL).title("πŸ“‘ External validation states")); - f.render_widget(snapshot, chunks[1]); + // Use Vec to avoid a single long line overflowing + let validation_states = Paragraph::new(field_lines) + .block(Block::default().borders(Borders::ALL).title("πŸ” Validation States")); + f.render_widget(validation_states, chunks[1]); - // Help - let help = Paragraph::new( - "Controls:\n\ - β€’ i/a: insert/append, Esc: exit edit (validates PSC)\n\ - β€’ Tab/Shift+Tab: switch fields (validates PSC)\n\ - β€’ v: validate current field now, c: clear validation state\n\ - β€’ r: toggle raw/display hints (panel only)\n\ - β€’ Ctrl+C/F10: quit\n\ - \n\ - PSC rules:\n\ - β€’ Raw typing only while editing (Feature 4) and max 5 digits\n\ - β€’ On blur/exit: external validation occurs (Feature 5)\n\ - β€’ Known Valid: 01001; Invalid: 00000 or non-digits; Warning: incomplete or 12345", - ) - .style(Style::default().fg(Color::Gray)) - .block(Block::default().borders(Borders::ALL).title("β„Ή Help")) - .wrap(Wrap { trim: true }); - f.render_widget(help, chunks[2]); + // History or Help panel + if editor.show_history { + let recent_history: Vec = editor.validation_history + .iter() + .rev() + .take(5) + .map(|(field_idx, value, result)| { + let field_name = match field_idx { + 0 => "PSC", + 1 => "Email", + 2 => "Username", + 3 => "API Key", + 4 => "Card", + _ => "Other", + }; + + let duration_ms = result.duration().as_millis(); + let cached_text = if result.cached { " (cached)" } else { "" }; + let short_value = if value.len() > 15 { + format!("{}...", &value[..12]) + } else { + value.clone() + }; + + let state_summary = match &result.state { + ExternalValidationState::Valid(_) => "βœ“ Valid", + ExternalValidationState::Invalid { .. } => "βœ– Invalid", + ExternalValidationState::Warning { .. } => "⚠ Warning", + ExternalValidationState::Validating => "… Validating", + ExternalValidationState::NotValidated => "β—‹ Not validated", + }; + + ListItem::new(format!( + "{}: '{}' β†’ {} ({}ms{})", + field_name, short_value, state_summary, duration_ms, cached_text + )) + }) + .collect(); + + let history = List::new(recent_history) + .block(Block::default().borders(Borders::ALL).title("πŸ“œ Validation History (recent 5)")); + f.render_widget(history, chunks[2]); + } else { + let help_text = match editor.mode() { + AppMode::ReadOnly => { + "πŸ§ͺ EXTERNAL VALIDATION DEMO - Multiple validation types with async simulation\n\ + \n\ + Commands: v=validate current, V=validate all, c=clear current, C=clear all\n\ + e=cycle examples, r=toggle history, h=field help, F1=toggle validation\n\ + Movement: Tab/Shift+Tab=switch fields, i/a=insert/append, Esc=exit edit\n\ + \n\ + Try different values to see validation in action!" + } + AppMode::Edit => { + "✏️ EDITING MODE - Type to see validation on field blur\n\ + \n\ + Current field validation will trigger when you:\n\ + β€’ Press Esc (exit edit mode)\n\ + β€’ Press Tab (move to next field)\n\ + β€’ Press 'v' manually\n\ + \n\ + Esc=exit edit, arrows=navigate, Backspace/Del=delete" + } + _ => "πŸ§ͺ Enhanced External Validation Demo" + }; + + let help = Paragraph::new(help_text) + .block(Block::default().borders(Borders::ALL).title("πŸš€ External Validation Features")) + .style(Style::default().fg(Color::Gray)) + .wrap(Wrap { trim: true }); + f.render_widget(help, chunks[2]); + } } fn main() -> Result<(), Box> { - println!("πŸ§ͺ Feature 5: External validation (UI-only) + Feature 4: Custom formatter"); + println!("πŸ§ͺ Enhanced External Validation Demo (Feature 5)"); println!("βœ… validation feature: ENABLED"); - println!("βœ… gui feature: ENABLED"); + println!("βœ… gui feature: ENABLED"); + println!("πŸ§ͺ Enhanced features:"); + println!(" β€’ 5 different external validation types with realistic scenarios"); + println!(" β€’ Validation caching and performance metrics"); + println!(" β€’ Comprehensive validation history and error handling"); + println!(" β€’ Multiple example datasets for testing edge cases"); + println!(" β€’ Progressive validation patterns (local + remote simulation)"); + println!(); enable_raw_mode()?; let mut stdout = io::stdout(); @@ -429,8 +1066,8 @@ fn main() -> Result<(), Box> { let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; - let data = DemoData::new(); - let editor = DemoEditor::new(data); + let data = ValidationDemoData::new(); + let editor = ValidationDemoEditor::new(data); let res = run_app(&mut terminal, editor); @@ -442,7 +1079,11 @@ fn main() -> Result<(), Box> { println!("{:?}", err); } - println!("πŸŽ‰ Example finished"); + println!("πŸ§ͺ Enhanced external validation demo completed!"); + println!("πŸ† You experienced comprehensive external validation with:"); + println!(" β€’ Multiple validation services (PSC, Email, Username, API Key, Credit Card)"); + println!(" β€’ Realistic async validation simulation with caching"); + println!(" β€’ Comprehensive error handling and user feedback"); + println!(" β€’ Performance metrics and validation history tracking"); Ok(()) } -