casual/lib.rs
1//! Simple crate for parsing user input.
2//!
3//! # Examples
4//!
5//! Rust type inference is used to know what to return.
6//!
7//! ```no_run
8//! let username: String = casual::prompt("Please enter your name: ").get();
9//! ```
10//!
11//! [`FromStr`] is used to parse the input, so you can read any type that
12//! implements [`FromStr`].
13//!
14//! ```no_run
15//! let age: u32 = casual::prompt("Please enter your age: ").get();
16//! ```
17//!
18//! [`.matches()`] can be used to validate the input data.
19//!
20//! ```no_run
21//! let age: u32 = casual::prompt("Please enter your age again: ")
22//! .matches(|x| *x < 120)
23//! .get();
24//! ```
25//!
26//! A convenience function [`confirm`] is provided for getting a yes or no
27//! answer.
28//!
29//! ```no_run
30//! if casual::confirm("Are you sure you want to continue?") {
31//! // continue
32//! } else {
33//! panic!("Aborted!");
34//! }
35//! ```
36//!
37//! [`FromStr`]: https://doc.rust-lang.org/std/str/trait.FromStr.html
38//! [`.matches()`]: struct.Input.html#method.matches
39//! [`confirm`]: fn.confirm.html
40
41use std::fmt::{self, Debug, Display};
42use std::io::{self, Write};
43use std::str::FromStr;
44
45/////////////////////////////////////////////////////////////////////////
46// Definitions
47/////////////////////////////////////////////////////////////////////////
48
49/// A validator for user input.
50struct Validator<T> {
51 raw: Box<dyn Fn(&T) -> bool + 'static>,
52}
53
54/// An input builder.
55pub struct Input<T> {
56 prompt: Option<String>,
57 prefix: Option<String>,
58 suffix: Option<String>,
59 default: Option<T>,
60 validator: Option<Validator<T>>,
61}
62
63/////////////////////////////////////////////////////////////////////////
64// Implementations
65/////////////////////////////////////////////////////////////////////////
66
67impl<T> Validator<T> {
68 /// Construct a new `Validator`.
69 fn new<F>(raw: F) -> Self
70 where
71 F: Fn(&T) -> bool + 'static,
72 {
73 Self { raw: Box::new(raw) }
74 }
75
76 /// Run the validator on the given input.
77 fn run(&self, input: &T) -> bool {
78 (self.raw)(input)
79 }
80}
81
82impl<T: Debug> Debug for Input<T> {
83 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
84 f.debug_struct("Input")
85 .field("prefix", &self.prefix)
86 .field("prompt", &self.prompt)
87 .field("suffix", &self.suffix)
88 .field("default", &self.default)
89 .finish() // FIXME rust-lang/rust#67364:
90 // use .finish_non_exhaustive() when it's stabilized
91 }
92}
93
94impl<T> Default for Input<T> {
95 /// Construct a new empty `Input`.
96 ///
97 /// Identical to [`Input::new()`](struct.Input.html#method.new).
98 fn default() -> Self {
99 Self::new()
100 }
101}
102
103impl<T> Input<T> {
104 /// Construct a new empty `Input`.
105 ///
106 /// Identical to [`Input::default()`](struct.Input.html#impl-Default).
107 pub fn new() -> Self {
108 Self {
109 prefix: None,
110 prompt: None,
111 suffix: None,
112 default: None,
113 validator: None,
114 }
115 }
116
117 /// Set the prompt to display before waiting for user input.
118 pub fn prompt<S: Into<String>>(mut self, prompt: S) -> Self {
119 self.prompt = Some(prompt.into());
120 self
121 }
122
123 /// Set the prompt prefix.
124 pub fn prefix<S: Into<String>>(mut self, prefix: S) -> Self {
125 self.prefix = Some(prefix.into());
126 self
127 }
128
129 /// Set the prompt suffix.
130 pub fn suffix<S: Into<String>>(mut self, suffix: S) -> Self {
131 self.suffix = Some(suffix.into());
132 self
133 }
134
135 /// Set the default value.
136 ///
137 /// If set, this will be returned in the event the user enters an empty
138 /// input.
139 pub fn default(mut self, default: T) -> Self {
140 self.default = Some(default);
141 self
142 }
143
144 /// Check input values.
145 ///
146 /// If set, this function will be called on the parsed user input and only
147 /// if it passes will we return the value.
148 ///
149 /// # Examples
150 ///
151 /// ```no_run
152 /// # use casual::Input;
153 /// let num: u32 = Input::new().matches(|x| *x != 10).get();
154 /// ```
155 pub fn matches<F>(mut self, matches: F) -> Self
156 where
157 F: Fn(&T) -> bool + 'static,
158 {
159 self.validator = Some(Validator::new(matches));
160 self
161 }
162}
163
164fn read_line(prompt: &Option<String>) -> io::Result<String> {
165 if let Some(prompt) = prompt {
166 let mut stdout = io::stdout();
167 stdout.write_all(prompt.as_bytes())?;
168 stdout.flush()?;
169 }
170 let mut result = String::new();
171 io::stdin().read_line(&mut result)?;
172 Ok(result)
173}
174
175impl<T> Input<T>
176where
177 T: FromStr,
178 <T as FromStr>::Err: Display,
179{
180 fn try_get_with<F>(self, read_line: F) -> io::Result<T>
181 where
182 F: Fn(&Option<String>) -> io::Result<String>,
183 {
184 let Self {
185 prompt,
186 prefix,
187 suffix,
188 default,
189 validator,
190 } = self;
191
192 let prompt = prompt.map(move |prompt| {
193 let mut p = String::new();
194 if let Some(prefix) = prefix {
195 p.push_str(&prefix);
196 }
197 p.push_str(&prompt);
198 if let Some(suffix) = suffix {
199 p.push_str(&suffix);
200 }
201 p
202 });
203
204 Ok(loop {
205 match read_line(&prompt)?.trim() {
206 "" => {
207 if let Some(default) = default {
208 break default;
209 } else {
210 continue;
211 }
212 }
213 raw => match raw.parse() {
214 Ok(result) => {
215 if let Some(validator) = &validator {
216 if !validator.run(&result) {
217 println!("Error: invalid input");
218 continue;
219 }
220 }
221 break result;
222 }
223 Err(err) => {
224 println!("Error: {}", err);
225 continue;
226 }
227 },
228 }
229 })
230 }
231
232 #[inline]
233 fn try_get(self) -> io::Result<T> {
234 self.try_get_with(read_line)
235 }
236
237 /// Consumes the `Input` and reads the input from the user.
238 ///
239 /// This function uses [`FromStr`] to parse the input data.
240 ///
241 /// ```no_run
242 /// # use casual::Input;
243 /// let num: u32 = Input::new().prompt("Enter a number: ").get();
244 /// ```
245 ///
246 /// [`FromStr`]: https://doc.rust-lang.org/std/str/trait.FromStr.html
247 pub fn get(self) -> T {
248 self.try_get().unwrap()
249 }
250
251 /// Consumes the `Input` and applies the given function to it.
252 ///
253 /// This function uses [`FromStr`] to parse the input data. The result is
254 /// then fed to the given closure.
255 ///
256 /// ```no_run
257 /// # use casual::Input;
258 /// let value = Input::new().map(|s: String| &s.to_lowercase() == "yes");
259 /// ```
260 ///
261 /// [`FromStr`]: https://doc.rust-lang.org/std/str/trait.FromStr.html
262 pub fn map<F, U>(self, map: F) -> U
263 where
264 F: Fn(T) -> U,
265 {
266 map(self.get())
267 }
268}
269
270/////////////////////////////////////////////////////////////////////////
271// Shortcut functions
272/////////////////////////////////////////////////////////////////////////
273
274/// Returns a new empty `Input`.
275///
276/// # Examples
277///
278/// Read in something without any prompt.
279///
280/// ```no_run
281/// # use casual::input;
282/// let data: String = input().get();
283/// ```
284pub fn input<T>() -> Input<T> {
285 Input::new()
286}
287
288/// Returns an `Input` that prompts the user for input.
289///
290/// # Examples
291///
292/// Read in a simple string:
293///
294/// ```no_run
295/// # use casual::prompt;
296/// let username: String = prompt("Please enter your name: ").get();
297/// ```
298///
299/// Types that implement [`FromStr`] will be automatically parsed.
300///
301/// ```no_run
302/// # use casual::prompt;
303/// let years = prompt("How many years have you been coding Rust: ")
304/// .default(0)
305/// .get();
306/// ```
307///
308/// [`FromStr`]: https://doc.rust-lang.org/std/str/trait.FromStr.html
309pub fn prompt<S, T>(text: S) -> Input<T>
310where
311 S: Into<String>,
312{
313 Input::new().prompt(text)
314}
315
316/// Prompts the user for confirmation (yes/no).
317///
318/// # Examples
319///
320/// ```no_run
321/// # use casual::confirm;
322/// if confirm("Are you sure you want to continue?") {
323/// // continue
324/// } else {
325/// panic!("Aborted!");
326/// }
327/// ```
328pub fn confirm<S: Into<String>>(text: S) -> bool {
329 prompt(text)
330 .suffix(" [y/N] ")
331 .default("n".to_string())
332 .matches(|s| matches!(&*s.trim().to_lowercase(), "n" | "no" | "y" | "yes"))
333 .map(|s| matches!(&*s.to_lowercase(), "y" | "yes"))
334}