1mod 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
16pub 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}