1use crate::backend::{BackendError, InterviewBackend};
2use crate::interview::{Question, QuestionKind};
3use crate::{AnswerValue, Answers};
4
5pub struct RequesttyBackend;
7
8impl RequesttyBackend {
9 pub const fn new() -> Self {
10 Self
11 }
12
13 fn execute_question_with_validator(
14 &self,
15 question: &Question,
16 answers: &mut Answers,
17 validator: &(dyn Fn(&str, &str, &Answers) -> Result<(), String> + Send + Sync),
18 ) -> Result<(), BackendError> {
19 let id = question.id().unwrap_or_else(|| question.name());
20
21 match question.kind() {
22 QuestionKind::Input(input_q) => {
23 let build_question = || {
24 let mut q = requestty::Question::input(id).message(question.prompt());
25
26 if let Some(default) = &input_q.default {
27 q = q.default(default.clone());
28 }
29
30 let answers_for_submit = answers.clone();
31 let answers_for_key = answers.clone();
32 q = q
33 .validate(move |value: &str, _prev_answers| -> Result<(), String> {
34 validator(id, value, &answers_for_submit)
35 })
36 .validate_on_key(move |value: &str, _prev_answers| {
37 validator(id, value, &answers_for_key).is_ok()
38 });
39
40 q.build()
41 };
42
43 loop {
44 match requestty::prompt_one(build_question()) {
45 Ok(requestty::Answer::String(s)) => {
46 if let Err(msg) = validator(id, &s, answers) {
48 println!("{msg}");
49 continue;
50 }
51 answers.insert(id.to_string(), AnswerValue::String(s));
52 break;
53 }
54 Ok(_) => {
55 return Err(BackendError::ExecutionError(
56 "Expected string answer".to_string(),
57 ));
58 }
59 Err(e) => {
60 println!("{e}");
61 continue;
62 }
63 }
64 }
65 }
66 QuestionKind::Multiline(multiline_q) => {
67 let build_question = || {
68 let mut q = requestty::Question::editor(id).message(question.prompt());
69
70 if let Some(default) = &multiline_q.default {
71 q = q.default(default.clone());
72 }
73
74 let answers_for_submit = answers.clone();
75 q = q.validate(move |value: &str, _prev_answers| -> Result<(), String> {
76 validator(id, value, &answers_for_submit)
77 });
78
79 q.build()
80 };
81
82 loop {
83 match requestty::prompt_one(build_question()) {
84 Ok(requestty::Answer::String(s)) => {
85 answers.insert(id.to_string(), AnswerValue::String(s));
86 break;
87 }
88 Ok(_) => {
89 return Err(BackendError::ExecutionError(
90 "Expected string answer".to_string(),
91 ));
92 }
93 Err(e) => {
94 println!("{e}");
95 continue;
96 }
97 }
98 }
99 }
100 QuestionKind::Masked(masked_q) => {
101 let build_question = || {
102 let mut q = requestty::Question::password(id).message(question.prompt());
103
104 if let Some(mask) = masked_q.mask {
105 q = q.mask(mask);
106 }
107
108 let answers_for_submit = answers.clone();
109 q = q.validate(move |value: &str, _prev_answers| -> Result<(), String> {
110 validator(id, value, &answers_for_submit)
111 });
112
113 q.build()
114 };
115
116 loop {
117 match requestty::prompt_one(build_question()) {
118 Ok(requestty::Answer::String(s)) => {
119 answers.insert(id.to_string(), AnswerValue::String(s));
120 break;
121 }
122 Ok(_) => {
123 return Err(BackendError::ExecutionError(
124 "Expected string answer".to_string(),
125 ));
126 }
127 Err(e) => {
128 println!("{e}");
129 continue;
130 }
131 }
132 }
133 }
134 QuestionKind::Sequence(questions) => {
135 if question.kind().is_enum_alternatives() {
137 let choices: Vec<String> =
138 questions.iter().map(|q| q.name().to_string()).collect();
139
140 let q = requestty::Question::select(id)
141 .message(question.prompt())
142 .choices(choices)
143 .default(0)
144 .build();
145
146 let answer = requestty::prompt_one(q).map_err(|e| {
147 BackendError::ExecutionError(format!("Failed to prompt: {e}"))
148 })?;
149
150 let selection = match answer {
151 requestty::Answer::ListItem(item) => item.index,
152 _ => return Err(BackendError::ExecutionError("Expected list item".into())),
153 };
154
155 let parent_prefix = id.strip_suffix(".alternatives");
156 let answer_key = parent_prefix.map_or_else(
157 || {
158 if id == "alternatives" {
159 crate::SELECTED_ALTERNATIVE_KEY.to_string()
160 } else {
161 format!("{}.{}", id, crate::SELECTED_ALTERNATIVE_KEY)
162 }
163 },
164 |prefix| format!("{}.{}", prefix, crate::SELECTED_ALTERNATIVE_KEY),
165 );
166
167 answers.insert(answer_key, AnswerValue::Int(selection as i64));
168
169 let selected_variant = &questions[selection];
170 if let QuestionKind::Alternative(_, fields) = selected_variant.kind() {
171 for field_q in fields {
172 if let Some(prefix) = parent_prefix {
173 let field_id = field_q.id().unwrap_or_else(|| field_q.name());
174 let prefixed_id = format!("{}.{}", prefix, field_id);
175 let prefixed_question = Question::new(
176 Some(prefixed_id.clone()),
177 prefixed_id,
178 field_q.prompt().to_string(),
179 field_q.kind().clone(),
180 );
181 self.execute_question_with_validator(
182 &prefixed_question,
183 answers,
184 validator,
185 )?;
186 } else {
187 self.execute_question_with_validator(field_q, answers, validator)?;
188 }
189 }
190 }
191 } else {
192 for q in questions {
193 self.execute_question_with_validator(q, answers, validator)?;
194 }
195 }
196 }
197 _ => self.execute_question(question, answers)?,
198 }
199
200 Ok(())
201 }
202
203 fn execute_question(
204 &self,
205 question: &Question,
206 answers: &mut Answers,
207 ) -> Result<(), BackendError> {
208 let id = question.id().unwrap_or_else(|| question.name());
209
210 match question.kind() {
211 QuestionKind::Input(input_q) => {
212 let mut q = requestty::Question::input(id).message(question.prompt());
213
214 if let Some(default) = &input_q.default {
215 q = q.default(default.clone());
216 }
217
218 let answer = requestty::prompt_one(q.build())
219 .map_err(|e| BackendError::ExecutionError(format!("Failed to prompt: {e}")))?;
220
221 if let requestty::Answer::String(s) = answer {
222 answers.insert(id.to_string(), AnswerValue::String(s));
223 }
224 }
225 QuestionKind::Multiline(multiline_q) => {
226 let mut q = requestty::Question::editor(id).message(question.prompt());
227
228 if let Some(default) = &multiline_q.default {
229 q = q.default(default.clone());
230 }
231
232 let answer = requestty::prompt_one(q.build())
233 .map_err(|e| BackendError::ExecutionError(format!("Failed to prompt: {e}")))?;
234
235 if let requestty::Answer::String(s) = answer {
236 answers.insert(id.to_string(), AnswerValue::String(s));
237 }
238 }
239 QuestionKind::Masked(masked_q) => {
240 let mut q = requestty::Question::password(id).message(question.prompt());
241
242 if let Some(mask) = masked_q.mask {
243 q = q.mask(mask);
244 }
245
246 let answer = requestty::prompt_one(q.build())
247 .map_err(|e| BackendError::ExecutionError(format!("Failed to prompt: {e}")))?;
248
249 if let requestty::Answer::String(s) = answer {
250 answers.insert(id.to_string(), AnswerValue::String(s));
251 }
252 }
253 QuestionKind::Int(int_q) => {
254 let mut q = requestty::Question::int(id).message(question.prompt());
255
256 if let Some(default) = int_q.default {
257 q = q.default(default);
258 }
259
260 let min = int_q.min;
262 let max = int_q.max;
263 if min.is_some() || max.is_some() {
264 q = q.validate(move |value, _| {
265 if let Some(min_val) = min
266 && value < min_val
267 {
268 return Err(format!("Value must be at least {min_val}"));
269 }
270 if let Some(max_val) = max
271 && value > max_val
272 {
273 return Err(format!("Value must be at most {max_val}"));
274 }
275 Ok(())
276 });
277 }
278
279 let answer = requestty::prompt_one(q.build())
280 .map_err(|e| BackendError::ExecutionError(format!("Failed to prompt: {e}")))?;
281
282 if let requestty::Answer::Int(i) = answer {
283 answers.insert(id.to_string(), AnswerValue::Int(i));
284 }
285 }
286 QuestionKind::Float(float_q) => {
287 let mut q = requestty::Question::float(id).message(question.prompt());
288
289 if let Some(default) = float_q.default {
290 q = q.default(default);
291 }
292
293 let min = float_q.min;
295 let max = float_q.max;
296 if min.is_some() || max.is_some() {
297 q = q.validate(move |value, _| {
298 if let Some(min_val) = min
299 && value < min_val
300 {
301 return Err(format!("Value must be at least {min_val}"));
302 }
303 if let Some(max_val) = max
304 && value > max_val
305 {
306 return Err(format!("Value must be at most {max_val}"));
307 }
308 Ok(())
309 });
310 }
311
312 let answer = requestty::prompt_one(q.build())
313 .map_err(|e| BackendError::ExecutionError(format!("Failed to prompt: {e}")))?;
314
315 if let requestty::Answer::Float(f) = answer {
316 answers.insert(id.to_string(), AnswerValue::Float(f));
317 }
318 }
319 QuestionKind::Confirm(confirm_q) => {
320 let q = requestty::Question::confirm(id)
321 .message(question.prompt())
322 .default(confirm_q.default)
323 .build();
324
325 let answer = requestty::prompt_one(q)
326 .map_err(|e| BackendError::ExecutionError(format!("Failed to prompt: {e}")))?;
327
328 if let requestty::Answer::Bool(b) = answer {
329 answers.insert(id.to_string(), AnswerValue::Bool(b));
330 }
331 }
332 QuestionKind::MultiSelect(multi_q) => {
333 let choices: Vec<_> = multi_q
335 .options
336 .iter()
337 .enumerate()
338 .map(|(idx, opt)| {
339 let selected = multi_q.defaults.contains(&idx);
340 (opt.clone(), selected)
341 })
342 .collect();
343
344 let q = requestty::Question::multi_select(id)
345 .message(question.prompt())
346 .choices_with_default(choices)
347 .build();
348
349 let answer = requestty::prompt_one(q)
350 .map_err(|e| BackendError::ExecutionError(format!("Failed to prompt: {e}")))?;
351
352 if let requestty::Answer::ListItems(items) = answer {
353 let indices: Vec<i64> =
354 items.into_iter().map(|item| item.index as i64).collect();
355 answers.insert(id.to_string(), AnswerValue::IntList(indices));
356 }
357 }
358 QuestionKind::Sequence(questions) => {
359 if question.kind().is_enum_alternatives() {
360 let choices: Vec<String> =
362 questions.iter().map(|q| q.name().to_string()).collect();
363
364 let q = requestty::Question::select(id)
365 .message(question.prompt())
366 .choices(choices)
367 .default(0)
368 .build();
369
370 let answer = requestty::prompt_one(q).map_err(|e| {
371 BackendError::ExecutionError(format!("Failed to prompt: {e}"))
372 })?;
373
374 let selection = match answer {
375 requestty::Answer::ListItem(item) => item.index,
376 _ => return Err(BackendError::ExecutionError("Expected list item".into())),
377 };
378
379 let parent_prefix = id.strip_suffix(".alternatives");
385
386 let answer_key = parent_prefix.map_or_else(
387 || {
388 if id == "alternatives" {
389 crate::SELECTED_ALTERNATIVE_KEY.to_string()
390 } else {
391 format!("{}.{}", id, crate::SELECTED_ALTERNATIVE_KEY)
393 }
394 },
395 |prefix| format!("{}.{}", prefix, crate::SELECTED_ALTERNATIVE_KEY),
396 );
397
398 answers.insert(answer_key, AnswerValue::Int(selection as i64));
399
400 let selected_variant = &questions[selection];
403 if let QuestionKind::Alternative(_, fields) = selected_variant.kind() {
404 for field_q in fields {
405 if let Some(prefix) = parent_prefix {
407 let field_id = field_q.id().unwrap_or_else(|| field_q.name());
408 let prefixed_id = format!("{}.{}", prefix, field_id);
409 let prefixed_question = Question::new(
410 Some(prefixed_id.clone()),
411 prefixed_id,
412 field_q.prompt().to_string(),
413 field_q.kind().clone(),
414 );
415 self.execute_question(&prefixed_question, answers)?;
416 } else {
417 self.execute_question(field_q, answers)?;
418 }
419 }
420 }
421 } else {
422 for q in questions {
424 self.execute_question(q, answers)?;
425 }
426 }
427 }
428 QuestionKind::Alternative(default_idx, alternatives) => {
429 let choices: Vec<String> = alternatives
431 .iter()
432 .map(|alt| alt.name().to_string())
433 .collect();
434
435 let q = requestty::Question::select(id)
436 .message(question.prompt())
437 .choices(choices)
438 .default(*default_idx)
439 .build();
440
441 let answer = requestty::prompt_one(q)
442 .map_err(|e| BackendError::ExecutionError(format!("Failed to prompt: {e}")))?;
443
444 let selected_idx = match answer {
445 requestty::Answer::ListItem(item) => item.index,
446 _ => return Err(BackendError::ExecutionError("Expected list item".into())),
447 };
448
449 answers.insert(
451 crate::SELECTED_ALTERNATIVE_KEY.to_string(),
452 AnswerValue::Int(selected_idx as i64),
453 );
454
455 if let QuestionKind::Alternative(_, alts) = alternatives[selected_idx].kind() {
457 for q in alts {
458 self.execute_question(q, answers)?;
459 }
460 }
461 }
462 }
463
464 Ok(())
465 }
466}
467
468impl Default for RequesttyBackend {
469 fn default() -> Self {
470 Self::new()
471 }
472}
473
474impl InterviewBackend for RequesttyBackend {
475 fn execute(&self, interview: &crate::interview::Interview) -> Result<Answers, BackendError> {
476 if let Some(prelude) = &interview.prelude {
478 println!("{}", prelude);
479 println!();
480 }
481
482 let mut answers = Answers::new();
483
484 for question in &interview.sections {
485 if let Some(assumed) = question.assumed() {
487 answers.insert(question.name().to_string(), assumed.into());
488 continue;
489 }
490
491 self.execute_question(question, &mut answers)?;
492 }
493
494 if let Some(epilogue) = &interview.epilogue {
496 println!();
497 println!("{}", epilogue);
498 }
499
500 Ok(answers)
501 }
502
503 fn execute_with_validator(
504 &self,
505 interview: &crate::interview::Interview,
506 validator: &(dyn Fn(&str, &str, &Answers) -> Result<(), String> + Send + Sync),
507 ) -> Result<Answers, BackendError> {
508 if let Some(prelude) = &interview.prelude {
510 println!("{}", prelude);
511 println!();
512 }
513
514 let mut answers = Answers::new();
515
516 for question in &interview.sections {
517 if let Some(assumed) = question.assumed() {
519 answers.insert(question.name().to_string(), assumed.into());
520 continue;
521 }
522
523 self.execute_question_with_validator(question, &mut answers, validator)?;
525 }
526
527 if let Some(epilogue) = &interview.epilogue {
529 println!();
530 println!("{}", epilogue);
531 }
532
533 Ok(answers)
534 }
535}