question/
lib.rs

1//! An easy to use library for asking users questions when
2//! designing Command Line Interface applications. Reduces
3//! asking questions to a one liner.
4//!
5//! # Examples
6//!
7//! Asking a user a yes or no question requiring that
8//! a valid response is provided.
9//!
10//! ```no_run
11//! # use question::Question;
12//! Question::new("Do you want to continue?").confirm();
13//! ```
14#![cfg_attr(feature = "strict", feature(plugin))]
15#![cfg_attr(feature = "strict", plugin(clippy))]
16#![cfg_attr(feature = "strict", deny(warnings))]
17
18use std::collections::HashMap;
19use std::io::{BufRead, BufReader, Read, Write};
20
21/// An `Answer` builder. Once a question has been formulated
22/// either `ask` or `confirm` may be used to get an answer.
23///
24/// # Examples
25///
26/// The `ask` function will execute exactly the configuration
27/// of the question. This will ask the user if they would like
28/// to continue until they provide a valid yes or no response.
29///
30/// ```no_run
31/// # use question::Question;
32/// Question::new("Do you want to continue?")
33///     .yes_no()
34///     .until_acceptable()
35///     .ask();
36/// ```
37///
38/// The following `confirm` function is exactly equivalent.
39///
40/// ```no_run
41/// # use question::Question;
42/// Question::new("Do you want to continue?").confirm();
43/// ```
44#[derive(Clone)]
45pub struct Question<R, W>
46where
47    R: Read,
48    W: Write,
49{
50    question: String,
51    prompt: String,
52    default: Option<Answer>,
53    clarification: Option<String>,
54    acceptable: Option<Vec<String>>,
55    valid_responses: Option<HashMap<String, Answer>>,
56    tries: Option<u64>,
57    until_acceptable: bool,
58    show_defaults: bool,
59    yes_no: bool,
60    reader: R,
61    writer: W,
62}
63
64impl Question<std::io::Stdin, std::io::Stdout> {
65    /// Create a new `Question`.
66    ///
67    /// # Examples
68    ///
69    /// ```no_run
70    /// # use question::Question;
71    /// Question::new("What is your favorite color?").ask();
72    /// ```
73    pub fn new(question: &str) -> Question<std::io::Stdin, std::io::Stdout> {
74        let question = question.to_string();
75        Question {
76            question: question.clone(),
77            prompt: question,
78            default: None,
79            acceptable: None,
80            valid_responses: None,
81            clarification: None,
82            tries: None,
83            until_acceptable: false,
84            show_defaults: false,
85            yes_no: false,
86            reader: std::io::stdin(),
87            writer: std::io::stdout(),
88        }
89    }
90}
91
92impl<R, W> Question<R, W>
93where
94    R: Read,
95    W: Write,
96{
97    #[cfg(test)]
98    pub fn with_cursor(question: &str, input: R, output: W) -> Question<R, W> {
99        let question = question.to_string();
100        Question {
101            question: question.clone(),
102            prompt: question,
103            default: None,
104            acceptable: None,
105            valid_responses: None,
106            clarification: None,
107            tries: None,
108            until_acceptable: false,
109            show_defaults: false,
110            yes_no: false,
111            reader: input,
112            writer: output,
113        }
114    }
115
116    /// Add a single acceptable response to the list.
117    ///
118    /// # Examples
119    ///
120    /// The following will ask the user if they would like
121    /// to continue until either "y" or "n" is entered.
122    ///
123    /// ```no_run
124    /// # use question::Question;
125    /// Question::new("Do you want to continue?")
126    ///     .accept("y")
127    ///     .accept("n")
128    ///     .until_acceptable()
129    ///     .ask();
130    /// ```
131    pub fn accept(&mut self, accepted: &str) -> &mut Question<R, W> {
132        let accepted = accepted.to_string();
133        match self.acceptable {
134            Some(ref mut vec) => vec.push(accepted),
135            None => {
136                let mut vec = Vec::new();
137                vec.push(accepted);
138                self.acceptable = Some(vec);
139            }
140        }
141        self
142    }
143
144    /// Add a collection of acceptable responses to the list.
145    ///
146    /// # Examples
147    ///
148    /// The following will ask the user if they would like
149    /// to continue until either "y" or "n" is entered.
150    ///
151    /// ```no_run
152    /// # use question::Question;
153    /// Question::new("Do you want to continue?")
154    ///     .acceptable(vec!["y", "n"])
155    ///     .until_acceptable()
156    ///     .ask();
157    /// ```
158    pub fn acceptable(&mut self, accepted: Vec<&str>) -> &mut Question<R, W> {
159        let mut accepted = accepted.into_iter().map(|x| x.into()).collect();
160        match self.acceptable {
161            Some(ref mut vec) => vec.append(&mut accepted),
162            None => self.acceptable = Some(accepted),
163        }
164        self
165    }
166
167    /// Shorthand the most common case of a yes/no question.
168    ///
169    /// # Examples
170    ///
171    /// The following will ask the user if they would like
172    /// to continue until either "y", "n", "yes", or "no",
173    /// is entered.
174    ///
175    /// ```no_run
176    /// # use question::Question;
177    /// Question::new("Do you want to continue?")
178    ///     .yes_no()
179    ///     .until_acceptable()
180    ///     .ask();
181    /// ```
182    pub fn yes_no(&mut self) -> &mut Question<R, W> {
183        self.yes_no = true;
184        let response_keys = vec![
185            String::from("yes"),
186            String::from("y"),
187            String::from("no"),
188            String::from("n"),
189        ];
190
191        let response_values = vec![Answer::YES, Answer::YES, Answer::NO, Answer::NO];
192        let mut valid_responses: HashMap<String, Answer> = response_keys
193            .into_iter()
194            .zip(response_values.into_iter())
195            .collect();
196
197        match self.valid_responses {
198            Some(ref mut hashmap) => for (k, v) in valid_responses.drain() {
199                hashmap.insert(k, v);
200            },
201            None => self.valid_responses = Some(valid_responses),
202        }
203        self
204    }
205
206    /// Set a maximum number of attempts to try and get an
207    /// acceptable answer from the user.
208    ///
209    /// # Examples
210    ///
211    /// The following will ask the user if they would like
212    /// to continue until either "y", "n", "yes", or "no",
213    /// is entered, or until they have entered 3 invalid
214    /// responses.
215    ///
216    /// ```no_run
217    /// # use question::Question;
218    /// Question::new("Do you want to continue?")
219    ///     .yes_no()
220    ///     .tries(3)
221    ///     .ask();
222    /// ```
223    pub fn tries(&mut self, tries: u64) -> &mut Question<R, W> {
224        match tries {
225            0 => self.until_acceptable = true,
226            1 => return self,
227            _ => self.tries = Some(tries),
228        }
229        self
230    }
231
232    /// Never stop asking until the user provides an acceptable
233    /// answer.
234    ///
235    /// # Examples
236    ///
237    /// The following will ask the user if they would like
238    /// to continue until either "y", "n", "yes", or "no",
239    /// is entered.
240    ///
241    /// ```no_run
242    /// # use question::Question;
243    /// Question::new("Do you want to continue?")
244    ///     .yes_no()
245    ///     .until_acceptable()
246    ///     .ask();
247    /// ```
248    pub fn until_acceptable(&mut self) -> &mut Question<R, W> {
249        self.until_acceptable = true;
250        self
251    }
252
253    /// Show the default response to the user that will be
254    /// submitted if they enter an empty string `"\n"`.
255    ///
256    /// # Examples
257    ///
258    /// The following will ask the user if they would like
259    /// to continue until either "y", "n", "yes", or "no",
260    /// is entered. Since no default was set the prompt will
261    /// be displayed with `(y/n)` after it, and if the user
262    /// enters an empty string no answer will be returned
263    /// and they will be re-prompted.
264    ///
265    /// ```no_run
266    /// # use question::Question;
267    /// Question::new("Do you want to continue?")
268    ///     .yes_no()
269    ///     .until_acceptable()
270    ///     .show_defaults()
271    ///     .ask();
272    /// ```
273    ///
274    /// If either `Answer::YES` or `Answer::NO` have been set
275    /// as default then the prompt will be shown with that
276    /// entry capitalized, either `(Y/n)` or `(y/N)`.
277    pub fn show_defaults(&mut self) -> &mut Question<R, W> {
278        self.show_defaults = true;
279        self
280    }
281
282    /// Provide a default answer.
283    ///
284    /// # Examples
285    ///
286    /// The following will ask the user if they would like
287    /// to continue until either "y", "n", "yes", "no", or
288    /// "" an empty string is entered.  If an empty string
289    /// is entered `Answer::YES` will be returned.
290    ///
291    /// ```no_run
292    /// # use question::{Question, Answer};
293    /// Question::new("Do you want to continue?")
294    ///     .yes_no()
295    ///     .until_acceptable()
296    ///     .default(Answer::YES)
297    ///     .show_defaults()
298    ///     .ask();
299    /// ```
300    pub fn default(&mut self, answer: Answer) -> &mut Question<R, W> {
301        self.default = Some(answer);
302        self
303    }
304
305    /// Provide a clarification to be shown if the user does
306    /// not enter an acceptable answer on the first try.
307    ///
308    /// # Examples
309    ///
310    /// The following will ask the user if they would like
311    /// to continue until either "y", "n", "yes", "no", or
312    /// "" an empty string is entered.  If an empty string
313    /// is entered `Answer::YES` will be returned. If the
314    /// user does not enter a valid response on the first
315    /// attempt, the clarification will be added to the
316    /// prompt.
317    ///
318    /// > Please enter either 'yes' or 'no'
319    /// > Do you want to continue? (Y/n)
320    ///
321    /// ```no_run
322    /// # use question::{Question, Answer};
323    /// Question::new("Do you want to continue?")
324    ///     .yes_no()
325    ///     .until_acceptable()
326    ///     .default(Answer::YES)
327    ///     .show_defaults()
328    ///     .clarification("Please enter either 'yes' or 'no'\n")
329    ///     .ask();
330    /// ```
331    pub fn clarification(&mut self, c: &str) -> &mut Question<R, W> {
332        self.clarification = Some(c.into());
333        self
334    }
335
336    /// Ask the user a question exactly as it has been built.
337    ///
338    /// # Examples
339    ///
340    /// The following will return whatever the user enters
341    /// as an `Answer::RESPONSE(String)`.
342    ///
343    /// ```no_run
344    /// # use question::Question;
345    /// Question::new("What is your favorite color?").ask();
346    /// ```
347    pub fn ask(&mut self) -> Option<Answer> {
348        self.build_prompt();
349        if self.until_acceptable {
350            return Some(self.until_valid());
351        }
352        if self.tries.is_some() {
353            return self.max_tries();
354        }
355        match self.get_response() {
356            Ok(answer) => Some(answer),
357            Err(_) => None,
358        }
359    }
360
361    /// Ask a user a yes/no question until an acceptable
362    /// response is given.
363    ///
364    /// # Examples
365    ///
366    /// ```no_run
367    /// # use question::Question;
368    /// Question::new("Continue?").confirm();
369    /// ```
370    pub fn confirm(&mut self) -> Answer {
371        self.yes_no();
372        self.build_prompt();
373        self.until_valid()
374    }
375
376    fn get_response(&mut self) -> Result<Answer, std::io::Error> {
377        let prompt = self.prompt.clone();
378        match self.prompt_user(&prompt) {
379            Ok(ref answer) if (self.default != None) && answer == "" => {
380                Ok(self.default.clone().unwrap())
381            }
382            Ok(answer) => Ok(Answer::RESPONSE(answer)),
383            Err(e) => Err(e),
384        }
385    }
386
387    fn get_valid_response(&mut self) -> Option<Answer> {
388        let prompt = self.prompt.clone();
389        let valid_responses = match self.valid_responses.clone() {
390            Some(thing) => thing,
391            None => panic!(),
392        };
393        if let Ok(response) = self.prompt_user(&prompt) {
394            for key in valid_responses.keys() {
395                if *response.trim().to_lowercase() == *key {
396                    return Some(valid_responses[key].clone());
397                }
398                if let Some(default) = self.default.clone() {
399                    if response == "" {
400                        return Some(default);
401                    }
402                }
403            }
404        }
405        None
406    }
407
408    fn get_acceptable_response(&mut self) -> Option<Answer> {
409        let prompt = self.prompt.clone();
410        let acceptable_responses = match self.acceptable.clone() {
411            Some(thing) => thing,
412            None => panic!(),
413        };
414        if let Ok(response) = self.prompt_user(&prompt) {
415            for acceptable_response in acceptable_responses {
416                if *response.trim().to_lowercase() == acceptable_response {
417                    return Some(Answer::RESPONSE(acceptable_response.clone()));
418                }
419                if let Some(default) = self.default.clone() {
420                    if response == "" {
421                        return Some(default);
422                    }
423                }
424            }
425        }
426        None
427    }
428
429    fn max_tries(&mut self) -> Option<Answer> {
430        let mut attempts = 0;
431        while attempts < self.tries.unwrap() {
432            match self.get_valid_response() {
433                Some(answer) => return Some(answer),
434                None => {
435                    self.build_clarification();
436                    attempts += 1;
437                    continue;
438                }
439            }
440        }
441        None
442    }
443
444    fn until_valid(&mut self) -> Answer {
445        if self.valid_responses.is_some() {
446            loop {
447                match self.get_valid_response() {
448                    Some(answer) => return answer,
449                    None => {
450                        self.build_clarification();
451                        continue;
452                    }
453                }
454            }
455        }
456        if self.acceptable.is_some() {
457            loop {
458                match self.get_acceptable_response() {
459                    Some(answer) => return answer,
460                    None => {
461                        self.build_clarification();
462                        continue;
463                    }
464                }
465            }
466        }
467        panic!("Valid responses must be defined for `until_acceptable()`")
468    }
469
470    fn build_prompt(&mut self) {
471        if self.show_defaults {
472            match self.default {
473                Some(Answer::YES) => self.prompt += " (Y/n)",
474                Some(Answer::NO) => self.prompt += " (y/N)",
475                Some(Answer::RESPONSE(ref s)) => {
476                    self.prompt += " (";
477                    self.prompt += s;
478                    self.prompt += ")";
479                }
480                None => self.prompt += " (y/n)",
481            }
482        }
483        self.prompt += " ";
484    }
485
486    fn build_clarification(&mut self) {
487        if let Some(clarification) = self.clarification.clone() {
488            self.prompt = clarification;
489            self.prompt += "\n";
490            self.prompt += &self.question;
491            self.build_prompt();
492        }
493    }
494
495    fn prompt_user(&mut self, question: &str) -> Result<String, std::io::Error> {
496        let mut input = BufReader::new(&mut self.reader);
497        write!(&mut self.writer, "{}", question)?;
498        std::io::stdout().flush()?;
499        let mut s = String::new();
500        input.read_line(&mut s)?;
501        Ok(String::from(s.trim()))
502    }
503}
504
505/// An answer, the result of asking a `Question`.
506#[derive(Eq, PartialEq, Hash, Clone, Debug)]
507pub enum Answer {
508    /// A more complicated `RESPONSE(String)` that
509    /// can be evaluated in the context of the
510    /// application.
511    RESPONSE(String),
512
513    /// A "yes" answer.
514    ///
515    /// Used to represent any answers that are acceptable
516    /// as a "yes" when asking a yes/no question.
517    YES,
518
519    /// A "no" answer.
520    ///
521    /// Used to represent any answers that are acceptable
522    /// as a "no" when asking a yes/no question.
523    NO,
524}
525
526#[cfg(test)]
527mod tests {
528    use super::*;
529    use std::io::Cursor;
530
531    #[test]
532    fn default_constructor() {
533        let question = "Continue?";
534        let q = Question::new(question);
535        assert_eq!(question, q.question);
536        assert_eq!(question, q.prompt);
537        assert_eq!(None, q.default);
538        assert_eq!(None, q.acceptable);
539        assert_eq!(None, q.valid_responses);
540        assert_eq!(None, q.clarification);
541        assert_eq!(None, q.tries);
542        assert_eq!(false, q.until_acceptable);
543        assert_eq!(false, q.show_defaults);
544        assert_eq!(false, q.yes_no);
545    }
546
547    #[test]
548    fn set_default() {
549        macro_rules! default {
550            ($question:expr, $set:expr, $expected:expr) => {
551                let mut q = Question::new($question);
552                q.default($set);
553                assert_eq!($expected, q.default.unwrap());
554            };
555        }
556        let set = String::from("Yes Please!");
557        let response = String::from("Yes Please!");
558        default!("Continue?", Answer::NO, Answer::NO);
559        default!("Continue?", Answer::YES, Answer::YES);
560        default!(
561            "Continue?",
562            Answer::RESPONSE(set),
563            Answer::RESPONSE(response)
564        );
565    }
566
567    #[test]
568    fn accept() {
569        let mut q = Question::new("Continue?");
570
571        q.accept("y");
572        assert_eq!(vec!["y"], q.acceptable.unwrap());
573
574        let mut q = Question::new("Continue?");
575        q.accept("y");
576        q.accept("yes");
577        assert_eq!(vec!["y", "yes"], q.acceptable.unwrap());
578    }
579
580    #[test]
581    fn accept_ask() {
582        let response = String::from("y");
583        let input = Cursor::new(response.into_bytes());
584        let output = Cursor::new(Vec::new());
585        let actual = Question::with_cursor("y", input, output)
586            .accept("y")
587            .until_acceptable()
588            .ask();
589        assert_eq!(Some(Answer::RESPONSE(String::from("y"))), actual);
590    }
591
592    #[test]
593    fn acceptable() {
594        let mut q = Question::new("Continue?");
595
596        q.acceptable(vec!["y"]);
597        assert_eq!(vec!["y"], q.acceptable.unwrap());
598
599        let mut q = Question::new("Continue?");
600        q.accept("y");
601        q.acceptable(vec!["yes", "n", "no"]);
602        assert_eq!(vec!["y", "yes", "n", "no"], q.acceptable.unwrap());
603    }
604
605    #[test]
606    fn prompt() {
607        macro_rules! prompt {
608            ($question:expr, $user_input:expr) => {
609                let response = String::from($user_input);
610                let input = Cursor::new(response.clone().into_bytes());
611                let mut displayed_output = Cursor::new(Vec::new());
612                let result;
613
614                {
615                    let mut q = Question::with_cursor($question, input, &mut displayed_output);
616                    result = q.prompt_user($question).unwrap();
617                } // end borrow of output before using it
618
619                let output =
620                    String::from_utf8(displayed_output.into_inner()).expect("Not UTF-8");
621                assert_eq!($question, output);
622                assert_eq!(response, result);
623            };
624        }
625        prompt!("what is the meaning to life", "42");
626        prompt!("the universe", "42");
627        prompt!("everything", "42");
628        prompt!("Continue", "yes");
629        prompt!(
630            "What is the only manmade object visable from the moon?",
631            "The Great Wall of China"
632        );
633    }
634
635    #[test]
636    fn basic_confirm() {
637        macro_rules! confirm {
638            ($i:expr, $q:expr, $expected:expr) => {
639                let response = String::from($i);
640                let input = Cursor::new(response.into_bytes());
641                let output = Cursor::new(Vec::new());
642                let actual = Question::with_cursor($q, input, output).confirm();
643                assert_eq!($expected, actual);
644            };
645        }
646        confirm!("y", "Continue?", Answer::YES);
647        confirm!("yes", "Continue?", Answer::YES);
648        confirm!("n", "Continue?", Answer::NO);
649        confirm!("no", "Continue?", Answer::NO);
650    }
651
652    #[test]
653    fn basic_ask() {
654        macro_rules! ask {
655            ($i:expr, $q:expr, $expected:expr) => {
656                let response = String::from($i);
657                let input = Cursor::new(response.into_bytes());
658                let output = Cursor::new(Vec::new());
659                let actual = Question::with_cursor($q, input, output).ask();
660                assert_eq!(Some(Answer::RESPONSE(String::from($expected))), actual);
661            };
662        }
663
664        ask!("y\n", "Continue?", "y");
665        ask!("yes\n", "Continue?", "yes");
666        ask!("n\n", "Continue?", "n");
667        ask!("no\n", "Continue?", "no");
668        ask!("the universe,\n", "42", "the universe,");
669        ask!("and everything\n", "42", "and everything");
670        ask!(
671            "what is the meaning to life,\n",
672            "42",
673            "what is the meaning to life,"
674        );
675
676        ask!("y", "Continue?", "y");
677        ask!("yes", "Continue?", "yes");
678        ask!("n", "Continue?", "n");
679        ask!("no", "Continue?", "no");
680        ask!("the universe,", "42", "the universe,");
681        ask!("and everything", "42", "and everything");
682        ask!(
683            "what is the meaning to life,",
684            "42",
685            "what is the meaning to life,"
686        );
687    }
688
689    #[test]
690    fn set_clarification() {
691        macro_rules! confirm_clarification {
692            ($i:expr, $q:expr, $clarification:expr) => {
693                let response = String::from($i);
694                let input = Cursor::new(response.into_bytes());
695                let output = Cursor::new(Vec::new());
696                let mut q = Question::with_cursor($q, input, output);
697                q.clarification($clarification);
698                assert_eq!($clarification, q.clarification.unwrap());
699            };
700        }
701        confirm_clarification!("what is the meaning to life", "42", "14*3");
702        confirm_clarification!("Continue?", "wat", "Please respond with yes/no");
703    }
704
705    #[test]
706    fn set_max_tries() {
707        macro_rules! confirm_max_tries {
708            ($i:expr, $q:expr, $max_tries:expr) => {
709                let response = String::from($i);
710                let input = Cursor::new(response.into_bytes());
711                let output = Cursor::new(Vec::new());
712                let mut q = Question::with_cursor($q, input, output);
713                q.tries($max_tries);
714                assert_eq!($max_tries, q.tries.unwrap());
715            };
716        }
717        confirm_max_tries!("what is the meaning to life", "42", 42);
718        confirm_max_tries!("Continue?", "wat", 0x79);
719    }
720
721    #[test]
722    fn set_until_acceptable() {
723        macro_rules! confirm_until_acceptable {
724            ($i:expr, $q:expr, $until_acceptable:expr) => {
725                let response = String::from($i);
726                let input = Cursor::new(response.into_bytes());
727                let output = Cursor::new(Vec::new());
728                let mut q = Question::with_cursor($q, input, output);
729                q.until_acceptable();
730                assert_eq!($until_acceptable, q.until_acceptable);
731            };
732        }
733        confirm_until_acceptable!("what is the meaning to life", "42", true);
734    }
735
736    #[test]
737    fn set_show_defaults() {
738        macro_rules! confirm_show_defaults {
739            ($i:expr, $q:expr, $show_defaults:expr) => {
740                let response = String::from($i);
741                let input = Cursor::new(response.into_bytes());
742                let output = Cursor::new(Vec::new());
743                let mut q = Question::with_cursor($q, input, output);
744                q.show_defaults();
745                assert_eq!($show_defaults, q.show_defaults);
746            };
747        }
748        confirm_show_defaults!("what is the meaning to life", "42", true);
749    }
750
751    #[test]
752    fn set_yes_no() {
753        macro_rules! confirm_yes_no {
754            ($i:expr, $q:expr, $yes_no:expr) => {
755                let response = String::from($i);
756                let input = Cursor::new(response.into_bytes());
757                let output = Cursor::new(Vec::new());
758                let mut q = Question::with_cursor($q, input, output);
759                q.yes_no();
760                assert_eq!($yes_no, q.yes_no);
761            };
762        }
763        confirm_yes_no!("what is the meaning to life", "42", true);
764    }
765}