1use rmcp::schemars;
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, schemars::JsonSchema)]
16pub struct AskUserRequest {
17 #[schemars(
18 description = "List of questions to ask the user. Each question has a label, question text, and options."
19 )]
20 pub questions: Vec<AskUserQuestion>,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, schemars::JsonSchema)]
25pub struct AskUserQuestion {
26 #[schemars(description = "Short unique label for tab display (max ~15 chars recommended)")]
27 pub label: String,
28 #[schemars(description = "Full question text to display")]
29 pub question: String,
30 #[schemars(description = "Predefined answer options")]
31 pub options: Vec<AskUserOption>,
32 #[serde(default = "default_true")]
34 #[schemars(description = "Whether to allow custom text input (default: true)")]
35 pub allow_custom: bool,
36 #[serde(default)]
38 #[schemars(
39 description = "When true, user can select/deselect multiple options (checkbox list). Default: false (single-select radio behavior)."
40 )]
41 pub multi_select: bool,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, schemars::JsonSchema)]
46pub struct AskUserOption {
47 #[schemars(description = "Value to return to LLM when selected")]
48 pub value: String,
49 #[schemars(description = "Display label for the option")]
50 pub label: String,
51 #[serde(skip_serializing_if = "Option::is_none")]
53 #[schemars(description = "Optional description shown below the label")]
54 pub description: Option<String>,
55 #[serde(default)]
57 #[schemars(
58 description = "Default selection state when multi_select is true. Pre-marks this option as selected. Ignored for single-select questions."
59 )]
60 pub selected: bool,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, schemars::JsonSchema)]
69pub struct AskUserAnswer {
70 pub question_label: String,
72 pub answer: String,
75 pub is_custom: bool,
77 #[serde(default, skip_serializing_if = "Vec::is_empty")]
79 pub selected_values: Vec<String>,
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, schemars::JsonSchema)]
84pub struct AskUserResult {
85 pub answers: Vec<AskUserAnswer>,
87 pub completed: bool,
89 #[serde(skip_serializing_if = "Option::is_none")]
91 pub reason: Option<String>,
92}
93
94fn default_true() -> bool {
99 true
100}
101
102#[cfg(test)]
107mod tests {
108 use super::*;
109
110 #[test]
111 fn test_question_serialization() {
112 let question = AskUserQuestion {
113 label: "Environment".to_string(),
114 question: "Which environment should I deploy to?".to_string(),
115 options: vec![
116 AskUserOption {
117 value: "dev".to_string(),
118 label: "Development".to_string(),
119 description: Some("For testing changes".to_string()),
120 selected: false,
121 },
122 AskUserOption {
123 value: "prod".to_string(),
124 label: "Production".to_string(),
125 description: None,
126 selected: false,
127 },
128 ],
129 allow_custom: true,
130 multi_select: false,
131 };
132
133 let json = serde_json::to_string(&question).unwrap();
134 assert!(json.contains("\"label\":\"Environment\""));
135 assert!(json.contains("\"value\":\"dev\""));
136 assert!(json.contains("\"description\":\"For testing changes\""));
137 assert!(!json.contains("\"description\":null"));
139 }
140
141 #[test]
142 fn test_question_deserialization_with_defaults() {
143 let json = r#"{
144 "label": "Test",
145 "question": "Is this a test?",
146 "options": []
147 }"#;
148
149 let question: AskUserQuestion = serde_json::from_str(json).unwrap();
150 assert_eq!(question.label, "Test");
151 assert!(question.allow_custom, "allow_custom should default to true");
152 }
153
154 #[test]
155 fn test_question_deserialization_explicit_false() {
156 let json = r#"{
157 "label": "Test",
158 "question": "Is this a test?",
159 "options": [],
160 "allow_custom": false
161 }"#;
162
163 let question: AskUserQuestion = serde_json::from_str(json).unwrap();
164 assert!(!question.allow_custom);
165 }
166
167 #[test]
168 fn test_answer_serialization() {
169 let answer = AskUserAnswer {
170 question_label: "Environment".to_string(),
171 answer: "production".to_string(),
172 is_custom: false,
173 selected_values: vec![],
174 };
175
176 let json = serde_json::to_string(&answer).unwrap();
177 assert!(json.contains("\"question_label\":\"Environment\""));
178 assert!(json.contains("\"answer\":\"production\""));
179 assert!(json.contains("\"is_custom\":false"));
180 }
181
182 #[test]
183 fn test_answer_custom_input() {
184 let answer = AskUserAnswer {
185 question_label: "Feedback".to_string(),
186 answer: "User typed this custom response".to_string(),
187 is_custom: true,
188 selected_values: vec![],
189 };
190
191 let json = serde_json::to_string(&answer).unwrap();
192 assert!(json.contains("\"is_custom\":true"));
193 assert!(json.contains("User typed this custom response"));
194 }
195
196 #[test]
197 fn test_result_completed() {
198 let result = AskUserResult {
199 answers: vec![
200 AskUserAnswer {
201 question_label: "q1".to_string(),
202 answer: "a1".to_string(),
203 is_custom: false,
204 selected_values: vec![],
205 },
206 AskUserAnswer {
207 question_label: "q2".to_string(),
208 answer: "custom answer".to_string(),
209 is_custom: true,
210 selected_values: vec![],
211 },
212 ],
213 completed: true,
214 reason: None,
215 };
216
217 let json = serde_json::to_string(&result).unwrap();
218 assert!(json.contains("\"completed\":true"));
219 assert!(!json.contains("\"reason\""));
221 assert!(json.contains("\"question_label\":\"q1\""));
222 assert!(json.contains("\"question_label\":\"q2\""));
223 }
224
225 #[test]
226 fn test_result_cancelled() {
227 let result = AskUserResult {
228 answers: vec![],
229 completed: false,
230 reason: Some("User cancelled the question prompt.".to_string()),
231 };
232
233 let json = serde_json::to_string(&result).unwrap();
234 assert!(json.contains("\"completed\":false"));
235 assert!(json.contains("\"reason\":\"User cancelled the question prompt.\""));
236 assert!(json.contains("\"answers\":[]"));
237 }
238
239 #[test]
240 fn test_result_deserialization() {
241 let json = r#"{
242 "answers": [
243 {"question_label": "env", "answer": "dev", "is_custom": false}
244 ],
245 "completed": true
246 }"#;
247
248 let result: AskUserResult = serde_json::from_str(json).unwrap();
249 assert!(result.completed);
250 assert!(result.reason.is_none());
251 assert_eq!(result.answers.len(), 1);
252 assert_eq!(result.answers[0].question_label, "env");
253 assert_eq!(result.answers[0].answer, "dev");
254 assert!(!result.answers[0].is_custom);
255 }
256
257 #[test]
258 fn test_option_without_description() {
259 let option = AskUserOption {
260 value: "yes".to_string(),
261 label: "Yes".to_string(),
262 description: None,
263 selected: false,
264 };
265
266 let json = serde_json::to_string(&option).unwrap();
267 assert!(!json.contains("description"));
269 assert!(json.contains("\"value\":\"yes\""));
270 assert!(json.contains("\"label\":\"Yes\""));
271 }
272
273 #[test]
274 fn test_unicode_handling() {
275 let question = AskUserQuestion {
276 label: "言語".to_string(),
277 question: "どの言語を使用しますか?".to_string(),
278 options: vec![
279 AskUserOption {
280 value: "ja".to_string(),
281 label: "日本語".to_string(),
282 description: Some("Japanese language".to_string()),
283 selected: false,
284 },
285 AskUserOption {
286 value: "emoji".to_string(),
287 label: "🚀 Rocket".to_string(),
288 description: Some("With emoji 🎉".to_string()),
289 selected: false,
290 },
291 ],
292 allow_custom: true,
293 multi_select: false,
294 };
295
296 let json = serde_json::to_string(&question).unwrap();
297 let parsed: AskUserQuestion = serde_json::from_str(&json).unwrap();
298
299 assert_eq!(parsed.label, "言語");
300 assert_eq!(parsed.question, "どの言語を使用しますか?");
301 assert_eq!(parsed.options[0].label, "日本語");
302 assert_eq!(parsed.options[1].label, "🚀 Rocket");
303 }
304
305 #[test]
306 fn test_types_equality() {
307 let q1 = AskUserQuestion {
308 label: "Test".to_string(),
309 question: "Question?".to_string(),
310 options: vec![],
311 allow_custom: true,
312 multi_select: false,
313 };
314
315 let q2 = q1.clone();
316 assert_eq!(q1, q2);
317
318 let a1 = AskUserAnswer {
319 question_label: "Test".to_string(),
320 answer: "answer".to_string(),
321 is_custom: false,
322 selected_values: vec![],
323 };
324
325 let a2 = a1.clone();
326 assert_eq!(a1, a2);
327
328 let r1 = AskUserResult {
329 answers: vec![a1],
330 completed: true,
331 reason: None,
332 };
333
334 let r2 = r1.clone();
335 assert_eq!(r1, r2);
336 }
337
338 #[test]
339 fn test_request_round_trip() {
340 let request = AskUserRequest {
341 questions: vec![AskUserQuestion {
342 label: "Env".to_string(),
343 question: "Which env?".to_string(),
344 options: vec![AskUserOption {
345 value: "dev".to_string(),
346 label: "Dev".to_string(),
347 description: None,
348 selected: false,
349 }],
350 allow_custom: false,
351 multi_select: false,
352 }],
353 };
354
355 let json = serde_json::to_string(&request).unwrap();
356 let parsed: AskUserRequest = serde_json::from_str(&json).unwrap();
357 assert_eq!(request, parsed);
358 }
359
360 #[test]
361 fn test_multi_select_defaults() {
362 let json = r#"{
363 "label": "Scope",
364 "question": "Which repos?",
365 "options": [
366 {"value": "a", "label": "Repo A"},
367 {"value": "b", "label": "Repo B", "selected": true}
368 ]
369 }"#;
370
371 let question: AskUserQuestion = serde_json::from_str(json).unwrap();
372 assert!(
373 !question.multi_select,
374 "multi_select should default to false"
375 );
376 assert!(
377 !question.options[0].selected,
378 "selected should default to false"
379 );
380 assert!(
381 question.options[1].selected,
382 "selected should be true when set"
383 );
384 }
385
386 #[test]
387 fn test_multi_select_question_round_trip() {
388 let question = AskUserQuestion {
389 label: "Scope".to_string(),
390 question: "Which repos should I include?".to_string(),
391 options: vec![
392 AskUserOption {
393 value: "repo:api".to_string(),
394 label: "~/projects/api".to_string(),
395 description: None,
396 selected: true,
397 },
398 AskUserOption {
399 value: "repo:web".to_string(),
400 label: "~/projects/web".to_string(),
401 description: None,
402 selected: false,
403 },
404 ],
405 allow_custom: false,
406 multi_select: true,
407 };
408
409 let json = serde_json::to_string(&question).unwrap();
410 assert!(json.contains("\"multi_select\":true"));
411 assert!(json.contains("\"selected\":true"));
412
413 let parsed: AskUserQuestion = serde_json::from_str(&json).unwrap();
414 assert_eq!(question, parsed);
415 }
416
417 #[test]
418 fn test_multi_select_answer_with_selected_values() {
419 let answer = AskUserAnswer {
420 question_label: "Scope".to_string(),
421 answer: "[\"repo:api\",\"repo:web\"]".to_string(),
422 is_custom: false,
423 selected_values: vec!["repo:api".to_string(), "repo:web".to_string()],
424 };
425
426 let json = serde_json::to_string(&answer).unwrap();
427 assert!(json.contains("\"selected_values\""));
428 assert!(json.contains("repo:api"));
429 assert!(json.contains("repo:web"));
430
431 let parsed: AskUserAnswer = serde_json::from_str(&json).unwrap();
432 assert_eq!(parsed.selected_values.len(), 2);
433 }
434
435 #[test]
436 fn test_selected_values_omitted_when_empty() {
437 let answer = AskUserAnswer {
438 question_label: "Env".to_string(),
439 answer: "dev".to_string(),
440 is_custom: false,
441 selected_values: vec![],
442 };
443
444 let json = serde_json::to_string(&answer).unwrap();
445 assert!(
446 !json.contains("selected_values"),
447 "selected_values should be omitted when empty"
448 );
449 }
450
451 #[test]
452 fn test_answer_deserialization_without_selected_values() {
453 let json = r#"{"question_label": "env", "answer": "dev", "is_custom": false}"#;
455 let answer: AskUserAnswer = serde_json::from_str(json).unwrap();
456 assert!(answer.selected_values.is_empty());
457 }
458}