feature4 implemented and working properly well

This commit is contained in:
Priec
2025-08-06 23:16:04 +02:00
parent 4c8cfd4f80
commit 34c68858a3
8 changed files with 1330 additions and 170 deletions

View File

@@ -53,24 +53,10 @@ pub fn render_canvas_with_highlight<T: CanvasTheme, D: DataProvider>(
for i in 0..field_count {
fields.push(data_provider.field_name(i));
// Use display text that applies masks if configured
// Use editor-provided effective display text per field (Feature 4/mask aware)
#[cfg(feature = "validation")]
{
if i == editor.current_field() {
inputs.push(editor.current_display_text());
} else {
// For non-current fields, we need to apply mask manually
let raw = data_provider.field_value(i);
if let Some(cfg) = editor.ui_state().validation_state().get_field_config(i) {
if let Some(mask) = &cfg.display_mask {
inputs.push(mask.apply_to_display(raw));
} else {
inputs.push(raw.to_string());
}
} else {
inputs.push(raw.to_string());
}
}
inputs.push(editor.display_text_for_field(i));
}
#[cfg(not(feature = "validation"))]
{
@@ -93,23 +79,10 @@ pub fn render_canvas_with_highlight<T: CanvasTheme, D: DataProvider>(
editor.display_cursor_position(), // Use display cursor position for masks
false, // TODO: track unsaved changes in editor
|i| {
// Get display value for field i
// Get display value for field i using editor logic (Feature 4 + masks)
#[cfg(feature = "validation")]
{
if i == editor.current_field() {
editor.current_display_text()
} else {
let raw = data_provider.field_value(i);
if let Some(cfg) = editor.ui_state().validation_state().get_field_config(i) {
if let Some(mask) = &cfg.display_mask {
mask.apply_to_display(raw)
} else {
raw.to_string()
}
} else {
raw.to_string()
}
}
editor.display_text_for_field(i)
}
#[cfg(not(feature = "validation"))]
{
@@ -117,12 +90,21 @@ pub fn render_canvas_with_highlight<T: CanvasTheme, D: DataProvider>(
}
},
|i| {
// Check if field has display override (mask)
// Check if field has display override (custom formatter or mask)
#[cfg(feature = "validation")]
{
editor.ui_state().validation_state().get_field_config(i)
.and_then(|cfg| cfg.display_mask.as_ref())
.is_some()
.map(|cfg| {
// Formatter takes precedence; if present, it's a display override
#[allow(unused_mut)]
let mut has_override = false;
#[cfg(feature = "validation")]
{
has_override = cfg.custom_formatter.is_some();
}
has_override || cfg.display_mask.is_some()
})
.unwrap_or(false)
}
#[cfg(not(feature = "validation"))]
{

View File

@@ -85,7 +85,14 @@ impl<D: DataProvider> FormEditor<D> {
}
}
/// Get current field text for display, applying mask if configured
/// Get current field text for display.
///
/// Policies:
/// - Feature 4 (custom formatter):
/// - While editing the focused field: ALWAYS show raw (no custom formatting).
/// - When not editing the field: show formatted (fallback to raw on error).
/// - Mask-only fields: mask applies even in Edit mode (preserve legacy behavior).
/// - Otherwise: raw.
#[cfg(feature = "validation")]
pub fn current_display_text(&self) -> String {
let field_index = self.ui_state.current_field;
@@ -96,10 +103,29 @@ impl<D: DataProvider> FormEditor<D> {
};
if let Some(cfg) = self.ui_state.validation.get_field_config(field_index) {
// 1) Mask-only fields: mask applies even in Edit (legacy behavior)
if cfg.custom_formatter.is_none() {
if let Some(mask) = &cfg.display_mask {
return mask.apply_to_display(raw);
}
}
// 2) Feature 4 fields: raw while editing, formatted otherwise
if cfg.custom_formatter.is_some() {
if matches!(self.ui_state.current_mode, AppMode::Edit) {
return raw.to_string();
}
if let Some((formatted, _mapper, _warning)) = cfg.run_custom_formatter(raw) {
return formatted;
}
}
// 3) Fallback to mask if present (when formatter didn't produce output)
if let Some(mask) = &cfg.display_mask {
return mask.apply_to_display(raw);
}
}
raw.to_string()
}
@@ -108,6 +134,53 @@ impl<D: DataProvider> FormEditor<D> {
&self.ui_state
}
/// Get effective display text for any field index.
///
/// Policies:
/// - Feature 4 fields (with custom formatter):
/// - If the field is currently focused AND in Edit mode: return raw (no formatting).
/// - Otherwise: return formatted (fallback to raw on error).
/// - Mask-only fields: mask applies regardless of mode (legacy behavior).
/// - Otherwise: raw.
#[cfg(feature = "validation")]
pub fn display_text_for_field(&self, field_index: usize) -> String {
let raw = if field_index < self.data_provider.field_count() {
self.data_provider.field_value(field_index)
} else {
""
};
if let Some(cfg) = self.ui_state.validation.get_field_config(field_index) {
// Mask-only fields: mask applies even in Edit mode
if cfg.custom_formatter.is_none() {
if let Some(mask) = &cfg.display_mask {
return mask.apply_to_display(raw);
}
}
// Feature 4 fields:
if cfg.custom_formatter.is_some() {
// Focused + Edit -> raw
if field_index == self.ui_state.current_field
&& matches!(self.ui_state.current_mode, AppMode::Edit)
{
return raw.to_string();
}
// Not editing -> formatted
if let Some((formatted, _mapper, _warning)) = cfg.run_custom_formatter(raw) {
return formatted;
}
}
// Fallback to mask if present (in case formatter didn't return output)
if let Some(mask) = &cfg.display_mask {
return mask.apply_to_display(raw);
}
}
raw.to_string()
}
/// Get reference to data provider for rendering
pub fn data_provider(&self) -> &D {
&self.data_provider
@@ -959,7 +1032,7 @@ impl<D: DataProvider> FormEditor<D> {
self.ui_state.ideal_cursor_column = clamped_pos;
}
/// Get cursor position for display (maps raw cursor to display position with mask)
/// Get cursor position for display (maps raw cursor to display position with formatter/mask)
pub fn display_cursor_position(&self) -> usize {
let current_text = self.current_text();
let raw_pos = match self.ui_state.current_mode {
@@ -977,6 +1050,13 @@ impl<D: DataProvider> FormEditor<D> {
{
let field_index = self.ui_state.current_field;
if let Some(cfg) = self.ui_state.validation.get_field_config(field_index) {
// Only apply custom formatter cursor mapping when NOT editing
if !matches!(self.ui_state.current_mode, AppMode::Edit) {
if let Some((formatted, mapper, _warning)) = cfg.run_custom_formatter(current_text) {
return mapper.raw_to_formatted(current_text, &formatted, raw_pos);
}
}
// Fallback to display mask
if let Some(mask) = &cfg.display_mask {
return mask.raw_pos_to_display_pos(self.ui_state.cursor_pos);
}

View File

@@ -37,6 +37,8 @@ pub use validation::{
CharacterLimits, ValidationConfigBuilder, ValidationState,
ValidationSummary, PatternFilters, PositionFilter, PositionRange, CharacterFilter,
DisplayMask, // Simple display mask instead of complex ReservedCharacters
// Feature 4: custom formatting exports
CustomFormatter, FormattingResult, PositionMapper, DefaultPositionMapper,
};
// Theming and GUI

View File

@@ -2,9 +2,12 @@
//! Validation configuration types and builders
use crate::validation::{CharacterLimits, PatternFilters, DisplayMask};
#[cfg(feature = "validation")]
use crate::validation::{CustomFormatter, FormattingResult, PositionMapper};
use std::sync::Arc;
/// Main validation configuration for a field
#[derive(Debug, Clone, Default)]
#[derive(Clone, Default)]
pub struct ValidationConfig {
/// Character limit configuration
pub character_limits: Option<CharacterLimits>,
@@ -15,13 +18,199 @@ pub struct ValidationConfig {
/// User-defined display mask for visual formatting
pub display_mask: Option<DisplayMask>,
/// Future: Custom formatting
pub custom_formatting: Option<()>, // Placeholder for future implementation
/// Optional: user-provided custom formatter (feature 4)
#[cfg(feature = "validation")]
pub custom_formatter: Option<Arc<dyn CustomFormatter + Send + Sync>>,
/// Future: External validation
pub external_validation: Option<()>, // Placeholder for future implementation
}
/// Manual Debug to avoid requiring Debug on dyn CustomFormatter
impl std::fmt::Debug for ValidationConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut ds = f.debug_struct("ValidationConfig");
ds.field("character_limits", &self.character_limits)
.field("pattern_filters", &self.pattern_filters)
.field("display_mask", &self.display_mask)
// Do not print the formatter itself to avoid requiring Debug
.field(
"custom_formatter",
&{
#[cfg(feature = "validation")]
{
if self.custom_formatter.is_some() { &"Some(<CustomFormatter>)" } else { &"None" }
}
#[cfg(not(feature = "validation"))]
{
&"N/A"
}
},
)
.field("external_validation", &self.external_validation)
.finish()
}
}
// ✅ FIXED: Move function from struct definition to impl block
impl ValidationConfig {
/// If a custom formatter is configured, run it and return the formatted text,
/// the position mapper and an optional warning message.
///
/// Returns None when no custom formatter is configured.
#[cfg(feature = "validation")]
pub fn run_custom_formatter(
&self,
raw: &str,
) -> Option<(String, Arc<dyn PositionMapper>, Option<String>)> {
let formatter = self.custom_formatter.as_ref()?;
match formatter.format(raw) {
FormattingResult::Success { formatted, mapper } => {
Some((formatted, mapper, None))
}
FormattingResult::Warning { formatted, message, mapper } => {
Some((formatted, mapper, Some(message)))
}
FormattingResult::Error { .. } => None, // Fall back to raw display
}
}
/// Create a new empty validation configuration
pub fn new() -> Self {
Self::default()
}
/// Create a configuration with just character limits
pub fn with_max_length(max_length: usize) -> Self {
ValidationConfigBuilder::new()
.with_max_length(max_length)
.build()
}
/// Create a configuration with pattern filters
pub fn with_patterns(patterns: PatternFilters) -> Self {
ValidationConfigBuilder::new()
.with_pattern_filters(patterns)
.build()
}
/// Create a configuration with user-defined display mask
///
/// # Examples
/// ```
/// use canvas::{ValidationConfig, DisplayMask};
///
/// let phone_mask = DisplayMask::new("(###) ###-####", '#');
/// let config = ValidationConfig::with_mask(phone_mask);
/// ```
pub fn with_mask(mask: DisplayMask) -> Self {
ValidationConfigBuilder::new()
.with_display_mask(mask)
.build()
}
/// Validate a character insertion at a specific position (raw text space).
///
/// Note: Display masks are visual-only and do not participate in validation.
/// Editor logic is responsible for skipping mask separator positions; here we
/// only validate the raw insertion against limits and patterns.
pub fn validate_char_insertion(
&self,
current_text: &str,
position: usize,
character: char,
) -> ValidationResult {
// Character limits validation
if let Some(ref limits) = self.character_limits {
// ✅ FIXED: Explicit return type annotation
if let Some(result) = limits.validate_insertion(current_text, position, character) {
if !result.is_acceptable() {
return result;
}
}
}
// Pattern filters validation
if let Some(ref patterns) = self.pattern_filters {
// ✅ FIXED: Explicit error handling
if let Err(message) = patterns.validate_char_at_position(position, character) {
return ValidationResult::error(message);
}
}
// Future: Add other validation types here
ValidationResult::Valid
}
/// Validate the current text content (raw text space)
pub fn validate_content(&self, text: &str) -> ValidationResult {
// Character limits validation
if let Some(ref limits) = self.character_limits {
// ✅ FIXED: Explicit return type annotation
if let Some(result) = limits.validate_content(text) {
if !result.is_acceptable() {
return result;
}
}
}
// Pattern filters validation
if let Some(ref patterns) = self.pattern_filters {
// ✅ FIXED: Explicit error handling
if let Err(message) = patterns.validate_text(text) {
return ValidationResult::error(message);
}
}
// Future: Add other validation types here
ValidationResult::Valid
}
/// Check if any validation rules are configured
pub fn has_validation(&self) -> bool {
self.character_limits.is_some()
|| self.pattern_filters.is_some()
|| self.display_mask.is_some()
|| {
#[cfg(feature = "validation")]
{ self.custom_formatter.is_some() }
#[cfg(not(feature = "validation"))]
{ false }
}
}
pub fn allows_field_switch(&self, text: &str) -> bool {
// Character limits validation
if let Some(ref limits) = self.character_limits {
// ✅ FIXED: Direct boolean return
if !limits.allows_field_switch(text) {
return false;
}
}
// Future: Add other validation types here
true
}
/// Get reason why field switching is blocked (if any)
pub fn field_switch_block_reason(&self, text: &str) -> Option<String> {
// Character limits validation
if let Some(ref limits) = self.character_limits {
// ✅ FIXED: Direct option return
if let Some(reason) = limits.field_switch_block_reason(text) {
return Some(reason);
}
}
// Future: Add other validation types here
None
}
}
/// Builder for creating validation configurations
#[derive(Debug, Default)]
pub struct ValidationConfigBuilder {
@@ -47,24 +236,24 @@ impl ValidationConfigBuilder {
}
/// Set user-defined display mask for visual formatting
///
///
/// # Examples
/// ```
/// use canvas::{ValidationConfigBuilder, DisplayMask};
///
///
/// // Phone number with dynamic formatting
/// let phone_mask = DisplayMask::new("(###) ###-####", '#');
/// let config = ValidationConfigBuilder::new()
/// .with_display_mask(phone_mask)
/// .build();
///
/// // Date with template formatting
///
/// // Date with template formatting
/// let date_mask = DisplayMask::new("##/##/####", '#')
/// .with_template('_');
/// let config = ValidationConfigBuilder::new()
/// .with_display_mask(date_mask)
/// .build();
///
///
/// // Custom business format
/// let employee_id = DisplayMask::new("EMP-####-##", '#')
/// .with_template('•');
@@ -78,6 +267,18 @@ impl ValidationConfigBuilder {
self
}
/// Set optional custom formatter (feature 4)
#[cfg(feature = "validation")]
pub fn with_custom_formatter<F>(mut self, formatter: Arc<F>) -> Self
where
F: CustomFormatter + Send + Sync + 'static,
{
self.config.custom_formatter = Some(formatter);
// When custom formatter is present, it takes precedence over display mask.
self.config.display_mask = None;
self
}
/// Set maximum number of characters (convenience method)
pub fn with_max_length(mut self, max_length: usize) -> Self {
self.config.character_limits = Some(CharacterLimits::new(max_length));
@@ -134,131 +335,6 @@ impl ValidationResult {
}
}
impl ValidationConfig {
/// Create a new empty validation configuration
pub fn new() -> Self {
Self::default()
}
/// Create a configuration with just character limits
pub fn with_max_length(max_length: usize) -> Self {
ValidationConfigBuilder::new()
.with_max_length(max_length)
.build()
}
/// Create a configuration with pattern filters
pub fn with_patterns(patterns: PatternFilters) -> Self {
ValidationConfigBuilder::new()
.with_pattern_filters(patterns)
.build()
}
/// Create a configuration with user-defined display mask
///
/// # Examples
/// ```
/// use canvas::{ValidationConfig, DisplayMask};
///
/// let phone_mask = DisplayMask::new("(###) ###-####", '#');
/// let config = ValidationConfig::with_mask(phone_mask);
/// ```
pub fn with_mask(mask: DisplayMask) -> Self {
ValidationConfigBuilder::new()
.with_display_mask(mask)
.build()
}
/// Validate a character insertion at a specific position (raw text space).
///
/// Note: Display masks are visual-only and do not participate in validation.
/// Editor logic is responsible for skipping mask separator positions; here we
/// only validate the raw insertion against limits and patterns.
pub fn validate_char_insertion(
&self,
current_text: &str,
position: usize,
character: char,
) -> ValidationResult {
// Character limits validation
if let Some(ref limits) = self.character_limits {
if let Some(result) = limits.validate_insertion(current_text, position, character) {
if !result.is_acceptable() {
return result;
}
}
}
// Pattern filters validation
if let Some(ref patterns) = self.pattern_filters {
if let Err(message) = patterns.validate_char_at_position(position, character) {
return ValidationResult::error(message);
}
}
// Future: Add other validation types here
ValidationResult::Valid
}
/// Validate the current text content (raw text space)
pub fn validate_content(&self, text: &str) -> ValidationResult {
// Character limits validation
if let Some(ref limits) = self.character_limits {
if let Some(result) = limits.validate_content(text) {
if !result.is_acceptable() {
return result;
}
}
}
// Pattern filters validation
if let Some(ref patterns) = self.pattern_filters {
if let Err(message) = patterns.validate_text(text) {
return ValidationResult::error(message);
}
}
// Future: Add other validation types here
ValidationResult::Valid
}
/// Check if any validation rules are configured
pub fn has_validation(&self) -> bool {
self.character_limits.is_some()
|| self.pattern_filters.is_some()
|| self.display_mask.is_some()
}
pub fn allows_field_switch(&self, text: &str) -> bool {
// Character limits validation
if let Some(ref limits) = self.character_limits {
if !limits.allows_field_switch(text) {
return false;
}
}
// Future: Add other validation types here
true
}
/// Get reason why field switching is blocked (if any)
pub fn field_switch_block_reason(&self, text: &str) -> Option<String> {
// Character limits validation
if let Some(ref limits) = self.character_limits {
if let Some(reason) = limits.field_switch_block_reason(text) {
return Some(reason);
}
}
// Future: Add other validation types here
None
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -268,7 +344,7 @@ mod tests {
// User creates their own phone mask
let phone_mask = DisplayMask::new("(###) ###-####", '#');
let config = ValidationConfig::with_mask(phone_mask);
// has_validation should be true because mask is configured
assert!(config.has_validation());

View File

@@ -0,0 +1,217 @@
/* canvas/src/validation/formatting.rs
Add new formatting module with CustomFormatter, PositionMapper, DefaultPositionMapper, and FormattingResult
*/
use std::sync::Arc;
/// Bidirectional mapping between raw input positions and formatted display positions.
///
/// The library uses this to keep cursor/selection behavior intuitive when the UI
/// shows a formatted transformation (e.g., "01001" -> "010 01") while the editor
/// still stores raw text.
pub trait PositionMapper: Send + Sync {
/// Map a raw cursor position to a formatted cursor position.
///
/// raw_pos is an index into the raw text (0..=raw.len() in char positions).
/// Implementations should return a position within 0..=formatted.len() (in char positions).
fn raw_to_formatted(&self, raw: &str, formatted: &str, raw_pos: usize) -> usize;
/// Map a formatted cursor position to a raw cursor position.
///
/// formatted_pos is an index into the formatted text (0..=formatted.len()).
/// Implementations should return a position within 0..=raw.len() (in char positions).
fn formatted_to_raw(&self, raw: &str, formatted: &str, formatted_pos: usize) -> usize;
}
/// A reasonable default mapper that works for "insert separators" style formatting,
/// such as grouping digits or adding dashes/spaces.
///
/// Heuristic:
/// - Treat letters and digits (is_alphanumeric) in the formatted string as user-entered characters
/// corresponding to raw characters, in order.
/// - Treat any non-alphanumeric characters as purely visual separators.
/// - Raw positions are mapped by counting alphanumeric characters in the formatted string.
/// - If the formatted contains fewer alphanumeric characters than the raw (shouldn't happen
/// for plain grouping), we cap at the end of the formatted string.
#[derive(Clone, Default)]
pub struct DefaultPositionMapper;
impl PositionMapper for DefaultPositionMapper {
fn raw_to_formatted(&self, raw: &str, formatted: &str, raw_pos: usize) -> usize {
// Convert to char indices for correctness in presence of UTF-8
let raw_len = raw.chars().count();
let clamped_raw_pos = raw_pos.min(raw_len);
// Count alphanumerics in formatted, find the index where we've seen `clamped_raw_pos` of them.
let mut seen_user_chars = 0usize;
for (idx, ch) in formatted.char_indices() {
if ch.is_alphanumeric() {
if seen_user_chars == clamped_raw_pos {
// Cursor is positioned before this user character in the formatted view
return idx;
}
seen_user_chars += 1;
}
}
// If we consumed all alphanumeric chars and still haven't reached clamped_raw_pos,
// place cursor at the end of the formatted string.
formatted.len()
}
fn formatted_to_raw(&self, raw: &str, formatted: &str, formatted_pos: usize) -> usize {
let clamped_fmt_pos = formatted_pos.min(formatted.len());
// Count alphanumerics in formatted up to formatted_pos.
let mut seen_user_chars = 0usize;
for (idx, ch) in formatted.char_indices() {
if idx >= clamped_fmt_pos {
break;
}
if ch.is_alphanumeric() {
seen_user_chars += 1;
}
}
// Map to raw position by clamping to raw char count
let raw_len = raw.chars().count();
seen_user_chars.min(raw_len)
}
}
/// Result of invoking a custom formatter on the raw input.
///
/// Success variants carry the formatted string and a position mapper to translate
/// between raw and formatted cursor positions. If you don't provide a custom mapper,
/// the library will fall back to DefaultPositionMapper.
pub enum FormattingResult {
/// Successfully produced a formatted display value and a position mapper.
Success {
formatted: String,
/// Mapper to convert cursor positions between raw and formatted representations.
mapper: Arc<dyn PositionMapper>,
},
/// Successfully produced a formatted value, but with a non-fatal warning message
/// that can be shown in the UI (e.g., "incomplete value").
Warning {
formatted: String,
message: String,
mapper: Arc<dyn PositionMapper>,
},
/// Failed to produce a formatted display. The library will typically fall back to raw.
Error {
message: String,
},
}
impl FormattingResult {
/// Convenience to create a success result using the default mapper.
pub fn success(formatted: impl Into<String>) -> Self {
FormattingResult::Success {
formatted: formatted.into(),
mapper: Arc::new(DefaultPositionMapper::default()),
}
}
/// Convenience to create a warning result using the default mapper.
pub fn warning(formatted: impl Into<String>, message: impl Into<String>) -> Self {
FormattingResult::Warning {
formatted: formatted.into(),
message: message.into(),
mapper: Arc::new(DefaultPositionMapper::default()),
}
}
/// Convenience to create a success result with a custom mapper.
pub fn success_with_mapper(
formatted: impl Into<String>,
mapper: Arc<dyn PositionMapper>,
) -> Self {
FormattingResult::Success {
formatted: formatted.into(),
mapper,
}
}
/// Convenience to create a warning result with a custom mapper.
pub fn warning_with_mapper(
formatted: impl Into<String>,
message: impl Into<String>,
mapper: Arc<dyn PositionMapper>,
) -> Self {
FormattingResult::Warning {
formatted: formatted.into(),
message: message.into(),
mapper,
}
}
/// Convenience to create an error result.
pub fn error(message: impl Into<String>) -> Self {
FormattingResult::Error {
message: message.into(),
}
}
}
/// A user-implemented formatter that turns raw input into a formatted display string,
/// optionally providing a custom cursor position mapper.
///
/// Notes:
/// - The library will keep raw input authoritative for editing and validation.
/// - The formatted value is only used for display.
/// - If formatting fails, return Error; the library will show the raw value.
/// - For common grouping (spaces/dashes), you can return Success/Warning and rely
/// on DefaultPositionMapper, or provide your own mapper for advanced cases
/// (reordering, compression, locale-specific rules, etc.).
pub trait CustomFormatter: Send + Sync {
fn format(&self, raw: &str) -> FormattingResult;
}
#[cfg(test)]
mod tests {
use super::*;
struct GroupEvery3;
impl CustomFormatter for GroupEvery3 {
fn format(&self, raw: &str) -> FormattingResult {
let mut out = String::new();
for (i, ch) in raw.chars().enumerate() {
if i > 0 && i % 3 == 0 {
out.push(' ');
}
out.push(ch);
}
FormattingResult::success(out)
}
}
#[test]
fn default_mapper_roundtrip_basic() {
let mapper = DefaultPositionMapper::default();
let raw = "01001";
let formatted = "010 01";
// raw_to_formatted monotonicity and bounds
for rp in 0..=raw.chars().count() {
let fp = mapper.raw_to_formatted(raw, formatted, rp);
assert!(fp <= formatted.len());
}
// formatted_to_raw bounds
for fp in 0..=formatted.len() {
let rp = mapper.formatted_to_raw(raw, formatted, fp);
assert!(rp <= raw.chars().count());
}
}
#[test]
fn formatter_groups_every_3() {
let f = GroupEvery3;
match f.format("1234567") {
FormattingResult::Success { formatted, .. } => {
assert_eq!(formatted, "123 456 7");
}
_ => panic!("expected success"),
}
}
}

View File

@@ -6,6 +6,7 @@ pub mod limits;
pub mod state;
pub mod patterns;
pub mod mask; // Simple display mask instead of complex reserved chars
pub mod formatting; // Custom formatter and position mapping (feature 4)
// Re-export main types
pub use config::{ValidationConfig, ValidationResult, ValidationConfigBuilder};
@@ -13,6 +14,7 @@ pub use limits::{CharacterLimits, LimitCheckResult};
pub use state::{ValidationState, ValidationSummary};
pub use patterns::{PatternFilters, PositionFilter, PositionRange, CharacterFilter};
pub use mask::DisplayMask; // Simple mask instead of ReservedCharacters
pub use formatting::{CustomFormatter, FormattingResult, PositionMapper, DefaultPositionMapper};
/// Validation error types
#[derive(Debug, Clone, thiserror::Error)]

View File

@@ -2,6 +2,8 @@
//! Validation state management
use crate::validation::{ValidationConfig, ValidationResult};
#[cfg(feature = "validation")]
use crate::validation::{PositionMapper};
use std::collections::HashMap;
/// Validation state for all fields in a form
@@ -121,6 +123,18 @@ impl ValidationState {
pub fn get_field_result(&self, field_index: usize) -> Option<&ValidationResult> {
self.field_results.get(&field_index)
}
/// Get formatted display for a field if a custom formatter is configured.
/// Returns (formatted_text, position_mapper, optional_warning_message).
#[cfg(feature = "validation")]
pub fn formatted_for(
&self,
field_index: usize,
raw: &str,
) -> Option<(String, std::sync::Arc<dyn PositionMapper>, Option<String>)> {
let config = self.field_configs.get(&field_index)?;
config.run_custom_formatter(raw)
}
/// Check if a field has been validated
pub fn is_field_validated(&self, field_index: usize) -> bool {