use regex::Regex;
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
type ValidationFn = Rc<dyn Fn(&str) -> Option<String>>;
#[derive(Clone)]
pub enum Validator {
Required,
MinLength(usize),
MaxLength(usize),
Pattern(Regex),
Email,
Number,
Integer,
Custom(ValidationFn),
}
impl Validator {
pub fn custom<F>(f: F) -> Self
where
F: Fn(&str) -> Option<String> + 'static,
{
Validator::Custom(Rc::new(f))
}
pub fn pattern(pattern: &str) -> Result<Self, regex::Error> {
Ok(Validator::Pattern(Regex::new(pattern)?))
}
pub fn validate(&self, value: &str) -> Option<String> {
match self {
Validator::Required => {
if value.trim().is_empty() {
Some("This field is required".to_string())
} else {
None
}
}
Validator::MinLength(min) => {
if value.len() < *min {
Some(format!("Must be at least {} characters", min))
} else {
None
}
}
Validator::MaxLength(max) => {
if value.len() > *max {
Some(format!("Must be at most {} characters", max))
} else {
None
}
}
Validator::Pattern(regex) => {
if regex.is_match(value) {
None
} else {
Some("Invalid format".to_string())
}
}
Validator::Email => {
let email_pattern = Regex::new(r"^[^@\s]+@[^@\s]+\.[^@\s]+$").unwrap();
if value.is_empty() || email_pattern.is_match(value) {
None
} else {
Some("Invalid email address".to_string())
}
}
Validator::Number => {
if value.is_empty() || value.parse::<f64>().is_ok() {
None
} else {
Some("Must be a valid number".to_string())
}
}
Validator::Integer => {
if value.is_empty() || value.parse::<i64>().is_ok() {
None
} else {
Some("Must be a valid integer".to_string())
}
}
Validator::Custom(f) => f(value),
}
}
}
#[derive(Clone)]
pub struct FormField {
pub name: String,
pub value: String,
pub validators: Vec<Validator>,
pub error: Option<String>,
pub custom_error_message: Option<String>,
pub touched: bool,
}
impl FormField {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
value: String::new(),
validators: Vec::new(),
error: None,
custom_error_message: None,
touched: false,
}
}
pub fn value(mut self, value: impl Into<String>) -> Self {
self.value = value.into();
self
}
pub fn validate(mut self, validator: Validator) -> Self {
self.validators.push(validator);
self
}
pub fn error_message(mut self, message: impl Into<String>) -> Self {
self.custom_error_message = Some(message.into());
self
}
pub fn run_validation(&mut self) -> Option<String> {
for validator in &self.validators {
if let Some(err) = validator.validate(&self.value) {
let error = self.custom_error_message.clone().unwrap_or(err);
self.error = Some(error.clone());
return Some(error);
}
}
self.error = None;
None
}
pub fn is_valid(&self) -> bool {
self.error.is_none()
}
}
#[derive(Clone)]
pub struct FormState {
inner: Rc<RefCell<FormStateInner>>,
}
struct FormStateInner {
fields: HashMap<String, FormField>,
field_order: Vec<String>,
}
impl Default for FormState {
fn default() -> Self {
Self::new()
}
}
impl FormState {
pub fn new() -> Self {
Self {
inner: Rc::new(RefCell::new(FormStateInner {
fields: HashMap::new(),
field_order: Vec::new(),
})),
}
}
pub fn field(self, field: FormField) -> Self {
let mut inner = self.inner.borrow_mut();
let name = field.name.clone();
inner.fields.insert(name.clone(), field);
if !inner.field_order.contains(&name) {
inner.field_order.push(name);
}
drop(inner);
self
}
pub fn get_field(&self, name: &str) -> Option<FormField> {
let inner = self.inner.borrow();
inner.fields.get(name).cloned()
}
pub fn get_value(&self, name: &str) -> String {
let inner = self.inner.borrow();
inner
.fields
.get(name)
.map(|f| f.value.clone())
.unwrap_or_default()
}
pub fn set_value(&self, name: &str, value: String) {
let mut inner = self.inner.borrow_mut();
if let Some(field) = inner.fields.get_mut(name) {
field.value = value;
field.touched = true;
field.run_validation();
}
}
pub fn get_error(&self, name: &str) -> Option<String> {
let inner = self.inner.borrow();
inner.fields.get(name).and_then(|f| f.error.clone())
}
pub fn is_touched(&self, name: &str) -> bool {
let inner = self.inner.borrow();
inner.fields.get(name).map(|f| f.touched).unwrap_or(false)
}
pub fn touch(&self, name: &str) {
let mut inner = self.inner.borrow_mut();
if let Some(field) = inner.fields.get_mut(name) {
field.touched = true;
field.run_validation();
}
}
pub fn validate(&self) -> bool {
let mut inner = self.inner.borrow_mut();
let mut all_valid = true;
for field in inner.fields.values_mut() {
if field.run_validation().is_some() {
all_valid = false;
}
}
all_valid
}
pub fn is_valid(&self) -> bool {
let inner = self.inner.borrow();
inner.fields.values().all(|f| f.error.is_none())
}
pub fn values(&self) -> HashMap<String, String> {
let inner = self.inner.borrow();
inner
.fields
.iter()
.map(|(k, v)| (k.clone(), v.value.clone()))
.collect()
}
pub fn field_names(&self) -> Vec<String> {
let inner = self.inner.borrow();
inner.field_order.clone()
}
pub fn reset(&self) {
let mut inner = self.inner.borrow_mut();
for field in inner.fields.values_mut() {
field.value = String::new();
field.error = None;
field.touched = false;
}
}
pub fn errors(&self) -> HashMap<String, String> {
let inner = self.inner.borrow();
inner
.fields
.iter()
.filter_map(|(k, v)| v.error.as_ref().map(|e| (k.clone(), e.clone())))
.collect()
}
}
pub struct FieldBuilder {
field: FormField,
}
impl FieldBuilder {
pub fn new(name: impl Into<String>) -> Self {
Self {
field: FormField::new(name),
}
}
pub fn value(mut self, value: impl Into<String>) -> Self {
self.field.value = value.into();
self
}
pub fn required(mut self) -> Self {
self.field.validators.push(Validator::Required);
self
}
pub fn min_length(mut self, min: usize) -> Self {
self.field.validators.push(Validator::MinLength(min));
self
}
pub fn max_length(mut self, max: usize) -> Self {
self.field.validators.push(Validator::MaxLength(max));
self
}
pub fn email(mut self) -> Self {
self.field.validators.push(Validator::Email);
self
}
pub fn number(mut self) -> Self {
self.field.validators.push(Validator::Number);
self
}
pub fn integer(mut self) -> Self {
self.field.validators.push(Validator::Integer);
self
}
pub fn pattern(mut self, pattern: &str) -> Result<Self, regex::Error> {
self.field
.validators
.push(Validator::Pattern(Regex::new(pattern)?));
Ok(self)
}
pub fn custom<F>(mut self, f: F) -> Self
where
F: Fn(&str) -> Option<String> + 'static,
{
self.field.validators.push(Validator::Custom(Rc::new(f)));
self
}
pub fn error_message(mut self, message: impl Into<String>) -> Self {
self.field.custom_error_message = Some(message.into());
self
}
pub fn build(self) -> FormField {
self.field
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_required_validator() {
assert!(Validator::Required.validate("").is_some());
assert!(Validator::Required.validate(" ").is_some());
assert!(Validator::Required.validate("hello").is_none());
}
#[test]
fn test_min_length_validator() {
let validator = Validator::MinLength(5);
assert!(validator.validate("hi").is_some());
assert!(validator.validate("hello").is_none());
assert!(validator.validate("hello world").is_none());
}
#[test]
fn test_email_validator() {
assert!(Validator::Email.validate("invalid").is_some());
assert!(Validator::Email.validate("test@example.com").is_none());
assert!(Validator::Email.validate("").is_none()); }
#[test]
fn test_form_state() {
let form = FormState::new()
.field(FieldBuilder::new("email").required().email().build())
.field(
FieldBuilder::new("password")
.required()
.min_length(8)
.build(),
);
assert!(!form.validate());
form.set_value("email", "test@example.com".to_string());
form.set_value("password", "password123".to_string());
assert!(form.validate());
assert!(form.is_valid());
form.set_value("email", "invalid".to_string());
assert!(!form.is_valid());
}
#[test]
fn test_custom_validator() {
let field = FieldBuilder::new("username")
.custom(|v| {
if v.contains(' ') {
Some("Username cannot contain spaces".to_string())
} else {
None
}
})
.build();
let form = FormState::new().field(field);
form.set_value("username", "hello world".to_string());
assert!(!form.is_valid());
assert_eq!(
form.get_error("username"),
Some("Username cannot contain spaces".to_string())
);
form.set_value("username", "helloworld".to_string());
assert!(form.is_valid());
}
}