Skip to main content

rskit_cli/prompt/
validate.rs

1//! Answer validation with interactive re-ask.
2//!
3//! A [`Validator`] inspects a candidate answer and either accepts it
4//! or returns a short human-readable reason. In an interactive prompt the reason is shown
5//! and the question is re-asked;
6//! in [`PromptMode::NonInteractive`](crate::prompt::PromptMode::NonInteractive) a rejected default is a typed error rather than a silent bad value.
7//!
8//! Any `Fn(&str) -> Result<(), String>` is a `Validator`, so callers pass a closure directly;
9//! reusable rules can also implement the trait by hand.
10
11/// The outcome of validating a candidate answer: `Ok(())` accepts it,
12/// `Err` carries a short reason to display before re-asking.
13pub type Validation = Result<(), String>;
14
15/// Inspects a candidate answer, accepting it or explaining why it is rejected.
16pub trait Validator {
17    /// Validate `input`, returning `Ok(())` to accept or `Err(reason)` to reject.
18    ///
19    /// # Errors
20    ///
21    /// Returns the human-readable rejection reason to surface to the user.
22    fn validate(&self, input: &str) -> Validation;
23}
24
25impl<F> Validator for F
26where
27    F: Fn(&str) -> Validation,
28{
29    fn validate(&self, input: &str) -> Validation {
30        self(input)
31    }
32}
33
34/// A validator that rejects empty or whitespace-only input with `message`.
35///
36/// A convenience for the most common rule; equivalent to a closure that trims and checks for emptiness.
37#[must_use]
38pub fn non_empty(message: impl Into<String>) -> impl Validator {
39    let message = message.into();
40    move |input: &str| {
41        if input.trim().is_empty() {
42            Err(message.clone())
43        } else {
44            Ok(())
45        }
46    }
47}
48
49#[cfg(test)]
50mod tests {
51    use super::{Validator, non_empty};
52
53    #[test]
54    fn closure_is_a_validator() {
55        let v = |input: &str| {
56            if input.len() >= 2 {
57                Ok(())
58            } else {
59                Err("too short".to_string())
60            }
61        };
62        assert!(v.validate("ok").is_ok());
63        assert_eq!(v.validate("x"), Err("too short".to_string()));
64    }
65
66    #[test]
67    fn non_empty_rejects_blank() {
68        let v = non_empty("required");
69        assert!(v.validate("value").is_ok());
70        assert_eq!(v.validate("   "), Err("required".to_string()));
71    }
72}