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}