use std::collections::HashMap;
use gpui::App;
use super::signal::Signal;
pub type Validator = Box<dyn Fn(&str) -> Option<String> + 'static>;
#[derive(Default)]
pub struct FormState {
values: HashMap<&'static str, String>,
errors: HashMap<&'static str, String>,
validators: HashMap<&'static str, Validator>,
}
impl FormState {
pub fn new() -> Self {
FormState::default()
}
pub fn field(mut self, name: &'static str, initial: impl Into<String>) -> Self {
self.values.insert(name, initial.into());
self
}
pub fn validator(mut self, name: &'static str, validator: Validator) -> Self {
self.validators.insert(name, validator);
self
}
pub fn value(&self, name: &str) -> &str {
self.values.get(name).map(String::as_str).unwrap_or("")
}
pub fn set(&mut self, name: &'static str, value: impl Into<String>) {
self.values.insert(name, value.into());
self.errors.remove(name);
}
pub fn validate_field(&mut self, name: &'static str) -> bool {
if let Some(validator) = self.validators.get(name) {
let value = self.values.get(name).map(String::as_str).unwrap_or("");
match validator(value) {
Some(message) => {
self.errors.insert(name, message);
return false;
}
None => {
self.errors.remove(name);
}
}
}
true
}
pub fn validate(&mut self) -> bool {
let names: Vec<&'static str> = self.validators.keys().copied().collect();
let mut ok = true;
for name in names {
ok &= self.validate_field(name);
}
ok
}
pub fn error(&self, name: &str) -> Option<&str> {
self.errors.get(name).map(String::as_str)
}
pub fn is_valid(&self) -> bool {
self.errors.is_empty()
}
}
pub mod validators {
use super::Validator;
pub fn required() -> Validator {
Box::new(|v: &str| {
if v.trim().is_empty() {
Some("Required".to_string())
} else {
None
}
})
}
pub fn min_len(n: usize) -> Validator {
Box::new(move |v: &str| {
if v.chars().count() < n {
Some(format!("Must be at least {n} characters"))
} else {
None
}
})
}
pub fn email() -> Validator {
Box::new(|v: &str| {
let ok = v
.split_once('@')
.map(|(user, domain)| {
!user.is_empty() && domain.contains('.') && !domain.starts_with('.')
})
.unwrap_or(false);
if ok {
None
} else {
Some("Enter a valid email".to_string())
}
})
}
}
pub fn use_form(cx: &mut App, state: FormState) -> Signal<FormState> {
Signal::new(cx, state)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn required_and_min_len() {
let req = validators::required();
assert!(req("").is_some());
assert!(req(" ").is_some());
assert!(req("x").is_none());
let min = validators::min_len(3);
assert!(min("ab").is_some());
assert!(min("abc").is_none());
}
#[test]
fn email_shape() {
let email = validators::email();
assert!(email("nope").is_some());
assert!(email("a@b").is_some());
assert!(email("a@b.com").is_none());
}
#[test]
fn set_clears_error_then_validate_repopulates() {
let mut form = FormState::new()
.field("name", "")
.validator("name", validators::required());
assert!(!form.validate());
assert_eq!(form.error("name"), Some("Required"));
form.set("name", "Ada");
assert_eq!(form.error("name"), None);
assert!(form.validate());
assert!(form.is_valid());
}
}