feature5 implementation is full now
This commit is contained in:
448
canvas/examples/validation_5.rs
Normal file
448
canvas/examples/validation_5.rs
Normal file
@@ -0,0 +1,448 @@
|
|||||||
|
// examples/validation_5.rs
|
||||||
|
//! Feature 5: External validation (UI-only) demo with Feature 4 (custom formatter)
|
||||||
|
//!
|
||||||
|
//! 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.
|
||||||
|
//!
|
||||||
|
//! 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)
|
||||||
|
//! - v: manually trigger validation of current field
|
||||||
|
//! - c: clear external validation state for current field
|
||||||
|
//! - r: toggle raw/format view flag in panel (visual only; canvas follows library rules)
|
||||||
|
//! - F10/Ctrl+C: quit
|
||||||
|
//!
|
||||||
|
//! Run: cargo run --example validation_5 --features "gui,validation"
|
||||||
|
|
||||||
|
#![allow(clippy::needless_return)]
|
||||||
|
|
||||||
|
#[cfg(not(all(feature = "validation", feature = "gui")))]
|
||||||
|
compile_error!(
|
||||||
|
"This example requires the 'validation' and 'gui' features. \
|
||||||
|
Run with: cargo run --example validation_5 --features \"gui,validation\""
|
||||||
|
);
|
||||||
|
|
||||||
|
use std::io;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
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,
|
||||||
|
ValidationConfigBuilder, CustomFormatter, FormattingResult,
|
||||||
|
validation::ExternalValidationState,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// PSC custom formatter for display ("XXX XX").
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
FormattingResult::success(out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Demo data provider: PSC + Notes
|
||||||
|
struct DemoData {
|
||||||
|
fields: Vec<(String, String)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DemoData {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
fields: vec![
|
||||||
|
("🏁 PSC (5 digits)".to_string(), "".to_string()),
|
||||||
|
("📝 Notes".to_string(), "".to_string()),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DataProvider for DemoData {
|
||||||
|
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<canvas::ValidationConfig> {
|
||||||
|
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()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_ => 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<D: DataProvider> {
|
||||||
|
editor: FormEditor<D>,
|
||||||
|
debug_message: String,
|
||||||
|
show_raw_hint: bool, // panel-only toggle, does not affect canvas
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<D: DataProvider> DemoEditor<D> {
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
self.debug_message = format!("🚫 Cannot move to next field: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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<B: Backend>(
|
||||||
|
terminal: &mut Terminal<B>,
|
||||||
|
mut editor: DemoEditor<DemoData>,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
loop {
|
||||||
|
terminal.draw(|f| ui(f, &editor))?;
|
||||||
|
|
||||||
|
if let Event::Key(key) = event::read()? {
|
||||||
|
let mode = editor.mode();
|
||||||
|
let kc = key.code;
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
match (mode, kc, km) {
|
||||||
|
// Modes
|
||||||
|
(AppMode::ReadOnly, KeyCode::Char('i'), _) => editor.enter_edit_mode(),
|
||||||
|
(AppMode::ReadOnly, KeyCode::Char('a'), _) => editor.enter_append_mode(),
|
||||||
|
(_, KeyCode::Esc, _) => editor.exit_edit_mode(),
|
||||||
|
|
||||||
|
// Movement
|
||||||
|
(_, KeyCode::Tab, _) => editor.next_field(),
|
||||||
|
(_, KeyCode::BackTab, _) => editor.prev_field(),
|
||||||
|
|
||||||
|
// Edit
|
||||||
|
(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(); }
|
||||||
|
|
||||||
|
// 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(),
|
||||||
|
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ui(f: &mut Frame, editor: &DemoEditor<DemoData>) {
|
||||||
|
let chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([Constraint::Min(8), Constraint::Length(14)])
|
||||||
|
.split(f.area());
|
||||||
|
|
||||||
|
render_canvas_default(f, chunks[0], &editor.editor);
|
||||||
|
|
||||||
|
render_status_panel(f, chunks[1], editor);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_status_panel(f: &mut Frame, area: Rect, editor: &DemoEditor<DemoData>) {
|
||||||
|
let chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([
|
||||||
|
Constraint::Length(3),
|
||||||
|
Constraint::Length(5),
|
||||||
|
Constraint::Length(6),
|
||||||
|
])
|
||||||
|
.split(area);
|
||||||
|
|
||||||
|
// Bar
|
||||||
|
let mode_text = match editor.mode() {
|
||||||
|
AppMode::Edit => "INSERT",
|
||||||
|
AppMode::ReadOnly => "NORMAL",
|
||||||
|
_ => "OTHER",
|
||||||
|
};
|
||||||
|
|
||||||
|
let bar = Paragraph::new(Line::from(Span::raw(format!(
|
||||||
|
"-- {} -- {}",
|
||||||
|
mode_text,
|
||||||
|
editor.debug_message
|
||||||
|
))))
|
||||||
|
.block(Block::default().borders(Borders::ALL).title("🧪 External Validation Demo"));
|
||||||
|
|
||||||
|
f.render_widget(bar, 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 {
|
||||||
|
ExternalValidationState::NotValidated => ("Not validated", Color::Gray),
|
||||||
|
ExternalValidationState::Validating => ("Validating…", Color::Blue),
|
||||||
|
ExternalValidationState::Valid(Some(_)) => ("Valid ✓", Color::Green),
|
||||||
|
ExternalValidationState::Valid(None) => ("Valid ✓", Color::Green),
|
||||||
|
ExternalValidationState::Invalid { .. } => ("Invalid ✖", Color::Red),
|
||||||
|
ExternalValidationState::Warning { .. } => ("Warning ⚠", Color::Yellow),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut text = format!("{}: ", name);
|
||||||
|
if editor.show_raw_hint {
|
||||||
|
text.push_str(&format!("raw='{}' display='{}' | ", raw, display));
|
||||||
|
}
|
||||||
|
text.push_str(label);
|
||||||
|
|
||||||
|
lines.push(Span::styled(text, Style::default().fg(color)));
|
||||||
|
lines.push(Span::raw("\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let snapshot = Paragraph::new(Line::from(lines))
|
||||||
|
.block(Block::default().borders(Borders::ALL).title("📡 External validation states"));
|
||||||
|
f.render_widget(snapshot, 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]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
println!("🧪 Feature 5: External validation (UI-only) + Feature 4: Custom formatter");
|
||||||
|
println!("✅ validation feature: ENABLED");
|
||||||
|
println!("✅ gui feature: ENABLED");
|
||||||
|
|
||||||
|
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 = DemoData::new();
|
||||||
|
let editor = DemoEditor::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!("🎉 Example finished");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
@@ -134,6 +134,24 @@ impl<D: DataProvider> FormEditor<D> {
|
|||||||
&self.ui_state
|
&self.ui_state
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set external validation state for a field (Feature 5)
|
||||||
|
#[cfg(feature = "validation")]
|
||||||
|
pub fn set_external_validation(
|
||||||
|
&mut self,
|
||||||
|
field_index: usize,
|
||||||
|
state: crate::validation::ExternalValidationState,
|
||||||
|
) {
|
||||||
|
self.ui_state
|
||||||
|
.validation
|
||||||
|
.set_external_validation(field_index, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear external validation state for a field (Feature 5)
|
||||||
|
#[cfg(feature = "validation")]
|
||||||
|
pub fn clear_external_validation(&mut self, field_index: usize) {
|
||||||
|
self.ui_state.validation.clear_external_validation(field_index);
|
||||||
|
}
|
||||||
|
|
||||||
/// Get effective display text for any field index.
|
/// Get effective display text for any field index.
|
||||||
///
|
///
|
||||||
/// Policies:
|
/// Policies:
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ pub struct ValidationConfig {
|
|||||||
#[cfg(feature = "validation")]
|
#[cfg(feature = "validation")]
|
||||||
pub custom_formatter: Option<Arc<dyn CustomFormatter + Send + Sync>>,
|
pub custom_formatter: Option<Arc<dyn CustomFormatter + Send + Sync>>,
|
||||||
|
|
||||||
|
/// Enable external validation indicator UI (feature 5)
|
||||||
|
pub external_validation_enabled: bool,
|
||||||
|
|
||||||
/// Future: External validation
|
/// Future: External validation
|
||||||
pub external_validation: Option<()>, // Placeholder for future implementation
|
pub external_validation: Option<()>, // Placeholder for future implementation
|
||||||
}
|
}
|
||||||
@@ -47,6 +50,7 @@ impl std::fmt::Debug for ValidationConfig {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
.field("external_validation_enabled", &self.external_validation_enabled)
|
||||||
.field("external_validation", &self.external_validation)
|
.field("external_validation", &self.external_validation)
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
@@ -285,6 +289,12 @@ impl ValidationConfigBuilder {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Enable or disable external validation indicator UI (feature 5)
|
||||||
|
pub fn with_external_validation_enabled(mut self, enabled: bool) -> Self {
|
||||||
|
self.config.external_validation_enabled = enabled;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Build the final validation configuration
|
/// Build the final validation configuration
|
||||||
pub fn build(self) -> ValidationConfig {
|
pub fn build(self) -> ValidationConfig {
|
||||||
self.config
|
self.config
|
||||||
|
|||||||
@@ -16,6 +16,16 @@ pub use patterns::{PatternFilters, PositionFilter, PositionRange, CharacterFilte
|
|||||||
pub use mask::DisplayMask; // Simple mask instead of ReservedCharacters
|
pub use mask::DisplayMask; // Simple mask instead of ReservedCharacters
|
||||||
pub use formatting::{CustomFormatter, FormattingResult, PositionMapper, DefaultPositionMapper};
|
pub use formatting::{CustomFormatter, FormattingResult, PositionMapper, DefaultPositionMapper};
|
||||||
|
|
||||||
|
/// External validation UI state (Feature 5)
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum ExternalValidationState {
|
||||||
|
NotValidated,
|
||||||
|
Validating,
|
||||||
|
Valid(Option<String>),
|
||||||
|
Invalid { message: String, suggestion: Option<String> },
|
||||||
|
Warning { message: String },
|
||||||
|
}
|
||||||
|
|
||||||
/// Validation error types
|
/// Validation error types
|
||||||
#[derive(Debug, Clone, thiserror::Error)]
|
#[derive(Debug, Clone, thiserror::Error)]
|
||||||
pub enum ValidationError {
|
pub enum ValidationError {
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
// src/validation/state.rs
|
// src/validation/state.rs
|
||||||
//! Validation state management
|
//! Validation state management
|
||||||
|
|
||||||
use crate::validation::{ValidationConfig, ValidationResult};
|
use crate::validation::{ValidationConfig, ValidationResult, ExternalValidationState};
|
||||||
#[cfg(feature = "validation")]
|
|
||||||
use crate::validation::{PositionMapper};
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
/// Validation state for all fields in a form
|
/// Validation state for all fields in a form
|
||||||
@@ -20,6 +18,9 @@ pub struct ValidationState {
|
|||||||
|
|
||||||
/// Global validation enabled/disabled
|
/// Global validation enabled/disabled
|
||||||
enabled: bool,
|
enabled: bool,
|
||||||
|
|
||||||
|
/// External validation results per field (Feature 5)
|
||||||
|
external_results: HashMap<usize, ExternalValidationState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ValidationState {
|
impl ValidationState {
|
||||||
@@ -30,6 +31,7 @@ impl ValidationState {
|
|||||||
field_results: HashMap::new(),
|
field_results: HashMap::new(),
|
||||||
validated_fields: std::collections::HashSet::new(),
|
validated_fields: std::collections::HashSet::new(),
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
external_results: HashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,6 +42,7 @@ impl ValidationState {
|
|||||||
// Clear all validation results when disabled
|
// Clear all validation results when disabled
|
||||||
self.field_results.clear();
|
self.field_results.clear();
|
||||||
self.validated_fields.clear();
|
self.validated_fields.clear();
|
||||||
|
self.external_results.clear(); // Also clear external results
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,12 +53,13 @@ impl ValidationState {
|
|||||||
|
|
||||||
/// Set validation configuration for a field
|
/// Set validation configuration for a field
|
||||||
pub fn set_field_config(&mut self, field_index: usize, config: ValidationConfig) {
|
pub fn set_field_config(&mut self, field_index: usize, config: ValidationConfig) {
|
||||||
if config.has_validation() {
|
if config.has_validation() || config.external_validation_enabled {
|
||||||
self.field_configs.insert(field_index, config);
|
self.field_configs.insert(field_index, config);
|
||||||
} else {
|
} else {
|
||||||
self.field_configs.remove(&field_index);
|
self.field_configs.remove(&field_index);
|
||||||
self.field_results.remove(&field_index);
|
self.field_results.remove(&field_index);
|
||||||
self.validated_fields.remove(&field_index);
|
self.validated_fields.remove(&field_index);
|
||||||
|
self.external_results.remove(&field_index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,6 +73,30 @@ impl ValidationState {
|
|||||||
self.field_configs.remove(&field_index);
|
self.field_configs.remove(&field_index);
|
||||||
self.field_results.remove(&field_index);
|
self.field_results.remove(&field_index);
|
||||||
self.validated_fields.remove(&field_index);
|
self.validated_fields.remove(&field_index);
|
||||||
|
self.external_results.remove(&field_index);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set external validation state for a field (Feature 5)
|
||||||
|
pub fn set_external_validation(&mut self, field_index: usize, state: ExternalValidationState) {
|
||||||
|
self.external_results.insert(field_index, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get current external validation state for a field
|
||||||
|
pub fn get_external_validation(&self, field_index: usize) -> ExternalValidationState {
|
||||||
|
self.external_results
|
||||||
|
.get(&field_index)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or(ExternalValidationState::NotValidated)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear external validation state for a field
|
||||||
|
pub fn clear_external_validation(&mut self, field_index: usize) {
|
||||||
|
self.external_results.remove(&field_index);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear all external validation states
|
||||||
|
pub fn clear_all_external_validation(&mut self) {
|
||||||
|
self.external_results.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Validate character insertion for a field
|
/// Validate character insertion for a field
|
||||||
@@ -123,7 +151,7 @@ impl ValidationState {
|
|||||||
pub fn get_field_result(&self, field_index: usize) -> Option<&ValidationResult> {
|
pub fn get_field_result(&self, field_index: usize) -> Option<&ValidationResult> {
|
||||||
self.field_results.get(&field_index)
|
self.field_results.get(&field_index)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get formatted display for a field if a custom formatter is configured.
|
/// Get formatted display for a field if a custom formatter is configured.
|
||||||
/// Returns (formatted_text, position_mapper, optional_warning_message).
|
/// Returns (formatted_text, position_mapper, optional_warning_message).
|
||||||
#[cfg(feature = "validation")]
|
#[cfg(feature = "validation")]
|
||||||
@@ -131,7 +159,7 @@ impl ValidationState {
|
|||||||
&self,
|
&self,
|
||||||
field_index: usize,
|
field_index: usize,
|
||||||
raw: &str,
|
raw: &str,
|
||||||
) -> Option<(String, std::sync::Arc<dyn PositionMapper>, Option<String>)> {
|
) -> Option<(String, std::sync::Arc<dyn crate::validation::PositionMapper>, Option<String>)> {
|
||||||
let config = self.field_configs.get(&field_index)?;
|
let config = self.field_configs.get(&field_index)?;
|
||||||
config.run_custom_formatter(raw)
|
config.run_custom_formatter(raw)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user