Skip to main content

hjkl_form/
validate.rs

1//! Field validators.
2
3use crate::field::Field;
4use crate::form::Form;
5
6/// Validation function for a single text field. Returns `Err(message)`
7/// to surface an error on the field; `Ok(())` clears any prior error.
8pub type Validator = Box<dyn Fn(&str) -> Result<(), String> + Send>;
9
10/// Run a single field's validator (if any). Populates `meta.error` on
11/// failure and clears it on success. Returns `true` when the field is
12/// valid (or has no validator), `false` when it failed.
13pub fn validate_field(field: &mut Field) -> bool {
14    let (text, validator, meta) = match field {
15        Field::SingleLineText(f) => {
16            let text = f.editor.buffer().as_string();
17            (text, &f.validator, &mut f.meta)
18        }
19        Field::MultiLineText(f) => {
20            let text = f.editor.buffer().as_string();
21            (text, &f.validator, &mut f.meta)
22        }
23        // Non-text fields don't run text validators.
24        Field::Select(_) | Field::Checkbox(_) | Field::Submit(_) => return true,
25    };
26    match validator {
27        Some(v) => match v(&text) {
28            Ok(()) => {
29                meta.error = None;
30                true
31            }
32            Err(msg) => {
33                meta.error = Some(msg);
34                false
35            }
36        },
37        None => {
38            meta.error = None;
39            true
40        }
41    }
42}
43
44impl Form {
45    /// Validate the focused field. Returns the same boolean as
46    /// [`validate_field`].
47    pub fn validate_focused(&mut self) -> bool {
48        let idx = self.focused();
49        match self.fields.get_mut(idx) {
50            Some(field) => validate_field(field),
51            None => true,
52        }
53    }
54
55    /// Validate every field. On error, returns the list of
56    /// `(field_index, error_message)` tuples and leaves each field's
57    /// `meta.error` populated.
58    pub fn validate_all(&mut self) -> Result<(), Vec<(usize, String)>> {
59        let mut errors = Vec::new();
60        for (idx, field) in self.fields.iter_mut().enumerate() {
61            if !validate_field(field)
62                && let Some(msg) = field.meta().error.clone()
63            {
64                errors.push((idx, msg));
65            }
66        }
67        if errors.is_empty() {
68            Ok(())
69        } else {
70            Err(errors)
71        }
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78    use crate::field::{FieldMeta, SubmitField, TextFieldEditor};
79    use hjkl_engine::{Input, Key};
80
81    fn key(c: char) -> Input {
82        Input {
83            key: Key::Char(c),
84            ..Input::default()
85        }
86    }
87
88    #[test]
89    fn empty_validator_blocks_and_populates_error() {
90        let mut name = TextFieldEditor::with_meta(FieldMeta::new("Name").required(true), 1);
91        name.validator = Some(Box::new(|s: &str| {
92            if s.is_empty() {
93                Err("required".into())
94            } else {
95                Ok(())
96            }
97        }));
98        let mut form = Form::new()
99            .with_field(Field::SingleLineText(name))
100            .with_field(Field::Submit(SubmitField::new(FieldMeta::new("Sub"))));
101        let res = form.validate_all();
102        assert!(res.is_err());
103        let errs = res.unwrap_err();
104        assert_eq!(errs.len(), 1);
105        assert_eq!(errs[0].0, 0);
106        assert_eq!(form.fields[0].meta().error.as_deref(), Some("required"));
107    }
108
109    #[test]
110    fn blur_validate_runs_on_focus_change() {
111        let mut name = TextFieldEditor::with_meta(FieldMeta::new("Name").required(true), 1);
112        name.validator = Some(Box::new(|s: &str| {
113            if s.is_empty() {
114                Err("required".into())
115            } else {
116                Ok(())
117            }
118        }));
119        let mut form = Form::new()
120            .with_field(Field::SingleLineText(name))
121            .with_field(Field::Submit(SubmitField::new(FieldMeta::new("Sub"))));
122        // Move off field 0 — blur validator runs and populates error.
123        form.handle_input(key('j'));
124        assert_eq!(form.fields[0].meta().error.as_deref(), Some("required"));
125    }
126}