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}