Skip to main content

telex/
form.rs

1//! Form validation system for declarative input validation.
2//!
3//! Forms collect multiple fields and validate them against rules,
4//! providing error messages when validation fails.
5
6use regex::Regex;
7use std::cell::RefCell;
8use std::collections::HashMap;
9use std::rc::Rc;
10
11/// Type alias for custom validation functions.
12type ValidationFn = Rc<dyn Fn(&str) -> Option<String>>;
13
14/// A validator that can be applied to form fields.
15#[derive(Clone)]
16pub enum Validator {
17    /// Field is required (non-empty).
18    Required,
19    /// Field must have at least this many characters.
20    MinLength(usize),
21    /// Field must have at most this many characters.
22    MaxLength(usize),
23    /// Field must match a regex pattern.
24    Pattern(Regex),
25    /// Field must be a valid email address.
26    Email,
27    /// Field must be a valid number.
28    Number,
29    /// Field must be a valid integer.
30    Integer,
31    /// Custom validation with error message.
32    Custom(ValidationFn),
33}
34
35impl Validator {
36    /// Create a custom validator with a closure.
37    pub fn custom<F>(f: F) -> Self
38    where
39        F: Fn(&str) -> Option<String> + 'static,
40    {
41        Validator::Custom(Rc::new(f))
42    }
43
44    /// Create a pattern validator from a regex string.
45    pub fn pattern(pattern: &str) -> Result<Self, regex::Error> {
46        Ok(Validator::Pattern(Regex::new(pattern)?))
47    }
48
49    /// Validate a value, returning an error message if invalid.
50    pub fn validate(&self, value: &str) -> Option<String> {
51        match self {
52            Validator::Required => {
53                if value.trim().is_empty() {
54                    Some("This field is required".to_string())
55                } else {
56                    None
57                }
58            }
59            Validator::MinLength(min) => {
60                if value.len() < *min {
61                    Some(format!("Must be at least {} characters", min))
62                } else {
63                    None
64                }
65            }
66            Validator::MaxLength(max) => {
67                if value.len() > *max {
68                    Some(format!("Must be at most {} characters", max))
69                } else {
70                    None
71                }
72            }
73            Validator::Pattern(regex) => {
74                if regex.is_match(value) {
75                    None
76                } else {
77                    Some("Invalid format".to_string())
78                }
79            }
80            Validator::Email => {
81                // Simple email validation
82                let email_pattern = Regex::new(r"^[^@\s]+@[^@\s]+\.[^@\s]+$").unwrap();
83                if value.is_empty() || email_pattern.is_match(value) {
84                    None
85                } else {
86                    Some("Invalid email address".to_string())
87                }
88            }
89            Validator::Number => {
90                if value.is_empty() || value.parse::<f64>().is_ok() {
91                    None
92                } else {
93                    Some("Must be a valid number".to_string())
94                }
95            }
96            Validator::Integer => {
97                if value.is_empty() || value.parse::<i64>().is_ok() {
98                    None
99                } else {
100                    Some("Must be a valid integer".to_string())
101                }
102            }
103            Validator::Custom(f) => f(value),
104        }
105    }
106}
107
108/// A single field in a form.
109#[derive(Clone)]
110pub struct FormField {
111    /// The field name (identifier).
112    pub name: String,
113    /// Current value.
114    pub value: String,
115    /// Validators to apply.
116    pub validators: Vec<Validator>,
117    /// Current error message (if validation failed).
118    pub error: Option<String>,
119    /// Custom error message to use instead of validator messages.
120    pub custom_error_message: Option<String>,
121    /// Whether this field has been "touched" (user interacted with it).
122    pub touched: bool,
123}
124
125impl FormField {
126    /// Create a new form field.
127    pub fn new(name: impl Into<String>) -> Self {
128        Self {
129            name: name.into(),
130            value: String::new(),
131            validators: Vec::new(),
132            error: None,
133            custom_error_message: None,
134            touched: false,
135        }
136    }
137
138    /// Set the initial value.
139    pub fn value(mut self, value: impl Into<String>) -> Self {
140        self.value = value.into();
141        self
142    }
143
144    /// Add a validator.
145    pub fn validate(mut self, validator: Validator) -> Self {
146        self.validators.push(validator);
147        self
148    }
149
150    /// Set a custom error message to use for any validation failure.
151    pub fn error_message(mut self, message: impl Into<String>) -> Self {
152        self.custom_error_message = Some(message.into());
153        self
154    }
155
156    /// Run validation and return the error if any.
157    pub fn run_validation(&mut self) -> Option<String> {
158        for validator in &self.validators {
159            if let Some(err) = validator.validate(&self.value) {
160                let error = self.custom_error_message.clone().unwrap_or(err);
161                self.error = Some(error.clone());
162                return Some(error);
163            }
164        }
165        self.error = None;
166        None
167    }
168
169    /// Check if the field is valid (no error).
170    pub fn is_valid(&self) -> bool {
171        self.error.is_none()
172    }
173}
174
175/// Form state managing multiple fields.
176#[derive(Clone)]
177pub struct FormState {
178    inner: Rc<RefCell<FormStateInner>>,
179}
180
181struct FormStateInner {
182    fields: HashMap<String, FormField>,
183    field_order: Vec<String>,
184}
185
186impl Default for FormState {
187    fn default() -> Self {
188        Self::new()
189    }
190}
191
192impl FormState {
193    /// Create a new empty form state.
194    pub fn new() -> Self {
195        Self {
196            inner: Rc::new(RefCell::new(FormStateInner {
197                fields: HashMap::new(),
198                field_order: Vec::new(),
199            })),
200        }
201    }
202
203    /// Add a field to the form.
204    pub fn field(self, field: FormField) -> Self {
205        let mut inner = self.inner.borrow_mut();
206        let name = field.name.clone();
207        inner.fields.insert(name.clone(), field);
208        if !inner.field_order.contains(&name) {
209            inner.field_order.push(name);
210        }
211        drop(inner);
212        self
213    }
214
215    /// Get a field by name.
216    pub fn get_field(&self, name: &str) -> Option<FormField> {
217        let inner = self.inner.borrow();
218        inner.fields.get(name).cloned()
219    }
220
221    /// Get the value of a field.
222    pub fn get_value(&self, name: &str) -> String {
223        let inner = self.inner.borrow();
224        inner
225            .fields
226            .get(name)
227            .map(|f| f.value.clone())
228            .unwrap_or_default()
229    }
230
231    /// Set the value of a field.
232    pub fn set_value(&self, name: &str, value: String) {
233        let mut inner = self.inner.borrow_mut();
234        if let Some(field) = inner.fields.get_mut(name) {
235            field.value = value;
236            field.touched = true;
237            // Re-validate on change
238            field.run_validation();
239        }
240    }
241
242    /// Get the error for a field.
243    pub fn get_error(&self, name: &str) -> Option<String> {
244        let inner = self.inner.borrow();
245        inner.fields.get(name).and_then(|f| f.error.clone())
246    }
247
248    /// Check if a field has been touched.
249    pub fn is_touched(&self, name: &str) -> bool {
250        let inner = self.inner.borrow();
251        inner.fields.get(name).map(|f| f.touched).unwrap_or(false)
252    }
253
254    /// Mark a field as touched.
255    pub fn touch(&self, name: &str) {
256        let mut inner = self.inner.borrow_mut();
257        if let Some(field) = inner.fields.get_mut(name) {
258            field.touched = true;
259            // Validate when touched
260            field.run_validation();
261        }
262    }
263
264    /// Validate all fields and return whether the form is valid.
265    pub fn validate(&self) -> bool {
266        let mut inner = self.inner.borrow_mut();
267        let mut all_valid = true;
268        for field in inner.fields.values_mut() {
269            if field.run_validation().is_some() {
270                all_valid = false;
271            }
272        }
273        all_valid
274    }
275
276    /// Check if the entire form is valid (all fields pass validation).
277    pub fn is_valid(&self) -> bool {
278        let inner = self.inner.borrow();
279        inner.fields.values().all(|f| f.error.is_none())
280    }
281
282    /// Get all field values as a HashMap.
283    pub fn values(&self) -> HashMap<String, String> {
284        let inner = self.inner.borrow();
285        inner
286            .fields
287            .iter()
288            .map(|(k, v)| (k.clone(), v.value.clone()))
289            .collect()
290    }
291
292    /// Get field names in order.
293    pub fn field_names(&self) -> Vec<String> {
294        let inner = self.inner.borrow();
295        inner.field_order.clone()
296    }
297
298    /// Reset all fields to empty and clear errors.
299    pub fn reset(&self) {
300        let mut inner = self.inner.borrow_mut();
301        for field in inner.fields.values_mut() {
302            field.value = String::new();
303            field.error = None;
304            field.touched = false;
305        }
306    }
307
308    /// Get all errors as a HashMap.
309    pub fn errors(&self) -> HashMap<String, String> {
310        let inner = self.inner.borrow();
311        inner
312            .fields
313            .iter()
314            .filter_map(|(k, v)| v.error.as_ref().map(|e| (k.clone(), e.clone())))
315            .collect()
316    }
317}
318
319/// Builder for creating form fields fluently.
320pub struct FieldBuilder {
321    field: FormField,
322}
323
324impl FieldBuilder {
325    /// Create a new field builder.
326    pub fn new(name: impl Into<String>) -> Self {
327        Self {
328            field: FormField::new(name),
329        }
330    }
331
332    /// Set the initial value.
333    pub fn value(mut self, value: impl Into<String>) -> Self {
334        self.field.value = value.into();
335        self
336    }
337
338    /// Add the Required validator.
339    pub fn required(mut self) -> Self {
340        self.field.validators.push(Validator::Required);
341        self
342    }
343
344    /// Add the MinLength validator.
345    pub fn min_length(mut self, min: usize) -> Self {
346        self.field.validators.push(Validator::MinLength(min));
347        self
348    }
349
350    /// Add the MaxLength validator.
351    pub fn max_length(mut self, max: usize) -> Self {
352        self.field.validators.push(Validator::MaxLength(max));
353        self
354    }
355
356    /// Add the Email validator.
357    pub fn email(mut self) -> Self {
358        self.field.validators.push(Validator::Email);
359        self
360    }
361
362    /// Add the Number validator.
363    pub fn number(mut self) -> Self {
364        self.field.validators.push(Validator::Number);
365        self
366    }
367
368    /// Add the Integer validator.
369    pub fn integer(mut self) -> Self {
370        self.field.validators.push(Validator::Integer);
371        self
372    }
373
374    /// Add a pattern validator.
375    pub fn pattern(mut self, pattern: &str) -> Result<Self, regex::Error> {
376        self.field
377            .validators
378            .push(Validator::Pattern(Regex::new(pattern)?));
379        Ok(self)
380    }
381
382    /// Add a custom validator.
383    pub fn custom<F>(mut self, f: F) -> Self
384    where
385        F: Fn(&str) -> Option<String> + 'static,
386    {
387        self.field.validators.push(Validator::Custom(Rc::new(f)));
388        self
389    }
390
391    /// Set a custom error message.
392    pub fn error_message(mut self, message: impl Into<String>) -> Self {
393        self.field.custom_error_message = Some(message.into());
394        self
395    }
396
397    /// Build the field.
398    pub fn build(self) -> FormField {
399        self.field
400    }
401}
402
403#[cfg(test)]
404mod tests {
405    use super::*;
406
407    #[test]
408    fn test_required_validator() {
409        assert!(Validator::Required.validate("").is_some());
410        assert!(Validator::Required.validate("   ").is_some());
411        assert!(Validator::Required.validate("hello").is_none());
412    }
413
414    #[test]
415    fn test_min_length_validator() {
416        let validator = Validator::MinLength(5);
417        assert!(validator.validate("hi").is_some());
418        assert!(validator.validate("hello").is_none());
419        assert!(validator.validate("hello world").is_none());
420    }
421
422    #[test]
423    fn test_email_validator() {
424        assert!(Validator::Email.validate("invalid").is_some());
425        assert!(Validator::Email.validate("test@example.com").is_none());
426        assert!(Validator::Email.validate("").is_none()); // Empty is valid (use Required for mandatory)
427    }
428
429    #[test]
430    fn test_form_state() {
431        let form = FormState::new()
432            .field(FieldBuilder::new("email").required().email().build())
433            .field(
434                FieldBuilder::new("password")
435                    .required()
436                    .min_length(8)
437                    .build(),
438            );
439
440        // Initially invalid (empty values)
441        assert!(!form.validate());
442
443        // Set valid values
444        form.set_value("email", "test@example.com".to_string());
445        form.set_value("password", "password123".to_string());
446        assert!(form.validate());
447        assert!(form.is_valid());
448
449        // Invalid email
450        form.set_value("email", "invalid".to_string());
451        assert!(!form.is_valid());
452    }
453
454    #[test]
455    fn test_custom_validator() {
456        let field = FieldBuilder::new("username")
457            .custom(|v| {
458                if v.contains(' ') {
459                    Some("Username cannot contain spaces".to_string())
460                } else {
461                    None
462                }
463            })
464            .build();
465
466        let form = FormState::new().field(field);
467
468        form.set_value("username", "hello world".to_string());
469        assert!(!form.is_valid());
470        assert_eq!(
471            form.get_error("username"),
472            Some("Username cannot contain spaces".to_string())
473        );
474
475        form.set_value("username", "helloworld".to_string());
476        assert!(form.is_valid());
477    }
478}