Skip to main content

use_prompt/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::fmt;
5
6/// Commonly used prompt primitives.
7pub mod prelude {
8    pub use crate::{
9        ConfirmationParseError, PromptText, PromptTextError, YesNoAnswer, is_no, is_yes,
10        parse_confirmation,
11    };
12}
13
14/// Validation errors for prompt text.
15#[derive(Clone, Debug, PartialEq, Eq)]
16pub enum PromptTextError {
17    /// The prompt text was empty or only whitespace.
18    Empty,
19}
20
21impl fmt::Display for PromptTextError {
22    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
23        match self {
24            Self::Empty => formatter.write_str("prompt text cannot be empty"),
25        }
26    }
27}
28
29impl std::error::Error for PromptTextError {}
30
31/// Owned prompt text.
32#[derive(Clone, Debug, PartialEq, Eq, Hash)]
33pub struct PromptText {
34    text: String,
35}
36
37impl PromptText {
38    /// Creates prompt text.
39    ///
40    /// # Errors
41    ///
42    /// Returns [`PromptTextError::Empty`] when `text` is empty or only whitespace.
43    pub fn new(text: impl Into<String>) -> Result<Self, PromptTextError> {
44        let text = text.into();
45        if text.trim().is_empty() {
46            Err(PromptTextError::Empty)
47        } else {
48            Ok(Self { text })
49        }
50    }
51
52    /// Returns the prompt text.
53    #[must_use]
54    pub fn as_str(&self) -> &str {
55        &self.text
56    }
57
58    /// Returns the owned prompt text.
59    #[must_use]
60    pub fn into_string(self) -> String {
61        self.text
62    }
63}
64
65impl AsRef<str> for PromptText {
66    fn as_ref(&self) -> &str {
67        self.as_str()
68    }
69}
70
71impl fmt::Display for PromptText {
72    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
73        formatter.write_str(&self.text)
74    }
75}
76
77/// A primitive yes/no answer.
78#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
79pub enum YesNoAnswer {
80    /// Affirmative answer.
81    Yes,
82    /// Negative answer.
83    No,
84}
85
86impl YesNoAnswer {
87    /// Returns whether this answer is affirmative.
88    #[must_use]
89    pub const fn is_yes(self) -> bool {
90        matches!(self, Self::Yes)
91    }
92
93    /// Returns whether this answer is negative.
94    #[must_use]
95    pub const fn is_no(self) -> bool {
96        matches!(self, Self::No)
97    }
98}
99
100/// Errors returned while parsing confirmation answers.
101#[derive(Clone, Debug, PartialEq, Eq)]
102pub enum ConfirmationParseError {
103    /// The answer was empty after trimming whitespace.
104    Empty,
105    /// The answer was not recognized as yes or no.
106    Unrecognized,
107}
108
109impl fmt::Display for ConfirmationParseError {
110    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
111        match self {
112            Self::Empty => formatter.write_str("confirmation answer cannot be empty"),
113            Self::Unrecognized => formatter.write_str("confirmation answer must be yes or no"),
114        }
115    }
116}
117
118impl std::error::Error for ConfirmationParseError {}
119
120/// Parses a yes/no confirmation answer.
121///
122/// # Errors
123///
124/// Returns [`ConfirmationParseError`] when `input` is empty or not a recognized yes/no answer.
125pub fn parse_confirmation(input: &str) -> Result<YesNoAnswer, ConfirmationParseError> {
126    let trimmed = input.trim();
127    if trimmed.is_empty() {
128        return Err(ConfirmationParseError::Empty);
129    }
130
131    match trimmed.to_ascii_lowercase().as_str() {
132        "y" | "yes" => Ok(YesNoAnswer::Yes),
133        "n" | "no" => Ok(YesNoAnswer::No),
134        _ => Err(ConfirmationParseError::Unrecognized),
135    }
136}
137
138/// Returns whether `input` parses as yes.
139#[must_use]
140pub fn is_yes(input: &str) -> bool {
141    matches!(parse_confirmation(input), Ok(YesNoAnswer::Yes))
142}
143
144/// Returns whether `input` parses as no.
145#[must_use]
146pub fn is_no(input: &str) -> bool {
147    matches!(parse_confirmation(input), Ok(YesNoAnswer::No))
148}
149
150#[cfg(test)]
151mod tests {
152    use super::{
153        ConfirmationParseError, PromptText, PromptTextError, YesNoAnswer, is_no, is_yes,
154        parse_confirmation,
155    };
156
157    #[test]
158    fn validates_prompt_text() -> Result<(), PromptTextError> {
159        let prompt = PromptText::new("Continue?")?;
160
161        assert_eq!(prompt.as_str(), "Continue?");
162        assert_eq!(PromptText::new("  "), Err(PromptTextError::Empty));
163        Ok(())
164    }
165
166    #[test]
167    fn parses_confirmation_answers() -> Result<(), ConfirmationParseError> {
168        assert_eq!(parse_confirmation("yes")?, YesNoAnswer::Yes);
169        assert_eq!(parse_confirmation(" N ")?, YesNoAnswer::No);
170        assert!(is_yes("y"));
171        assert!(is_no("no"));
172        assert_eq!(
173            parse_confirmation("maybe"),
174            Err(ConfirmationParseError::Unrecognized)
175        );
176        Ok(())
177    }
178}