1use serde_json::Value;
8use std::collections::HashMap;
9use std::sync::Arc;
10use std::time::Duration;
11
12pub const QUESTION_WAIT: Duration = Duration::from_secs(1800);
14
15pub const REJECTED_MESSAGE: &str = "The user dismissed this question";
17
18#[derive(Debug, Clone)]
19pub enum Outcome {
20 Answers(Vec<Vec<String>>),
22 Rejected,
24}
25
26struct Pending {
27 request: Value,
28 tx: tokio::sync::oneshot::Sender<Outcome>,
29}
30
31#[derive(Default)]
33pub struct QuestionGate {
34 pending: parking_lot::Mutex<HashMap<String, Pending>>,
35}
36
37pub struct PendingGuard {
40 gate: Arc<QuestionGate>,
41 id: String,
42}
43
44impl Drop for PendingGuard {
45 fn drop(&mut self) {
46 self.gate.pending.lock().remove(&self.id);
47 }
48}
49
50impl QuestionGate {
51 pub fn new() -> Arc<Self> {
52 Arc::new(Self::default())
53 }
54
55 pub fn register(
57 self: &Arc<Self>,
58 id: &str,
59 request: Value,
60 ) -> (tokio::sync::oneshot::Receiver<Outcome>, PendingGuard) {
61 let (tx, rx) = tokio::sync::oneshot::channel();
62 self.pending
63 .lock()
64 .insert(id.to_string(), Pending { request, tx });
65 let guard = PendingGuard {
66 gate: Arc::clone(self),
67 id: id.to_string(),
68 };
69 (rx, guard)
70 }
71
72 pub fn reply(&self, id: &str, answers: Vec<Vec<String>>) -> bool {
75 match self.pending.lock().remove(id) {
76 Some(p) => p.tx.send(Outcome::Answers(answers)).is_ok(),
77 None => false,
78 }
79 }
80
81 pub fn reject(&self, id: &str) -> bool {
83 match self.pending.lock().remove(id) {
84 Some(p) => p.tx.send(Outcome::Rejected).is_ok(),
85 None => false,
86 }
87 }
88
89 pub fn remove(&self, id: &str) {
91 self.pending.lock().remove(id);
92 }
93
94 pub fn list(&self) -> Vec<Value> {
96 self.pending
97 .lock()
98 .values()
99 .map(|p| p.request.clone())
100 .collect()
101 }
102
103 pub async fn wait(&self, id: &str, rx: tokio::sync::oneshot::Receiver<Outcome>) -> Outcome {
105 match tokio::time::timeout(QUESTION_WAIT, rx).await {
106 Ok(Ok(outcome)) => outcome,
107 Ok(Err(_)) => Outcome::Rejected, Err(_) => {
109 self.remove(id);
110 Outcome::Rejected
111 }
112 }
113 }
114}
115
116pub fn format_output(questions: &Value, answers: &[Vec<String>]) -> String {
118 let items: Vec<String> = questions
119 .as_array()
120 .map(|qs| {
121 qs.iter()
122 .enumerate()
123 .map(|(i, q)| {
124 let answer = answers
125 .get(i)
126 .map(|a| a.join(", "))
127 .filter(|s| !s.is_empty())
128 .unwrap_or_else(|| "Unanswered".to_string());
129 format!(
130 "\"{}\"=\"{}\"",
131 q["question"].as_str().unwrap_or_default(),
132 answer
133 )
134 })
135 .collect()
136 })
137 .unwrap_or_default();
138 format!(
139 "User has answered your questions: {}. You can now continue with the user's answers in mind.",
140 items.join(", ")
141 )
142}
143
144pub fn format_title(questions: &Value) -> String {
146 let n = questions.as_array().map(|a| a.len()).unwrap_or(0);
147 format!("Asked {} question{}", n, if n > 1 { "s" } else { "" })
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153 use serde_json::json;
154
155 #[test]
156 fn output_matches_v1_template() {
157 let qs = json!([
158 {"question": "Pick a color", "header": "Color",
159 "options": [{"label": "Red", "description": "r"}]},
160 {"question": "Pick speed", "header": "Speed",
161 "options": [{"label": "Fast", "description": "f"}]}
162 ]);
163 let answers = vec![vec!["Red".to_string()], vec![]];
164 assert_eq!(
165 format_output(&qs, &answers),
166 "User has answered your questions: \"Pick a color\"=\"Red\", \
167 \"Pick speed\"=\"Unanswered\". You can now continue with the user's answers in mind."
168 );
169 assert_eq!(format_title(&qs), "Asked 2 questions");
170 assert_eq!(format_title(&json!([])), "Asked 0 question"); }
172
173 #[tokio::test]
174 async fn register_reply_list_roundtrip() {
175 let gate = QuestionGate::new();
176 let req = json!({"id": "que_1", "sessionID": "ses_1", "questions": []});
177 let (rx, _guard) = gate.register("que_1", req.clone());
178 assert_eq!(gate.list(), vec![req.clone()]);
179 assert!(gate.reply("que_1", vec![vec!["a".into()]]));
180 assert!(gate.list().is_empty(), "entry removed on reply");
181 match rx.await.unwrap() {
182 Outcome::Answers(a) => assert_eq!(a, vec![vec!["a".to_string()]]),
183 _ => panic!("expected answers"),
184 }
185 assert!(!gate.reply("que_1", vec![]));
187 }
188
189 #[tokio::test]
190 async fn reject_resolves_and_unknown_is_false() {
191 let gate = QuestionGate::new();
192 let (rx, _guard) = gate.register("que_2", json!({"id": "que_2"}));
193 assert!(gate.reject("que_2"));
194 assert!(matches!(rx.await.unwrap(), Outcome::Rejected));
195 assert!(!gate.reject("que_missing"));
196 assert!(!gate.reply("que_missing", vec![]));
197 }
198
199 #[tokio::test(start_paused = true)]
200 async fn wait_timeout_rejects_and_cleans_pending() {
201 let gate = QuestionGate::new();
202 let (rx, _guard) = gate.register("que_3", json!({"id": "que_3"}));
203 assert_eq!(gate.list().len(), 1);
204 tokio::time::advance(QUESTION_WAIT + Duration::from_secs(1)).await;
206 let outcome = gate.wait("que_3", rx).await;
207 assert!(matches!(outcome, Outcome::Rejected));
208 assert!(gate.list().is_empty(), "timeout cleans the pending map");
209 }
210
211 #[tokio::test]
212 async fn guard_drop_cleans_pending_on_abort() {
213 let gate = QuestionGate::new();
214 {
215 let (_rx, _guard) = gate.register("que_4", json!({"id": "que_4"}));
216 assert_eq!(gate.list().len(), 1);
217 } assert!(
219 gate.list().is_empty(),
220 "drop removes pending (upstream ensuring)"
221 );
222 }
223}