Skip to main content

promptt/
lib.rs

1//! Interactive CLI prompts: text, confirm, select.
2//!
3//! Use `prompt()` for sequential questions or `run_prompt()` for a single question.
4
5mod elements;
6mod prompts;
7mod util;
8
9pub use elements::{Choice, ConfirmPromptOptions, SelectPromptOptions, TextPromptOptions};
10pub use prompts::{PromptValue, Question, run_prompt};
11pub use util::Figures;
12
13use std::collections::HashMap;
14use std::io::{self, BufRead, Write};
15
16/// Runs questions in sequence. Returns a name-to-value map. I/O via stdin/stdout.
17pub fn prompt<R: BufRead, W: Write>(
18    questions: &[Question],
19    stdin: &mut R,
20    stdout: &mut W,
21) -> io::Result<HashMap<String, PromptValue>> {
22    let mut answers = HashMap::with_capacity(questions.len());
23    for q in questions {
24        if q.type_name.is_empty() {
25            continue;
26        }
27        if q.message.is_empty() {
28            return Err(io::Error::new(
29                io::ErrorKind::InvalidInput,
30                "prompt message is required",
31            ));
32        }
33        match run_prompt(q, stdin, stdout) {
34            Ok(Some(value)) => {
35                answers.insert(q.name.clone(), value);
36            }
37            Ok(None) => {}
38            Err(e) => return Err(e),
39        }
40    }
41    Ok(answers)
42}
43
44#[cfg(test)]
45mod tests {
46    use super::*;
47    use std::io::Cursor;
48
49    #[test]
50    fn test_figures() {
51        let f = Figures::default();
52        assert!(!f.ellipsis.is_empty());
53        assert!(!f.pointer_small.is_empty());
54    }
55
56    #[test]
57    fn test_question_default() {
58        let q = Question::default();
59        assert!(q.type_name.is_empty());
60        assert!(q.message.is_empty());
61    }
62
63    #[test]
64    fn prompt_skips_empty_type_name() {
65        let questions = vec![Question {
66            name: "skip".into(),
67            type_name: String::new(),
68            message: "Skipped?".into(),
69            ..Default::default()
70        }];
71        let mut stdin = Cursor::new(b"");
72        let mut stdout = Vec::new();
73        let r = prompt(&questions, &mut stdin, &mut stdout);
74        assert!(r.is_ok());
75        let answers = r.unwrap();
76        assert!(answers.is_empty());
77    }
78
79    #[test]
80    fn prompt_requires_message() {
81        let questions = vec![Question {
82            name: "x".into(),
83            type_name: "text".into(),
84            message: String::new(),
85            ..Default::default()
86        }];
87        let mut stdin = Cursor::new(b"");
88        let mut stdout = Vec::new();
89        let r = prompt(&questions, &mut stdin, &mut stdout);
90        assert!(r.is_err());
91    }
92
93    #[test]
94    fn prompt_collects_answers() {
95        let questions = vec![
96            Question {
97                name: "name".into(),
98                type_name: "text".into(),
99                message: "Name?".into(),
100                ..Default::default()
101            },
102            Question {
103                name: "ok".into(),
104                type_name: "confirm".into(),
105                message: "Ok?".into(),
106                initial_bool: Some(false),
107                ..Default::default()
108            },
109        ];
110        let mut stdin = Cursor::new(b"Alice\ny\n");
111        let mut stdout = Vec::new();
112        let r = prompt(&questions, &mut stdin, &mut stdout);
113        assert!(r.is_ok());
114        let answers = r.unwrap();
115        assert_eq!(answers.len(), 2);
116        match answers.get("name") {
117            Some(PromptValue::String(s)) => assert_eq!(s, "Alice"),
118            _ => panic!("expected name to be String(\"Alice\")"),
119        }
120        match answers.get("ok") {
121            Some(PromptValue::Bool(b)) => assert!(*b),
122            _ => panic!("expected ok to be Bool(true)"),
123        }
124    }
125
126    #[test]
127    fn prompt_returns_err_on_invalid_question_type() {
128        let questions = vec![Question {
129            name: "x".into(),
130            type_name: "invalid_type".into(),
131            message: "Msg".into(),
132            ..Default::default()
133        }];
134        let mut stdin = Cursor::new(b"");
135        let mut stdout = Vec::new();
136        let r = prompt(&questions, &mut stdin, &mut stdout);
137        assert!(r.is_err());
138    }
139
140    #[test]
141    fn prompt_multiple_empty_type_names_skip_all() {
142        let questions = vec![
143            Question {
144                name: "a".into(),
145                type_name: String::new(),
146                message: "A?".into(),
147                ..Default::default()
148            },
149            Question {
150                name: "b".into(),
151                type_name: String::new(),
152                message: "B?".into(),
153                ..Default::default()
154            },
155        ];
156        let mut stdin = Cursor::new(b"");
157        let mut stdout = Vec::new();
158        let r = prompt(&questions, &mut stdin, &mut stdout);
159        assert!(r.is_ok());
160        assert!(r.unwrap().is_empty());
161    }
162
163    #[test]
164    fn prompt_answer_keys_match_question_names() {
165        let questions = vec![
166            Question {
167                name: "first".into(),
168                type_name: "text".into(),
169                message: "First?".into(),
170                ..Default::default()
171            },
172            Question {
173                name: "second".into(),
174                type_name: "text".into(),
175                message: "Second?".into(),
176                ..Default::default()
177            },
178        ];
179        let mut stdin = Cursor::new(b"one\ntwo\n");
180        let mut stdout = Vec::new();
181        let r = prompt(&questions, &mut stdin, &mut stdout);
182        assert!(r.is_ok());
183        let answers = r.unwrap();
184        assert_eq!(
185            answers.get("first"),
186            Some(&PromptValue::String("one".into()))
187        );
188        assert_eq!(
189            answers.get("second"),
190            Some(&PromptValue::String("two".into()))
191        );
192    }
193
194    #[test]
195    fn prompt_first_question_invalid_type_returns_err_immediately() {
196        let questions = vec![
197            Question {
198                name: "x".into(),
199                type_name: "bad".into(),
200                message: "X?".into(),
201                ..Default::default()
202            },
203            Question {
204                name: "y".into(),
205                type_name: "text".into(),
206                message: "Y?".into(),
207                ..Default::default()
208            },
209        ];
210        let mut stdin = Cursor::new(b"ignored\n");
211        let mut stdout = Vec::new();
212        let r = prompt(&questions, &mut stdin, &mut stdout);
213        assert!(r.is_err());
214    }
215}