feature2 implemented bug needs to be addressed
This commit is contained in:
@@ -16,7 +16,7 @@ crossterm = { workspace = true, optional = true }
|
||||
anyhow.workspace = true
|
||||
tokio = { workspace = true, optional = true }
|
||||
toml = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde.workspace = true
|
||||
unicode-width.workspace = true
|
||||
thiserror = { workspace = true }
|
||||
|
||||
|
||||
290
canvas/examples/validation_patterns.rs
Normal file
290
canvas/examples/validation_patterns.rs
Normal file
@@ -0,0 +1,290 @@
|
||||
// examples/validation_patterns.rs
|
||||
//! Example demonstrating position-based pattern filtering
|
||||
//!
|
||||
//! Run with: cargo run --example validation_patterns --features validation
|
||||
|
||||
use canvas::{
|
||||
prelude::*,
|
||||
validation::{ValidationConfigBuilder, PatternFilters, PositionFilter, PositionRange, CharacterFilter},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct DocumentForm {
|
||||
license_plate: String,
|
||||
phone_number: String,
|
||||
credit_card: String,
|
||||
custom_id: String,
|
||||
}
|
||||
|
||||
impl DocumentForm {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
license_plate: String::new(),
|
||||
phone_number: String::new(),
|
||||
credit_card: String::new(),
|
||||
custom_id: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DataProvider for DocumentForm {
|
||||
fn field_count(&self) -> usize {
|
||||
4
|
||||
}
|
||||
|
||||
fn field_name(&self, index: usize) -> &str {
|
||||
match index {
|
||||
0 => "License Plate",
|
||||
1 => "Phone Number",
|
||||
2 => "Credit Card",
|
||||
3 => "Custom ID",
|
||||
_ => "",
|
||||
}
|
||||
}
|
||||
|
||||
fn field_value(&self, index: usize) -> &str {
|
||||
match index {
|
||||
0 => &self.license_plate,
|
||||
1 => &self.phone_number,
|
||||
2 => &self.credit_card,
|
||||
3 => &self.custom_id,
|
||||
_ => "",
|
||||
}
|
||||
}
|
||||
|
||||
fn set_field_value(&mut self, index: usize, value: String) {
|
||||
match index {
|
||||
0 => self.license_plate = value,
|
||||
1 => self.phone_number = value,
|
||||
2 => self.credit_card = value,
|
||||
3 => self.custom_id = value,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn validation_config(&self, field_index: usize) -> Option<ValidationConfig> {
|
||||
match field_index {
|
||||
0 => {
|
||||
// License plate: AB123 (2 letters, 3 numbers) - USER DEFINED
|
||||
let license_plate_pattern = PatternFilters::new()
|
||||
.add_filter(PositionFilter::new(
|
||||
PositionRange::Range(0, 1),
|
||||
CharacterFilter::Alphabetic,
|
||||
))
|
||||
.add_filter(PositionFilter::new(
|
||||
PositionRange::Range(2, 4),
|
||||
CharacterFilter::Numeric,
|
||||
));
|
||||
|
||||
Some(ValidationConfigBuilder::new()
|
||||
.with_pattern_filters(license_plate_pattern)
|
||||
.build())
|
||||
}
|
||||
1 => {
|
||||
// Phone number: 123-456-7890 - USER DEFINED
|
||||
let phone_pattern = PatternFilters::new()
|
||||
.add_filter(PositionFilter::new(
|
||||
PositionRange::Multiple(vec![0,1,2,4,5,6,8,9,10,11]),
|
||||
CharacterFilter::Numeric,
|
||||
))
|
||||
.add_filter(PositionFilter::new(
|
||||
PositionRange::Multiple(vec![3, 7]),
|
||||
CharacterFilter::Exact('-'),
|
||||
));
|
||||
|
||||
Some(ValidationConfigBuilder::new()
|
||||
.with_pattern_filters(phone_pattern)
|
||||
.build())
|
||||
}
|
||||
2 => {
|
||||
// Credit card: 1234-5678-9012-3456 - USER DEFINED
|
||||
let credit_card_pattern = PatternFilters::new()
|
||||
.add_filter(PositionFilter::new(
|
||||
PositionRange::Multiple(vec![0,1,2,3,5,6,7,8,10,11,12,13,15,16,17,18]),
|
||||
CharacterFilter::Numeric,
|
||||
))
|
||||
.add_filter(PositionFilter::new(
|
||||
PositionRange::Multiple(vec![4, 9, 14]),
|
||||
CharacterFilter::Exact('-'),
|
||||
));
|
||||
|
||||
Some(ValidationConfigBuilder::new()
|
||||
.with_pattern_filters(credit_card_pattern)
|
||||
.build())
|
||||
}
|
||||
3 => {
|
||||
// Custom ID: First 2 letters, rest alphanumeric - USER DEFINED
|
||||
let custom_id_pattern = PatternFilters::new()
|
||||
.add_filter(PositionFilter::new(
|
||||
PositionRange::Range(0, 1),
|
||||
CharacterFilter::Alphabetic,
|
||||
))
|
||||
.add_filter(PositionFilter::new(
|
||||
PositionRange::From(2),
|
||||
CharacterFilter::Alphanumeric,
|
||||
));
|
||||
|
||||
Some(ValidationConfigBuilder::new()
|
||||
.with_pattern_filters(custom_id_pattern)
|
||||
.build())
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("🎯 Canvas Pattern Filtering Demo");
|
||||
println!("=================================");
|
||||
println!();
|
||||
|
||||
let form = DocumentForm::new();
|
||||
let mut editor = FormEditor::new(form);
|
||||
|
||||
println!("📋 Form initialized with USER-DEFINED pattern validation rules:");
|
||||
for i in 0..editor.data_provider().field_count() {
|
||||
let field_name = editor.data_provider().field_name(i);
|
||||
println!(" • {}: Position-based pattern filtering (user-defined)", field_name);
|
||||
}
|
||||
println!();
|
||||
|
||||
// Test License Plate (Field 0)
|
||||
println!("1. Testing USER-DEFINED License Plate pattern (AB123 - 2 letters, 3 numbers):");
|
||||
|
||||
// Valid license plate
|
||||
println!(" Entering valid license plate 'AB123':");
|
||||
for ch in "AB123".chars() {
|
||||
match editor.insert_char(ch) {
|
||||
Ok(_) => println!(" '{}' ✓ accepted", ch),
|
||||
Err(e) => println!(" '{}' ✗ rejected: {}", ch, e),
|
||||
}
|
||||
}
|
||||
println!(" Result: '{}'", editor.current_text());
|
||||
println!();
|
||||
|
||||
// Clear and test invalid pattern
|
||||
editor.clear_current_field();
|
||||
println!(" Testing invalid pattern 'A1123':");
|
||||
for (i, ch) in "A1123".chars().enumerate() {
|
||||
match editor.insert_char(ch) {
|
||||
Ok(_) => println!(" Position {}: '{}' ✓ accepted", i, ch),
|
||||
Err(e) => println!(" Position {}: '{}' ✗ rejected: {}", i, ch, e),
|
||||
}
|
||||
}
|
||||
println!(" Result: '{}'", editor.current_text());
|
||||
println!();
|
||||
|
||||
// Move to phone number field
|
||||
editor.move_to_next_field()?;
|
||||
|
||||
// Test Phone Number (Field 1)
|
||||
println!("2. Testing USER-DEFINED Phone Number pattern (123-456-7890):");
|
||||
|
||||
// Valid phone number
|
||||
println!(" Entering valid phone number '123-456-7890':");
|
||||
for (i, ch) in "123-456-7890".chars().enumerate() {
|
||||
match editor.insert_char(ch) {
|
||||
Ok(_) => println!(" Position {}: '{}' ✓ accepted", i, ch),
|
||||
Err(e) => println!(" Position {}: '{}' ✗ rejected: {}", i, ch, e),
|
||||
}
|
||||
}
|
||||
println!(" Result: '{}'", editor.current_text());
|
||||
println!();
|
||||
|
||||
// Move to credit card field
|
||||
editor.move_to_next_field()?;
|
||||
|
||||
// Test Credit Card (Field 2)
|
||||
println!("3. Testing USER-DEFINED Credit Card pattern (1234-5678-9012-3456):");
|
||||
|
||||
// Valid credit card (first few characters)
|
||||
println!(" Entering valid credit card start '1234-56':");
|
||||
for (i, ch) in "1234-56".chars().enumerate() {
|
||||
match editor.insert_char(ch) {
|
||||
Ok(_) => println!(" Position {}: '{}' ✓ accepted", i, ch),
|
||||
Err(e) => println!(" Position {}: '{}' ✗ rejected: {}", i, ch, e),
|
||||
}
|
||||
}
|
||||
println!(" Result: '{}'", editor.current_text());
|
||||
println!();
|
||||
|
||||
// Test invalid character at dash position
|
||||
println!(" Testing invalid character at dash position:");
|
||||
editor.clear_current_field();
|
||||
for (i, ch) in "1234A56".chars().enumerate() {
|
||||
match editor.insert_char(ch) {
|
||||
Ok(_) => println!(" Position {}: '{}' ✓ accepted", i, ch),
|
||||
Err(e) => println!(" Position {}: '{}' ✗ rejected: {}", i, ch, e),
|
||||
}
|
||||
}
|
||||
println!(" Result: '{}'", editor.current_text());
|
||||
println!();
|
||||
|
||||
// Move to custom ID field
|
||||
editor.move_to_next_field()?;
|
||||
|
||||
// Test Custom ID (Field 3)
|
||||
println!("4. Testing USER-DEFINED Custom ID pattern (2 letters + alphanumeric):");
|
||||
|
||||
// Valid custom ID
|
||||
println!(" Entering valid custom ID 'AB123def':");
|
||||
for (i, ch) in "AB123def".chars().enumerate() {
|
||||
match editor.insert_char(ch) {
|
||||
Ok(_) => println!(" Position {}: '{}' ✓ accepted", i, ch),
|
||||
Err(e) => println!(" Position {}: '{}' ✗ rejected: {}", i, ch, e),
|
||||
}
|
||||
}
|
||||
println!(" Result: '{}'", editor.current_text());
|
||||
println!();
|
||||
|
||||
// Test invalid pattern
|
||||
editor.clear_current_field();
|
||||
println!(" Testing invalid pattern '1B123def' (number in first position):");
|
||||
for (i, ch) in "1B123def".chars().enumerate() {
|
||||
match editor.insert_char(ch) {
|
||||
Ok(_) => println!(" Position {}: '{}' ✓ accepted", i, ch),
|
||||
Err(e) => println!(" Position {}: '{}' ✗ rejected: {}", i, ch, e),
|
||||
}
|
||||
}
|
||||
println!(" Result: '{}'", editor.current_text());
|
||||
println!();
|
||||
|
||||
// Show validation summary
|
||||
println!("📊 Final validation summary:");
|
||||
let summary = editor.validation_summary();
|
||||
println!(" Total fields with validation: {}", summary.total_fields);
|
||||
println!(" Validated fields: {}", summary.validated_fields);
|
||||
println!(" Valid fields: {}", summary.valid_fields);
|
||||
println!(" Fields with warnings: {}", summary.warning_fields);
|
||||
println!(" Fields with errors: {}", summary.error_fields);
|
||||
println!();
|
||||
|
||||
// Show field-by-field status
|
||||
println!("📝 Field-by-field validation status:");
|
||||
for i in 0..editor.data_provider().field_count() {
|
||||
let field_name = editor.data_provider().field_name(i);
|
||||
let field_value = editor.data_provider().field_value(i);
|
||||
|
||||
if let Some(result) = editor.field_validation(i) {
|
||||
println!(" {} [{}]: {} - {:?}",
|
||||
field_name,
|
||||
field_value,
|
||||
if result.is_acceptable() { "✓" } else { "✗" },
|
||||
result
|
||||
);
|
||||
} else {
|
||||
println!(" {} [{}]: (not validated)", field_name, field_value);
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("✨ USER-DEFINED Pattern filtering demo completed!");
|
||||
println!("Key Features Demonstrated:");
|
||||
println!(" • Position-specific character filtering (USER DEFINES PATTERNS)");
|
||||
println!(" • Library provides CharacterFilter: Alphabetic, Numeric, Alphanumeric, Exact, OneOf, Custom");
|
||||
println!(" • User defines all patterns using library's building blocks");
|
||||
println!(" • Real-time validation during typing");
|
||||
println!(" • Flexible position ranges (single, range, from, multiple)");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -35,7 +35,7 @@ pub use canvas::actions::{CanvasAction, ActionResult};
|
||||
pub use validation::{
|
||||
ValidationConfig, ValidationResult, ValidationError,
|
||||
CharacterLimits, ValidationConfigBuilder, ValidationState,
|
||||
ValidationSummary,
|
||||
ValidationSummary, PatternFilters, PositionFilter, PositionRange, CharacterFilter,
|
||||
};
|
||||
|
||||
// Theming and GUI
|
||||
|
||||
@@ -1,29 +1,24 @@
|
||||
// src/validation/config.rs
|
||||
//! Validation configuration types and builders
|
||||
|
||||
use crate::validation::CharacterLimits;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::validation::{CharacterLimits, PatternFilters};
|
||||
|
||||
/// Main validation configuration for a field
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ValidationConfig {
|
||||
/// Character limit configuration
|
||||
pub character_limits: Option<CharacterLimits>,
|
||||
|
||||
/// Future: Predefined patterns
|
||||
#[serde(skip)]
|
||||
pub patterns: Option<()>, // Placeholder for future implementation
|
||||
/// Pattern filtering configuration
|
||||
pub pattern_filters: Option<PatternFilters>,
|
||||
|
||||
/// Future: Reserved characters
|
||||
#[serde(skip)]
|
||||
pub reserved_chars: Option<()>, // Placeholder for future implementation
|
||||
|
||||
/// Future: Custom formatting
|
||||
#[serde(skip)]
|
||||
pub custom_formatting: Option<()>, // Placeholder for future implementation
|
||||
|
||||
/// Future: External validation
|
||||
#[serde(skip)]
|
||||
pub external_validation: Option<()>, // Placeholder for future implementation
|
||||
}
|
||||
|
||||
@@ -45,6 +40,12 @@ impl ValidationConfigBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set pattern filters for the field
|
||||
pub fn with_pattern_filters(mut self, filters: PatternFilters) -> Self {
|
||||
self.config.pattern_filters = Some(filters);
|
||||
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));
|
||||
@@ -114,6 +115,13 @@ impl ValidationConfig {
|
||||
.build()
|
||||
}
|
||||
|
||||
/// Create a configuration with pattern filters
|
||||
pub fn with_patterns(patterns: PatternFilters) -> Self {
|
||||
ValidationConfigBuilder::new()
|
||||
.with_pattern_filters(patterns)
|
||||
.build()
|
||||
}
|
||||
|
||||
/// Validate a character insertion at a specific position
|
||||
pub fn validate_char_insertion(
|
||||
&self,
|
||||
@@ -130,6 +138,13 @@ impl ValidationConfig {
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -146,6 +161,13 @@ impl ValidationConfig {
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -153,8 +175,7 @@ impl ValidationConfig {
|
||||
|
||||
/// Check if any validation rules are configured
|
||||
pub fn has_validation(&self) -> bool {
|
||||
self.character_limits.is_some()
|
||||
// || self.patterns.is_some()
|
||||
self.character_limits.is_some() || self.pattern_filters.is_some()
|
||||
// || self.reserved_chars.is_some()
|
||||
// || self.custom_formatting.is_some()
|
||||
// || self.external_validation.is_some()
|
||||
@@ -232,4 +253,48 @@ mod tests {
|
||||
let result = config.validate_char_insertion("tests", 5, 'x');
|
||||
assert!(!result.is_acceptable());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_with_patterns() {
|
||||
use crate::validation::{PatternFilters, PositionFilter, PositionRange, CharacterFilter};
|
||||
|
||||
let patterns = PatternFilters::new()
|
||||
.add_filter(PositionFilter::new(
|
||||
PositionRange::Range(0, 1),
|
||||
CharacterFilter::Alphabetic,
|
||||
));
|
||||
|
||||
let config = ValidationConfig::with_patterns(patterns);
|
||||
assert!(config.has_validation());
|
||||
|
||||
// Test valid pattern insertion
|
||||
let result = config.validate_char_insertion("", 0, 'A');
|
||||
assert!(result.is_acceptable());
|
||||
|
||||
// Test invalid pattern insertion
|
||||
let result = config.validate_char_insertion("", 0, '1');
|
||||
assert!(!result.is_acceptable());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_builder_with_patterns() {
|
||||
use crate::validation::{PatternFilters, PositionFilter, PositionRange, CharacterFilter};
|
||||
|
||||
let patterns = PatternFilters::license_plate();
|
||||
let config = ValidationConfigBuilder::new()
|
||||
.with_pattern_filters(patterns)
|
||||
.with_max_length(5)
|
||||
.build();
|
||||
|
||||
assert!(config.has_validation());
|
||||
assert!(config.character_limits.is_some());
|
||||
assert!(config.pattern_filters.is_some());
|
||||
|
||||
// Test pattern validation
|
||||
let result = config.validate_content("AB123");
|
||||
assert!(result.is_acceptable());
|
||||
|
||||
let result = config.validate_content("A1123");
|
||||
assert!(!result.is_acceptable());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
// src/validation/mod.rs
|
||||
//! Validation module for canvas form fields
|
||||
|
||||
pub mod config;
|
||||
pub mod limits;
|
||||
pub mod state;
|
||||
pub mod patterns;
|
||||
|
||||
// Re-export main types
|
||||
pub use config::{ValidationConfig, ValidationResult, ValidationConfigBuilder};
|
||||
pub use limits::{CharacterLimits, LimitCheckResult};
|
||||
pub use state::{ValidationState, ValidationSummary};
|
||||
pub use patterns::{PatternFilters, PositionFilter, PositionRange, CharacterFilter};
|
||||
|
||||
/// Validation error types
|
||||
#[derive(Debug, Clone, thiserror::Error)]
|
||||
@@ -18,6 +21,9 @@ pub enum ValidationError {
|
||||
#[error("Invalid character '{char}' at position {position}")]
|
||||
InvalidCharacter { char: char, position: usize },
|
||||
|
||||
#[error("Pattern validation failed: {message}")]
|
||||
PatternValidationFailed { message: String },
|
||||
|
||||
#[error("Validation configuration error: {message}")]
|
||||
ConfigurationError { message: String },
|
||||
}
|
||||
|
||||
299
canvas/src/validation/patterns.rs
Normal file
299
canvas/src/validation/patterns.rs
Normal file
@@ -0,0 +1,299 @@
|
||||
// src/validation/patterns.rs
|
||||
//! Position-based pattern filtering for validation
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A filter that applies to specific character positions in a field
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PositionFilter {
|
||||
/// Which positions this filter applies to
|
||||
pub positions: PositionRange,
|
||||
/// What type of character filter to apply
|
||||
pub filter: CharacterFilter,
|
||||
}
|
||||
|
||||
/// Defines which character positions a filter applies to
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum PositionRange {
|
||||
/// Single position (e.g., position 3 only)
|
||||
Single(usize),
|
||||
/// Range of positions (e.g., positions 0-2, inclusive)
|
||||
Range(usize, usize),
|
||||
/// From position onwards (e.g., position 4 and beyond)
|
||||
From(usize),
|
||||
/// Multiple specific positions (e.g., positions 0, 2, 5)
|
||||
Multiple(Vec<usize>),
|
||||
}
|
||||
|
||||
/// Types of character filters that can be applied
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum CharacterFilter {
|
||||
/// Allow only alphabetic characters (a-z, A-Z)
|
||||
Alphabetic,
|
||||
/// Allow only numeric characters (0-9)
|
||||
Numeric,
|
||||
/// Allow alphanumeric characters (a-z, A-Z, 0-9)
|
||||
Alphanumeric,
|
||||
/// Allow only exact character match
|
||||
Exact(char),
|
||||
/// Allow any character from the provided set
|
||||
OneOf(Vec<char>),
|
||||
/// Custom user-defined filter function
|
||||
Custom(Box<dyn Fn(char) -> bool + Send + Sync>),
|
||||
}
|
||||
|
||||
impl PositionRange {
|
||||
/// Check if a position is included in this range
|
||||
pub fn contains(&self, position: usize) -> bool {
|
||||
match self {
|
||||
PositionRange::Single(pos) => position == *pos,
|
||||
PositionRange::Range(start, end) => position >= *start && position <= *end,
|
||||
PositionRange::From(start) => position >= *start,
|
||||
PositionRange::Multiple(positions) => positions.contains(&position),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all positions up to a given length that this range covers
|
||||
pub fn positions_up_to(&self, max_length: usize) -> Vec<usize> {
|
||||
match self {
|
||||
PositionRange::Single(pos) => {
|
||||
if *pos < max_length { vec![*pos] } else { vec![] }
|
||||
},
|
||||
PositionRange::Range(start, end) => {
|
||||
let actual_end = (*end).min(max_length.saturating_sub(1));
|
||||
if *start <= actual_end {
|
||||
(*start..=actual_end).collect()
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
},
|
||||
PositionRange::From(start) => {
|
||||
if *start < max_length {
|
||||
(*start..max_length).collect()
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
},
|
||||
PositionRange::Multiple(positions) => {
|
||||
positions.iter()
|
||||
.filter(|&&pos| pos < max_length)
|
||||
.copied()
|
||||
.collect()
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CharacterFilter {
|
||||
/// Test if a character passes this filter
|
||||
pub fn accepts(&self, ch: char) -> bool {
|
||||
match self {
|
||||
CharacterFilter::Alphabetic => ch.is_alphabetic(),
|
||||
CharacterFilter::Numeric => ch.is_numeric(),
|
||||
CharacterFilter::Alphanumeric => ch.is_alphanumeric(),
|
||||
CharacterFilter::Exact(expected) => ch == *expected,
|
||||
CharacterFilter::OneOf(chars) => chars.contains(&ch),
|
||||
CharacterFilter::Custom(func) => func(ch),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a human-readable description of this filter
|
||||
pub fn description(&self) -> String {
|
||||
match self {
|
||||
CharacterFilter::Alphabetic => "alphabetic characters (a-z, A-Z)".to_string(),
|
||||
CharacterFilter::Numeric => "numeric characters (0-9)".to_string(),
|
||||
CharacterFilter::Alphanumeric => "alphanumeric characters (a-z, A-Z, 0-9)".to_string(),
|
||||
CharacterFilter::Exact(ch) => format!("exactly '{}'", ch),
|
||||
CharacterFilter::OneOf(chars) => {
|
||||
let char_list: String = chars.iter().collect();
|
||||
format!("one of: {}", char_list)
|
||||
},
|
||||
CharacterFilter::Custom(_) => "custom filter".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PositionFilter {
|
||||
/// Create a new position filter
|
||||
pub fn new(positions: PositionRange, filter: CharacterFilter) -> Self {
|
||||
Self { positions, filter }
|
||||
}
|
||||
|
||||
/// Validate a character at a specific position
|
||||
pub fn validate_position(&self, position: usize, character: char) -> bool {
|
||||
if self.positions.contains(position) {
|
||||
self.filter.accepts(character)
|
||||
} else {
|
||||
true // Position not covered by this filter, allow any character
|
||||
}
|
||||
}
|
||||
|
||||
/// Get error message for invalid character at position
|
||||
pub fn error_message(&self, position: usize, character: char) -> Option<String> {
|
||||
if self.positions.contains(position) && !self.filter.accepts(character) {
|
||||
Some(format!(
|
||||
"Position {} requires {} but got '{}'",
|
||||
position,
|
||||
self.filter.description(),
|
||||
character
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A collection of position filters for a field
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct PatternFilters {
|
||||
filters: Vec<PositionFilter>,
|
||||
}
|
||||
|
||||
impl PatternFilters {
|
||||
/// Create empty pattern filters
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Add a position filter
|
||||
pub fn add_filter(mut self, filter: PositionFilter) -> Self {
|
||||
self.filters.push(filter);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add multiple filters
|
||||
pub fn add_filters(mut self, filters: Vec<PositionFilter>) -> Self {
|
||||
self.filters.extend(filters);
|
||||
self
|
||||
}
|
||||
|
||||
/// Validate a character at a specific position against all applicable filters
|
||||
pub fn validate_char_at_position(&self, position: usize, character: char) -> Result<(), String> {
|
||||
for filter in &self.filters {
|
||||
if let Some(error) = filter.error_message(position, character) {
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Validate entire text against all filters
|
||||
pub fn validate_text(&self, text: &str) -> Result<(), String> {
|
||||
for (position, character) in text.char_indices() {
|
||||
if let Err(error) = self.validate_char_at_position(position, character) {
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if any filters are configured
|
||||
pub fn has_filters(&self) -> bool {
|
||||
!self.filters.is_empty()
|
||||
}
|
||||
|
||||
/// Get all configured filters
|
||||
pub fn filters(&self) -> &[PositionFilter] {
|
||||
&self.filters
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_position_range_contains() {
|
||||
assert!(PositionRange::Single(3).contains(3));
|
||||
assert!(!PositionRange::Single(3).contains(2));
|
||||
|
||||
assert!(PositionRange::Range(1, 4).contains(3));
|
||||
assert!(!PositionRange::Range(1, 4).contains(5));
|
||||
|
||||
assert!(PositionRange::From(2).contains(5));
|
||||
assert!(!PositionRange::From(2).contains(1));
|
||||
|
||||
assert!(PositionRange::Multiple(vec![0, 2, 5]).contains(2));
|
||||
assert!(!PositionRange::Multiple(vec![0, 2, 5]).contains(3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_position_range_positions_up_to() {
|
||||
assert_eq!(PositionRange::Single(3).positions_up_to(5), vec![3]);
|
||||
assert_eq!(PositionRange::Single(5).positions_up_to(3), vec![]);
|
||||
|
||||
assert_eq!(PositionRange::Range(1, 3).positions_up_to(5), vec![1, 2, 3]);
|
||||
assert_eq!(PositionRange::Range(1, 5).positions_up_to(3), vec![1, 2]);
|
||||
|
||||
assert_eq!(PositionRange::From(2).positions_up_to(5), vec![2, 3, 4]);
|
||||
|
||||
assert_eq!(PositionRange::Multiple(vec![0, 2, 5]).positions_up_to(4), vec![0, 2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_character_filter_accepts() {
|
||||
assert!(CharacterFilter::Alphabetic.accepts('a'));
|
||||
assert!(CharacterFilter::Alphabetic.accepts('Z'));
|
||||
assert!(!CharacterFilter::Alphabetic.accepts('1'));
|
||||
|
||||
assert!(CharacterFilter::Numeric.accepts('5'));
|
||||
assert!(!CharacterFilter::Numeric.accepts('a'));
|
||||
|
||||
assert!(CharacterFilter::Alphanumeric.accepts('a'));
|
||||
assert!(CharacterFilter::Alphanumeric.accepts('5'));
|
||||
assert!(!CharacterFilter::Alphanumeric.accepts('-'));
|
||||
|
||||
assert!(CharacterFilter::Exact('x').accepts('x'));
|
||||
assert!(!CharacterFilter::Exact('x').accepts('y'));
|
||||
|
||||
assert!(CharacterFilter::OneOf(vec!['a', 'b', 'c']).accepts('b'));
|
||||
assert!(!CharacterFilter::OneOf(vec!['a', 'b', 'c']).accepts('d'));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_position_filter_validation() {
|
||||
let filter = PositionFilter::new(
|
||||
PositionRange::Range(0, 1),
|
||||
CharacterFilter::Alphabetic,
|
||||
);
|
||||
|
||||
assert!(filter.validate_position(0, 'A'));
|
||||
assert!(filter.validate_position(1, 'b'));
|
||||
assert!(!filter.validate_position(0, '1'));
|
||||
assert!(filter.validate_position(2, '1')); // Position 2 not covered, allow anything
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pattern_filters_validation() {
|
||||
let patterns = PatternFilters::new()
|
||||
.add_filter(PositionFilter::new(
|
||||
PositionRange::Range(0, 1),
|
||||
CharacterFilter::Alphabetic,
|
||||
))
|
||||
.add_filter(PositionFilter::new(
|
||||
PositionRange::Range(2, 4),
|
||||
CharacterFilter::Numeric,
|
||||
));
|
||||
|
||||
// Valid pattern: AB123
|
||||
assert!(patterns.validate_text("AB123").is_ok());
|
||||
|
||||
// Invalid: number in alphabetic position
|
||||
assert!(patterns.validate_text("A1123").is_err());
|
||||
|
||||
// Invalid: letter in numeric position
|
||||
assert!(patterns.validate_text("AB1A3").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_custom_filter() {
|
||||
let pattern = PatternFilters::new()
|
||||
.add_filter(PositionFilter::new(
|
||||
PositionRange::From(0),
|
||||
CharacterFilter::Custom(Box::new(|c| c.is_lowercase())),
|
||||
));
|
||||
|
||||
assert!(pattern.validate_text("hello").is_ok());
|
||||
assert!(pattern.validate_text("Hello").is_err()); // Uppercase not allowed
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user