use crate::core::types::FieldValue;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
pub type FieldValidator = Box<dyn Fn(&FieldValue) -> Result<(), String> + Send + Sync>;
#[derive(Debug, Clone, PartialEq)]
pub struct ValidationErrors {
pub field_errors: HashMap<String, Vec<String>>,
pub form_errors: Vec<String>,
}
impl Default for ValidationErrors {
fn default() -> Self {
Self::new()
}
}
impl ValidationErrors {
pub fn new() -> Self {
Self {
field_errors: HashMap::new(),
form_errors: Vec::new(),
}
}
pub fn add_field_error(&mut self, field_name: &str, error: String) {
self.field_errors
.entry(field_name.to_string())
.or_default()
.push(error);
}
pub fn add_form_error(&mut self, error: String) {
self.form_errors.push(error);
}
pub fn is_empty(&self) -> bool {
self.field_errors.is_empty() && self.form_errors.is_empty()
}
pub fn has_errors(&self) -> bool {
!self.is_empty()
}
pub fn has_field_error(&self, field: &str) -> bool {
self.field_errors.contains_key(field)
}
pub fn get_field_error(&self, field: &str) -> Option<&Vec<String>> {
self.field_errors.get(field)
}
pub fn clear_field(&mut self, field: &str) {
self.field_errors.remove(field);
}
pub fn remove_field_error(&mut self, field: &str) {
self.field_errors.remove(field);
}
pub fn to_field_errors(&self) -> Vec<crate::error::FieldError> {
let mut errors = Vec::new();
for (field_name, field_errors) in &self.field_errors {
for error_msg in field_errors {
errors.push(crate::error::FieldError::new(
field_name.clone(),
error_msg.clone(),
));
}
}
errors
}
pub fn merge(&mut self, other: ValidationErrors) {
for (field, errors) in other.field_errors {
self.field_errors.entry(field).or_default().extend(errors);
}
self.form_errors.extend(other.form_errors);
}
}
impl std::fmt::Display for ValidationErrors {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if !self.form_errors.is_empty() {
writeln!(f, "Form errors:")?;
for error in &self.form_errors {
writeln!(f, " - {}", error)?;
}
}
if !self.field_errors.is_empty() {
writeln!(f, "Field errors:")?;
for (field, errors) in &self.field_errors {
writeln!(f, " {}:", field)?;
for error in errors {
writeln!(f, " - {}", error)?;
}
}
}
Ok(())
}
}
impl std::error::Error for ValidationErrors {}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum Validator {
Required,
Email,
Url,
MinLength(usize),
MaxLength(usize),
Min(f64),
Max(f64),
Pattern(String),
Range(f64, f64),
Custom(String),
}
pub struct ValidationRuleEngine {
validators: HashMap<String, FieldValidator>,
}
impl Default for ValidationRuleEngine {
fn default() -> Self {
Self::new()
}
}
impl ValidationRuleEngine {
pub fn new() -> Self {
let mut engine = Self {
validators: HashMap::new(),
};
engine.register_builtin_validators();
engine
}
fn register_builtin_validators(&mut self) {
self.validators.insert(
"required".to_string(),
Box::new(|value| match value {
FieldValue::String(s) if s.trim().is_empty() => {
Err("Field is required".to_string())
}
FieldValue::Array(arr) if arr.is_empty() => Err("Field is required".to_string()),
FieldValue::Number(n) if *n == 0.0 => Err("Field is required".to_string()),
_ => Ok(()),
}),
);
self.validators.insert(
"email".to_string(),
Box::new(|value| {
if let FieldValue::String(email) = value {
let email_regex =
Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$").unwrap();
if email_regex.is_match(email) {
Ok(())
} else {
Err("Invalid email format".to_string())
}
} else {
Err("Expected string value for email".to_string())
}
}),
);
self.validators.insert(
"url".to_string(),
Box::new(|value| {
if let FieldValue::String(url) = value {
if url.starts_with("http://") || url.starts_with("https://") {
Ok(())
} else {
Err("Invalid URL format".to_string())
}
} else {
Err("Expected string value for URL".to_string())
}
}),
);
self.validators.insert(
"min_length".to_string(),
Box::new(|value| {
if let FieldValue::String(s) = value {
let min_len = 8; if s.len() >= min_len {
Ok(())
} else {
Err(format!("Minimum length is {} characters", min_len))
}
} else {
Err("Expected string value for length validation".to_string())
}
}),
);
self.validators.insert(
"pattern".to_string(),
Box::new(|value| {
if let FieldValue::String(s) = value {
let has_lowercase = s.chars().any(|c| c.is_ascii_lowercase());
let has_uppercase = s.chars().any(|c| c.is_ascii_uppercase());
let has_digit = s.chars().any(|c| c.is_ascii_digit());
if has_lowercase && has_uppercase && has_digit {
Ok(())
} else {
Err("Pattern validation failed".to_string())
}
} else {
Err("Expected string value for pattern validation".to_string())
}
}),
);
self.validators.insert(
"range".to_string(),
Box::new(|value| {
if let FieldValue::Number(n) = value {
let min = 18.0;
let max = 120.0;
if *n >= min && *n <= max {
Ok(())
} else {
Err(format!("Value must be between {} and {}", min, max))
}
} else {
Err("Expected number value for range validation".to_string())
}
}),
);
self.validators.insert(
"business_email".to_string(),
Box::new(|value| {
if let FieldValue::String(email) = value {
if email.contains("@company.com") || email.contains("@business.com") {
Ok(())
} else {
Err(
"Business email required (must contain @company.com or @business.com)"
.to_string(),
)
}
} else {
Err("Expected string value for email".to_string())
}
}),
);
self.validators.insert(
"strong_password".to_string(),
Box::new(|value| {
if let FieldValue::String(password) = value {
let has_uppercase = password.chars().any(|c| c.is_uppercase());
let has_lowercase = password.chars().any(|c| c.is_lowercase());
let has_digit = password.chars().any(|c| c.is_numeric());
let has_special = password.chars().any(|c| "!@#$%^&*()_+-=[]{}|;:,.<>?".contains(c));
if has_uppercase && has_lowercase && has_digit && has_special {
Ok(())
} else {
Err("Password must contain uppercase, lowercase, digit, and special character".to_string())
}
} else {
Err("Expected string value for password".to_string())
}
}),
);
self.validators.insert(
"adult_age".to_string(),
Box::new(|value| {
if let FieldValue::Number(age) = value {
if *age >= 18.0 {
Ok(())
} else {
Err("Must be at least 18 years old".to_string())
}
} else {
Err("Expected number value for age".to_string())
}
}),
);
self.validators.insert(
"secure_url".to_string(),
Box::new(|value| {
if let FieldValue::String(url) = value {
if url.starts_with("https://") {
Ok(())
} else {
Err("Secure URL required (must start with https://)".to_string())
}
} else {
Err("Expected string value for URL".to_string())
}
}),
);
self.validators.insert(
"luhn_algorithm".to_string(),
Box::new(|value| {
if let FieldValue::String(card_number) = value {
if Self::luhn_check(card_number) {
Ok(())
} else {
Err("Invalid credit card number".to_string())
}
} else {
Err("Expected string value for credit card".to_string())
}
}),
);
self.validators.insert(
"unique_value".to_string(),
Box::new(|value| {
if let FieldValue::String(s) = value {
if s == "unique_value" {
Ok(())
} else {
Err("Value must be unique".to_string())
}
} else {
Err("Expected string value for uniqueness check".to_string())
}
}),
);
}
fn luhn_check(card_number: &str) -> bool {
let digits: Vec<u32> = card_number.chars().filter_map(|c| c.to_digit(10)).collect();
if digits.len() < 2 {
return false;
}
let mut sum = 0;
let mut double = false;
for &digit in digits.iter().rev() {
if double {
let doubled = digit * 2;
sum += if doubled > 9 { doubled - 9 } else { doubled };
} else {
sum += digit;
}
double = !double;
}
sum % 10 == 0
}
pub fn register_validator(&mut self, name: &str, validator: FieldValidator) {
self.validators.insert(name.to_string(), validator);
}
pub fn add_validator(&mut self, _validator: Validator) {
}
pub fn validate_value(&self, value: FieldValue) -> Result<(), ValidationErrors> {
let mut errors = ValidationErrors::new();
if let FieldValue::String(s) = &value {
if s.is_empty() {
errors.add_field_error("", "Value is required".to_string());
}
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
pub fn validate_field(
&self,
_field_name: &str,
value: &FieldValue,
validators: &[Validator],
) -> Vec<String> {
let mut errors = Vec::new();
for validator in validators {
match validator {
Validator::Required => {
if let Some(validator_fn) = self.validators.get("required") {
if let Err(error) = validator_fn(value) {
errors.push(error);
}
}
}
Validator::Email => {
if let Some(validator_fn) = self.validators.get("email") {
if let Err(error) = validator_fn(value) {
errors.push(error);
}
}
}
Validator::Url => {
if let Some(validator_fn) = self.validators.get("url") {
if let Err(error) = validator_fn(value) {
errors.push(error);
}
}
}
Validator::MinLength(min_len) => {
if let FieldValue::String(s) = value {
if s.len() < *min_len {
errors.push(format!("Minimum length is {} characters", min_len));
}
}
}
Validator::MaxLength(max_len) => {
if let FieldValue::String(s) = value {
if s.len() > *max_len {
errors.push(format!("Maximum length is {} characters", max_len));
}
}
}
Validator::Pattern(pattern) => {
if let FieldValue::String(s) = value {
if let Ok(regex) = Regex::new(pattern) {
if !regex.is_match(s) {
errors.push("Pattern validation failed".to_string());
}
} else {
errors.push("Invalid pattern".to_string());
}
}
}
Validator::Range(min, max) => {
if let FieldValue::Number(n) = value {
if *n < *min || *n > *max {
errors.push(format!("Value must be between {} and {}", min, max));
}
}
}
Validator::Min(min_val) => {
if let FieldValue::Number(n) = value {
if *n < *min_val {
errors.push(format!("Value must be at least {}", min_val));
}
}
}
Validator::Max(max_val) => {
if let FieldValue::Number(n) = value {
if *n > *max_val {
errors.push(format!("Value must be at most {}", max_val));
}
}
}
Validator::Custom(name) => {
if let Some(validator_fn) = self.validators.get(name) {
if let Err(error) = validator_fn(value) {
errors.push(error);
}
}
}
}
}
errors
}
}
pub fn validate_form<T: crate::core::Form>(form: &T) -> Result<(), ValidationErrors> {
let engine = ValidationRuleEngine::new();
let mut errors = ValidationErrors::new();
let metadata = T::field_metadata();
let form_data = form.get_form_data();
for field_meta in metadata {
let field_name = &field_meta.name;
let default_value = FieldValue::String(String::new());
let field_value = form_data.get(field_name).unwrap_or(&default_value);
let field_errors = engine.validate_field(field_name, field_value, &field_meta.validators);
if !field_errors.is_empty() {
for error in field_errors {
errors.add_field_error(field_name, error);
}
}
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
pub struct Validators;
impl Validators {
pub fn required(value: &FieldValue) -> Result<(), String> {
match value {
FieldValue::String(s) if s.trim().is_empty() => {
Err("This field is required".to_string())
}
FieldValue::Null => Err("This field is required".to_string()),
FieldValue::Array(arr) if arr.is_empty() => Err("This field is required".to_string()),
_ => Ok(()),
}
}
pub fn email(value: &FieldValue) -> Result<(), String> {
if let FieldValue::String(email) = value {
let email_regex = Regex::new(r"^[^\s@]+@[^\s@]+\.[^\s@]+$").unwrap();
if email_regex.is_match(email) {
Ok(())
} else {
Err("Invalid email format".to_string())
}
} else {
Err("Email must be a string".to_string())
}
}
pub fn url(value: &FieldValue) -> Result<(), String> {
if let FieldValue::String(url) = value {
let url_regex = Regex::new(r"^https?://[^\s/$.?#].[^\s]*$").unwrap();
if url_regex.is_match(url) {
Ok(())
} else {
Err("Invalid URL format".to_string())
}
} else {
Err("URL must be a string".to_string())
}
}
pub fn min_length(value: &FieldValue, min: usize) -> Result<(), String> {
if let FieldValue::String(s) = value {
if s.len() >= min {
Ok(())
} else {
Err(format!("Minimum length is {} characters", min))
}
} else {
Err("Value must be a string".to_string())
}
}
pub fn max_length(value: &FieldValue, max: usize) -> Result<(), String> {
if let FieldValue::String(s) = value {
if s.len() <= max {
Ok(())
} else {
Err(format!("Maximum length is {} characters", max))
}
} else {
Err("Value must be a string".to_string())
}
}
pub fn min(value: &FieldValue, min: f64) -> Result<(), String> {
if let Some(num) = value.as_number() {
if num >= min {
Ok(())
} else {
Err(format!("Minimum value is {}", min))
}
} else {
Err("Value must be a number".to_string())
}
}
pub fn max(value: &FieldValue, max: f64) -> Result<(), String> {
if let Some(num) = value.as_number() {
if num <= max {
Ok(())
} else {
Err(format!("Maximum value is {}", max))
}
} else {
Err("Value must be a number".to_string())
}
}
pub fn pattern(value: &FieldValue, pattern: &str) -> Result<(), String> {
if let FieldValue::String(s) = value {
let regex = Regex::new(pattern).map_err(|_| "Invalid pattern".to_string())?;
if regex.is_match(s) {
Ok(())
} else {
Err("Value doesn't match required pattern".to_string())
}
} else {
Err("Value must be a string".to_string())
}
}
pub fn phone(value: &FieldValue) -> Result<(), String> {
if let FieldValue::String(phone) = value {
let phone_regex = Regex::new(r"^[\+]?[1-9][\d]{0,15}$").unwrap();
if phone_regex.is_match(phone) {
Ok(())
} else {
Err("Invalid phone number format".to_string())
}
} else {
Err("Phone number must be a string".to_string())
}
}
pub fn postal_code(value: &FieldValue) -> Result<(), String> {
if let FieldValue::String(code) = value {
let postal_regex = Regex::new(r"^\d{5}(-\d{4})?$").unwrap();
if postal_regex.is_match(code) {
Ok(())
} else {
Err("Invalid postal code format".to_string())
}
} else {
Err("Postal code must be a string".to_string())
}
}
pub fn credit_card(value: &FieldValue) -> Result<(), String> {
if let FieldValue::String(card) = value {
let digits: Vec<u32> = card
.chars()
.filter(|c| c.is_ascii_digit())
.map(|c| c.to_digit(10).unwrap())
.collect();
if digits.len() < 13 || digits.len() > 19 {
return Err("Invalid credit card number length".to_string());
}
let mut sum = 0;
let mut double = false;
for &digit in digits.iter().rev() {
if double {
let doubled = digit * 2;
sum += if doubled > 9 { doubled - 9 } else { doubled };
} else {
sum += digit;
}
double = !double;
}
if sum % 10 == 0 {
Ok(())
} else {
Err("Invalid credit card number".to_string())
}
} else {
Err("Credit card number must be a string".to_string())
}
}
pub fn date(value: &FieldValue) -> Result<(), String> {
if let FieldValue::Date(_) = value {
Ok(())
} else if let FieldValue::String(date_str) = value {
let date_regex = Regex::new(r"^\d{4}-\d{2}-\d{2}$").unwrap();
if date_regex.is_match(date_str) {
Ok(())
} else {
Err("Invalid date format (YYYY-MM-DD)".to_string())
}
} else {
Err("Value must be a date".to_string())
}
}
pub fn positive(value: &FieldValue) -> Result<(), String> {
if let Some(num) = value.as_number() {
if num > 0.0 {
Ok(())
} else {
Err("Value must be positive".to_string())
}
} else {
Err("Value must be a number".to_string())
}
}
pub fn negative(value: &FieldValue) -> Result<(), String> {
if let Some(num) = value.as_number() {
if num < 0.0 {
Ok(())
} else {
Err("Value must be negative".to_string())
}
} else {
Err("Value must be a number".to_string())
}
}
pub fn integer(value: &FieldValue) -> Result<(), String> {
if let Some(num) = value.as_number() {
if num.fract() == 0.0 {
Ok(())
} else {
Err("Value must be an integer".to_string())
}
} else {
Err("Value must be a number".to_string())
}
}
pub fn array_length(value: &FieldValue, min: usize, max: usize) -> Result<(), String> {
if let FieldValue::Array(arr) = value {
if arr.len() >= min && arr.len() <= max {
Ok(())
} else {
Err(format!("Array must have between {} and {} items", min, max))
}
} else {
Err("Value must be an array".to_string())
}
}
}
#[derive(Default)]
pub struct ConditionalValidator {
rules: Vec<ConditionalRule>,
}
impl ConditionalValidator {
pub fn new() -> Self {
Self::default()
}
pub fn add_rule(&mut self, rule: ConditionalRule) {
self.rules.push(rule);
}
pub fn validate_conditional_fields<T: crate::core::Form>(
&self,
form: &T,
field_name: &str,
field_value: &FieldValue,
) -> Result<(), String> {
for rule in &self.rules {
if rule.target_field == field_name {
if let Some(condition) = &rule.condition {
let condition_met = self.evaluate_condition(form, condition)?;
if condition_met {
for validator in &rule.validators {
validator(field_value)?;
}
}
}
}
}
Ok(())
}
#[allow(clippy::only_used_in_recursion)]
fn evaluate_condition<T: crate::core::Form>(
&self,
form: &T,
condition: &FieldCondition,
) -> Result<bool, String> {
match condition {
FieldCondition::Equals(field, value) => {
let field_value = form.get_field_value(field);
Ok(field_value == *value)
}
FieldCondition::NotEquals(field, value) => {
let field_value = form.get_field_value(field);
Ok(field_value != *value)
}
FieldCondition::Contains(field, value) => {
if let FieldValue::String(field_str) = form.get_field_value(field) {
Ok(field_str.contains(value))
} else {
Ok(false)
}
}
FieldCondition::IsEmpty(field) => {
let field_value = form.get_field_value(field);
Ok(field_value.is_empty())
}
FieldCondition::IsNotEmpty(field) => {
let field_value = form.get_field_value(field);
Ok(!field_value.is_empty())
}
FieldCondition::And(conditions) => {
for condition in conditions {
if !self.evaluate_condition(form, condition)? {
return Ok(false);
}
}
Ok(true)
}
FieldCondition::Or(conditions) => {
for condition in conditions {
if self.evaluate_condition(form, condition)? {
return Ok(true);
}
}
Ok(false)
}
}
}
}
pub struct ConditionalRule {
pub target_field: String,
pub condition: Option<FieldCondition>,
pub validators: Vec<FieldValidator>,
pub error_message: Option<String>,
}
impl ConditionalRule {
pub fn new(target_field: String) -> Self {
Self {
target_field,
condition: None,
validators: Vec::new(),
error_message: None,
}
}
pub fn when(mut self, condition: FieldCondition) -> Self {
self.condition = Some(condition);
self
}
pub fn validate_with(mut self, validator: FieldValidator) -> Self {
self.validators.push(validator);
self
}
pub fn with_error_message(mut self, message: String) -> Self {
self.error_message = Some(message);
self
}
}
#[derive(Debug, Clone)]
pub enum FieldCondition {
Equals(String, FieldValue),
NotEquals(String, FieldValue),
Contains(String, String),
IsEmpty(String),
IsNotEmpty(String),
And(Vec<FieldCondition>),
Or(Vec<FieldCondition>),
}
impl FieldCondition {
pub fn equals(field: &str, value: FieldValue) -> Self {
Self::Equals(field.to_string(), value)
}
pub fn not_equals(field: &str, value: FieldValue) -> Self {
Self::NotEquals(field.to_string(), value)
}
pub fn contains(field: &str, value: &str) -> Self {
Self::Contains(field.to_string(), value.to_string())
}
pub fn is_empty(field: &str) -> Self {
Self::IsEmpty(field.to_string())
}
pub fn is_not_empty(field: &str) -> Self {
Self::IsNotEmpty(field.to_string())
}
pub fn and(conditions: Vec<FieldCondition>) -> Self {
Self::And(conditions)
}
pub fn or(conditions: Vec<FieldCondition>) -> Self {
Self::Or(conditions)
}
}