1use std::collections::HashMap;
16
17use gpui::App;
18
19use super::signal::Signal;
20
21pub type Validator = Box<dyn Fn(&str) -> Option<String> + 'static>;
23
24#[derive(Default)]
26pub struct FormState {
27 values: HashMap<&'static str, String>,
28 errors: HashMap<&'static str, String>,
29 validators: HashMap<&'static str, Validator>,
30}
31
32impl FormState {
33 pub fn new() -> Self {
34 FormState::default()
35 }
36
37 pub fn field(mut self, name: &'static str, initial: impl Into<String>) -> Self {
39 self.values.insert(name, initial.into());
40 self
41 }
42
43 pub fn validator(mut self, name: &'static str, validator: Validator) -> Self {
45 self.validators.insert(name, validator);
46 self
47 }
48
49 pub fn value(&self, name: &str) -> &str {
51 self.values.get(name).map(String::as_str).unwrap_or("")
52 }
53
54 pub fn set(&mut self, name: &'static str, value: impl Into<String>) {
56 self.values.insert(name, value.into());
57 self.errors.remove(name);
58 }
59
60 pub fn validate_field(&mut self, name: &'static str) -> bool {
62 if let Some(validator) = self.validators.get(name) {
63 let value = self.values.get(name).map(String::as_str).unwrap_or("");
64 match validator(value) {
65 Some(message) => {
66 self.errors.insert(name, message);
67 return false;
68 }
69 None => {
70 self.errors.remove(name);
71 }
72 }
73 }
74 true
75 }
76
77 pub fn validate(&mut self) -> bool {
79 let names: Vec<&'static str> = self.validators.keys().copied().collect();
80 let mut ok = true;
81 for name in names {
82 ok &= self.validate_field(name);
83 }
84 ok
85 }
86
87 pub fn error(&self, name: &str) -> Option<&str> {
89 self.errors.get(name).map(String::as_str)
90 }
91
92 pub fn is_valid(&self) -> bool {
94 self.errors.is_empty()
95 }
96}
97
98pub mod validators {
100 use super::Validator;
101
102 pub fn required() -> Validator {
104 Box::new(|v: &str| {
105 if v.trim().is_empty() {
106 Some("Required".to_string())
107 } else {
108 None
109 }
110 })
111 }
112
113 pub fn min_len(n: usize) -> Validator {
115 Box::new(move |v: &str| {
116 if v.chars().count() < n {
117 Some(format!("Must be at least {n} characters"))
118 } else {
119 None
120 }
121 })
122 }
123
124 pub fn email() -> Validator {
126 Box::new(|v: &str| {
127 let ok = v
128 .split_once('@')
129 .map(|(user, domain)| {
130 !user.is_empty() && domain.contains('.') && !domain.starts_with('.')
131 })
132 .unwrap_or(false);
133 if ok {
134 None
135 } else {
136 Some("Enter a valid email".to_string())
137 }
138 })
139 }
140}
141
142pub fn use_form(cx: &mut App, state: FormState) -> Signal<FormState> {
144 Signal::new(cx, state)
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 #[test]
152 fn required_and_min_len() {
153 let req = validators::required();
154 assert!(req("").is_some());
155 assert!(req(" ").is_some());
156 assert!(req("x").is_none());
157
158 let min = validators::min_len(3);
159 assert!(min("ab").is_some());
160 assert!(min("abc").is_none());
161 }
162
163 #[test]
164 fn email_shape() {
165 let email = validators::email();
166 assert!(email("nope").is_some());
167 assert!(email("a@b").is_some());
168 assert!(email("a@b.com").is_none());
169 }
170
171 #[test]
172 fn set_clears_error_then_validate_repopulates() {
173 let mut form = FormState::new()
174 .field("name", "")
175 .validator("name", validators::required());
176 assert!(!form.validate());
177 assert_eq!(form.error("name"), Some("Required"));
178
179 form.set("name", "Ada");
180 assert_eq!(form.error("name"), None);
182 assert!(form.validate());
183 assert!(form.is_valid());
184 }
185}