feature5 implementation is full now

This commit is contained in:
Priec
2025-08-07 00:03:11 +02:00
parent 34c68858a3
commit 6ba0124779
5 changed files with 520 additions and 6 deletions

View 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(())
}

View File

@@ -134,6 +134,24 @@ impl<D: DataProvider> FormEditor<D> {
&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.
///
/// Policies:

View File

@@ -22,6 +22,9 @@ pub struct ValidationConfig {
#[cfg(feature = "validation")]
pub custom_formatter: Option<Arc<dyn CustomFormatter + Send + Sync>>,
/// Enable external validation indicator UI (feature 5)
pub external_validation_enabled: bool,
/// Future: External validation
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)
.finish()
}
@@ -285,6 +289,12 @@ impl ValidationConfigBuilder {
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
pub fn build(self) -> ValidationConfig {
self.config

View File

@@ -16,6 +16,16 @@ pub use patterns::{PatternFilters, PositionFilter, PositionRange, CharacterFilte
pub use mask::DisplayMask; // Simple mask instead of ReservedCharacters
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
#[derive(Debug, Clone, thiserror::Error)]
pub enum ValidationError {

View File

@@ -1,9 +1,7 @@
// src/validation/state.rs
//! Validation state management
use crate::validation::{ValidationConfig, ValidationResult};
#[cfg(feature = "validation")]
use crate::validation::{PositionMapper};
use crate::validation::{ValidationConfig, ValidationResult, ExternalValidationState};
use std::collections::HashMap;
/// Validation state for all fields in a form
@@ -20,6 +18,9 @@ pub struct ValidationState {
/// Global validation enabled/disabled
enabled: bool,
/// External validation results per field (Feature 5)
external_results: HashMap<usize, ExternalValidationState>,
}
impl ValidationState {
@@ -30,6 +31,7 @@ impl ValidationState {
field_results: HashMap::new(),
validated_fields: std::collections::HashSet::new(),
enabled: true,
external_results: HashMap::new(),
}
}
@@ -40,6 +42,7 @@ impl ValidationState {
// Clear all validation results when disabled
self.field_results.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
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);
} else {
self.field_configs.remove(&field_index);
self.field_results.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_results.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
@@ -131,7 +159,7 @@ impl ValidationState {
&self,
field_index: usize,
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)?;
config.run_custom_formatter(raw)
}