scud/attractor/
interviewer.rs1use async_trait::async_trait;
4
5pub struct Question {
7 pub text: String,
9 pub choices: Vec<String>,
11 pub default: Option<usize>,
13}
14
15pub struct Answer {
17 pub text: String,
19 pub index: Option<usize>,
21}
22
23#[async_trait]
25pub trait Interviewer: Send + Sync {
26 async fn ask(&self, question: Question) -> Answer;
28}
29
30pub struct ConsoleInterviewer;
32
33#[async_trait]
34impl Interviewer for ConsoleInterviewer {
35 async fn ask(&self, question: Question) -> Answer {
36 use std::io::{self, Write};
37
38 println!("\n{}", question.text);
39 for (i, choice) in question.choices.iter().enumerate() {
40 let marker = if Some(i) == question.default {
41 "*"
42 } else {
43 " "
44 };
45 println!(" {} [{}] {}", marker, i + 1, choice);
46 }
47
48 print!("> ");
49 let _ = io::stdout().flush();
50
51 let mut input = String::new();
52 let _ = io::stdin().read_line(&mut input);
53 let input = input.trim();
54
55 if let Ok(num) = input.parse::<usize>() {
57 if num >= 1 && num <= question.choices.len() {
58 return Answer {
59 text: question.choices[num - 1].clone(),
60 index: Some(num - 1),
61 };
62 }
63 }
64
65 Answer {
67 text: input.to_string(),
68 index: None,
69 }
70 }
71}
72
73pub struct AutoApproveInterviewer;
77
78#[async_trait]
79impl Interviewer for AutoApproveInterviewer {
80 async fn ask(&self, question: Question) -> Answer {
81 let (text, index) = if let Some(default) = question.default {
82 (question.choices[default].clone(), Some(default))
83 } else if !question.choices.is_empty() {
84 (question.choices[0].clone(), Some(0))
85 } else {
86 ("yes".to_string(), None)
87 };
88
89 Answer { text, index }
90 }
91}
92
93pub struct QueueInterviewer {
95 answers: std::sync::Mutex<Vec<Answer>>,
96}
97
98impl QueueInterviewer {
99 pub fn new(answers: Vec<Answer>) -> Self {
101 Self {
102 answers: std::sync::Mutex::new(answers),
103 }
104 }
105
106 pub fn from_strings(answers: Vec<&str>) -> Self {
108 Self::new(
109 answers
110 .into_iter()
111 .map(|s| Answer {
112 text: s.to_string(),
113 index: None,
114 })
115 .collect(),
116 )
117 }
118}
119
120#[async_trait]
121impl Interviewer for QueueInterviewer {
122 async fn ask(&self, question: Question) -> Answer {
123 let mut queue = self.answers.lock().unwrap();
124 if queue.is_empty() {
125 if !question.choices.is_empty() {
127 Answer {
128 text: question.choices[0].clone(),
129 index: Some(0),
130 }
131 } else {
132 Answer {
133 text: "yes".to_string(),
134 index: None,
135 }
136 }
137 } else {
138 queue.remove(0)
139 }
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 #[tokio::test]
148 async fn test_auto_approve_first_choice() {
149 let interviewer = AutoApproveInterviewer;
150 let answer = interviewer
151 .ask(Question {
152 text: "Choose".into(),
153 choices: vec!["A".into(), "B".into()],
154 default: None,
155 })
156 .await;
157 assert_eq!(answer.text, "A");
158 assert_eq!(answer.index, Some(0));
159 }
160
161 #[tokio::test]
162 async fn test_auto_approve_default() {
163 let interviewer = AutoApproveInterviewer;
164 let answer = interviewer
165 .ask(Question {
166 text: "Choose".into(),
167 choices: vec!["A".into(), "B".into()],
168 default: Some(1),
169 })
170 .await;
171 assert_eq!(answer.text, "B");
172 assert_eq!(answer.index, Some(1));
173 }
174
175 #[tokio::test]
176 async fn test_queue_interviewer() {
177 let interviewer = QueueInterviewer::from_strings(vec!["first", "second"]);
178
179 let a1 = interviewer
180 .ask(Question {
181 text: "Q1?".into(),
182 choices: vec![],
183 default: None,
184 })
185 .await;
186 assert_eq!(a1.text, "first");
187
188 let a2 = interviewer
189 .ask(Question {
190 text: "Q2?".into(),
191 choices: vec![],
192 default: None,
193 })
194 .await;
195 assert_eq!(a2.text, "second");
196 }
197
198 #[tokio::test]
199 async fn test_queue_interviewer_fallback() {
200 let interviewer = QueueInterviewer::from_strings(vec![]);
201 let answer = interviewer
202 .ask(Question {
203 text: "Q?".into(),
204 choices: vec!["option".into()],
205 default: None,
206 })
207 .await;
208 assert_eq!(answer.text, "option");
209 }
210}