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 or returns
4//! a short human-readable reason. In an interactive prompt the reason is shown
5//! and the question is re-asked; in [`PromptMode::NonInteractive`](crate::prompt::PromptMode::NonInteractive)
6//! 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
9//! closure directly; reusable rules can also implement the trait by hand.
10
11/// The outcome of validating a candidate answer: `Ok(())` accepts it, `Err`
12/// 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
37/// and checks for emptiness.
38#[must_use]
39pub fn non_empty(message: impl Into<String>) -> impl Validator {
40    let message = message.into();
41    move |input: &str| {
42        if input.trim().is_empty() {
43            Err(message.clone())
44        } else {
45            Ok(())
46        }
47    }
48}
49
50#[cfg(test)]
51mod tests {
52    use super::{Validator, non_empty};
53
54    #[test]
55    fn closure_is_a_validator() {
56        let v = |input: &str| {
57            if input.len() >= 2 {
58                Ok(())
59            } else {
60                Err("too short".to_string())
61            }
62        };
63        assert!(v.validate("ok").is_ok());
64        assert_eq!(v.validate("x"), Err("too short".to_string()));
65    }
66
67    #[test]
68    fn non_empty_rejects_blank() {
69        let v = non_empty("required");
70        assert!(v.validate("value").is_ok());
71        assert_eq!(v.validate("   "), Err("required".to_string()));
72    }
73}