1use serde::{Deserialize, Serialize};
7use serde_json::Value;
8use std::collections::HashMap;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12#[serde(rename_all = "camelCase")]
13pub struct StringSchema {
14 #[serde(rename = "type")]
15 pub schema_type: String, #[serde(skip_serializing_if = "Option::is_none")]
17 pub title: Option<String>,
18 #[serde(skip_serializing_if = "Option::is_none")]
19 pub description: Option<String>,
20 #[serde(skip_serializing_if = "Option::is_none")]
21 pub min_length: Option<usize>,
22 #[serde(skip_serializing_if = "Option::is_none")]
23 pub max_length: Option<usize>,
24 #[serde(skip_serializing_if = "Option::is_none")]
25 pub format: Option<StringFormat>,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
30#[serde(rename_all = "camelCase")]
31pub struct NumberSchema {
32 #[serde(rename = "type")]
33 pub schema_type: String, #[serde(skip_serializing_if = "Option::is_none")]
35 pub title: Option<String>,
36 #[serde(skip_serializing_if = "Option::is_none")]
37 pub description: Option<String>,
38 #[serde(skip_serializing_if = "Option::is_none")]
39 pub minimum: Option<f64>,
40 #[serde(skip_serializing_if = "Option::is_none")]
41 pub maximum: Option<f64>,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
46#[serde(rename_all = "camelCase")]
47pub struct BooleanSchema {
48 #[serde(rename = "type")]
49 pub schema_type: String, #[serde(skip_serializing_if = "Option::is_none")]
51 pub title: Option<String>,
52 #[serde(skip_serializing_if = "Option::is_none")]
53 pub description: Option<String>,
54 #[serde(skip_serializing_if = "Option::is_none")]
55 pub default: Option<bool>,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
60#[serde(rename_all = "camelCase")]
61pub struct EnumSchema {
62 #[serde(rename = "type")]
63 pub schema_type: String, #[serde(skip_serializing_if = "Option::is_none")]
65 pub title: Option<String>,
66 #[serde(skip_serializing_if = "Option::is_none")]
67 pub description: Option<String>,
68 #[serde(rename = "enum")]
69 pub enum_values: Vec<String>,
70 #[serde(skip_serializing_if = "Option::is_none")]
71 pub enum_names: Option<Vec<String>>, }
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
77#[serde(untagged)]
78pub enum PrimitiveSchemaDefinition {
79 String(StringSchema),
80 Number(NumberSchema),
81 Boolean(BooleanSchema),
82 Enum(EnumSchema),
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
87#[serde(rename_all = "kebab-case")]
88pub enum StringFormat {
89 Email,
90 Uri,
91 Date,
92 #[serde(rename = "date-time")]
93 DateTime,
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize)]
98#[serde(rename_all = "camelCase")]
99pub struct ElicitationSchema {
100 #[serde(rename = "type")]
101 pub schema_type: String, pub properties: HashMap<String, PrimitiveSchemaDefinition>,
103 #[serde(skip_serializing_if = "Option::is_none")]
104 pub required: Option<Vec<String>>,
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize)]
109#[serde(rename_all = "camelCase")]
110pub struct ElicitCreateParams {
111 pub message: String,
113 pub requested_schema: ElicitationSchema,
115 #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
117 pub meta: Option<HashMap<String, Value>>,
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize)]
122#[serde(rename_all = "camelCase")]
123pub struct ElicitCreateRequest {
124 pub method: String,
126 pub params: ElicitCreateParams,
128}
129
130impl ElicitCreateRequest {
131 pub fn new(message: impl Into<String>, requested_schema: ElicitationSchema) -> Self {
132 Self {
133 method: "elicitation/create".to_string(),
134 params: ElicitCreateParams {
135 message: message.into(),
136 requested_schema,
137 meta: None,
138 },
139 }
140 }
141
142 pub fn with_meta(mut self, meta: HashMap<String, Value>) -> Self {
143 self.params.meta = Some(meta);
144 self
145 }
146}
147
148impl ElicitCreateParams {
149 pub fn new(message: impl Into<String>, requested_schema: ElicitationSchema) -> Self {
150 Self {
151 message: message.into(),
152 requested_schema,
153 meta: None,
154 }
155 }
156
157 pub fn with_meta(mut self, meta: HashMap<String, Value>) -> Self {
158 self.meta = Some(meta);
159 self
160 }
161}
162
163use crate::traits::*;
165
166impl Params for ElicitCreateParams {}
167
168impl HasMetaParam for ElicitCreateParams {
169 fn meta(&self) -> Option<&HashMap<String, Value>> {
170 self.meta.as_ref()
171 }
172}
173
174impl HasMethod for ElicitCreateRequest {
175 fn method(&self) -> &str {
176 &self.method
177 }
178}
179
180impl HasParams for ElicitCreateRequest {
181 fn params(&self) -> Option<&dyn Params> {
182 Some(&self.params)
183 }
184}
185
186impl HasData for ElicitResult {
187 fn data(&self) -> HashMap<String, Value> {
188 let mut data = HashMap::new();
189 data.insert(
190 "action".to_string(),
191 serde_json::to_value(self.action).unwrap_or(Value::String("cancel".to_string())),
192 );
193 if let Some(ref content) = self.content {
194 data.insert(
195 "content".to_string(),
196 serde_json::to_value(content).unwrap_or(Value::Null),
197 );
198 }
199 data
200 }
201}
202
203impl HasMeta for ElicitResult {
204 fn meta(&self) -> Option<HashMap<String, Value>> {
205 self.meta.clone()
206 }
207}
208
209impl RpcResult for ElicitResult {}
210
211impl Default for ElicitationSchema {
212 fn default() -> Self {
213 Self::new()
214 }
215}
216
217impl ElicitationSchema {
218 pub fn new() -> Self {
219 Self {
220 schema_type: "object".to_string(),
221 properties: HashMap::new(),
222 required: None,
223 }
224 }
225
226 pub fn with_property(
227 mut self,
228 name: impl Into<String>,
229 schema: PrimitiveSchemaDefinition,
230 ) -> Self {
231 self.properties.insert(name.into(), schema);
232 self
233 }
234
235 pub fn with_required(mut self, required: Vec<String>) -> Self {
236 self.required = Some(required);
237 self
238 }
239}
240
241#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
243#[serde(rename_all = "lowercase")]
244pub enum ElicitAction {
245 Accept,
247 Decline,
249 Cancel,
251}
252
253#[derive(Debug, Clone, Serialize, Deserialize)]
255#[serde(rename_all = "camelCase")]
256pub struct ElicitResult {
257 pub action: ElicitAction,
259 #[serde(skip_serializing_if = "Option::is_none")]
262 pub content: Option<HashMap<String, Value>>,
263 #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
265 pub meta: Option<HashMap<String, Value>>,
266}
267
268impl ElicitResult {
269 pub fn accept(content: HashMap<String, Value>) -> Self {
270 Self {
271 action: ElicitAction::Accept,
272 content: Some(content),
273 meta: None,
274 }
275 }
276
277 pub fn decline() -> Self {
278 Self {
279 action: ElicitAction::Decline,
280 content: None,
281 meta: None,
282 }
283 }
284
285 pub fn cancel() -> Self {
286 Self {
287 action: ElicitAction::Cancel,
288 content: None,
289 meta: None,
290 }
291 }
292
293 pub fn with_meta(mut self, meta: HashMap<String, Value>) -> Self {
294 self.meta = Some(meta);
295 self
296 }
297}
298
299impl Default for StringSchema {
301 fn default() -> Self {
302 Self::new()
303 }
304}
305
306impl StringSchema {
307 pub fn new() -> Self {
308 Self {
309 schema_type: "string".to_string(),
310 title: None,
311 description: None,
312 min_length: None,
313 max_length: None,
314 format: None,
315 }
316 }
317
318 pub fn with_description(mut self, description: impl Into<String>) -> Self {
319 self.description = Some(description.into());
320 self
321 }
322}
323
324impl Default for NumberSchema {
325 fn default() -> Self {
326 Self::new()
327 }
328}
329
330impl NumberSchema {
331 pub fn new() -> Self {
332 Self {
333 schema_type: "number".to_string(),
334 title: None,
335 description: None,
336 minimum: None,
337 maximum: None,
338 }
339 }
340
341 pub fn integer() -> Self {
342 Self {
343 schema_type: "integer".to_string(),
344 title: None,
345 description: None,
346 minimum: None,
347 maximum: None,
348 }
349 }
350
351 pub fn with_description(mut self, description: impl Into<String>) -> Self {
352 self.description = Some(description.into());
353 self
354 }
355}
356
357impl Default for BooleanSchema {
358 fn default() -> Self {
359 Self::new()
360 }
361}
362
363impl BooleanSchema {
364 pub fn new() -> Self {
365 Self {
366 schema_type: "boolean".to_string(),
367 title: None,
368 description: None,
369 default: None,
370 }
371 }
372
373 pub fn with_description(mut self, description: impl Into<String>) -> Self {
374 self.description = Some(description.into());
375 self
376 }
377}
378
379impl EnumSchema {
380 pub fn new(enum_values: Vec<String>) -> Self {
381 Self {
382 schema_type: "string".to_string(),
383 title: None,
384 description: None,
385 enum_values,
386 enum_names: None,
387 }
388 }
389
390 pub fn with_description(mut self, description: impl Into<String>) -> Self {
391 self.description = Some(description.into());
392 self
393 }
394
395 pub fn with_enum_names(mut self, enum_names: Vec<String>) -> Self {
396 self.enum_names = Some(enum_names);
397 self
398 }
399}
400
401impl PrimitiveSchemaDefinition {
403 pub fn string() -> Self {
404 Self::String(StringSchema::new())
405 }
406
407 pub fn string_with_description(description: impl Into<String>) -> Self {
408 Self::String(StringSchema::new().with_description(description))
409 }
410
411 pub fn number() -> Self {
412 Self::Number(NumberSchema::new())
413 }
414
415 pub fn integer() -> Self {
416 Self::Number(NumberSchema::integer())
417 }
418
419 pub fn boolean() -> Self {
420 Self::Boolean(BooleanSchema::new())
421 }
422
423 pub fn enum_values(values: Vec<String>) -> Self {
424 Self::Enum(EnumSchema::new(values))
425 }
426}
427
428pub struct ElicitationBuilder;
430
431impl ElicitationBuilder {
432 pub fn text_input(
434 message: impl Into<String>,
435 field_name: impl Into<String>,
436 field_description: impl Into<String>,
437 ) -> ElicitCreateRequest {
438 let field_name = field_name.into();
439 let schema = ElicitationSchema::new()
440 .with_property(
441 field_name.clone(),
442 PrimitiveSchemaDefinition::string_with_description(field_description),
443 )
444 .with_required(vec![field_name]);
445
446 ElicitCreateRequest::new(message, schema)
447 }
448
449 pub fn number_input(
451 message: impl Into<String>,
452 field_name: impl Into<String>,
453 field_description: impl Into<String>,
454 min: Option<f64>,
455 max: Option<f64>,
456 ) -> ElicitCreateRequest {
457 let field_name = field_name.into();
458 let mut number_schema = NumberSchema::new().with_description(field_description);
459 number_schema.minimum = min;
460 number_schema.maximum = max;
461 let number_schema = PrimitiveSchemaDefinition::Number(number_schema);
462
463 let schema = ElicitationSchema::new()
464 .with_property(field_name.clone(), number_schema)
465 .with_required(vec![field_name]);
466
467 ElicitCreateRequest::new(message, schema)
468 }
469
470 pub fn confirm(message: impl Into<String>) -> ElicitCreateRequest {
472 let schema = ElicitationSchema::new()
473 .with_property(
474 "confirmed".to_string(),
475 PrimitiveSchemaDefinition::boolean(),
476 )
477 .with_required(vec!["confirmed".to_string()]);
478
479 ElicitCreateRequest::new(message, schema)
480 }
481}
482
483#[cfg(test)]
490mod tests {
491 use super::*;
492 use serde_json::json;
493
494 #[test]
495 fn test_primitive_schema_creation() {
496 let string_schema = PrimitiveSchemaDefinition::string_with_description("Enter your name");
497 let number_schema = PrimitiveSchemaDefinition::number();
498 let boolean_schema = PrimitiveSchemaDefinition::boolean();
499
500 assert!(matches!(
501 string_schema,
502 PrimitiveSchemaDefinition::String { .. }
503 ));
504 assert!(matches!(
505 number_schema,
506 PrimitiveSchemaDefinition::Number { .. }
507 ));
508 assert!(matches!(
509 boolean_schema,
510 PrimitiveSchemaDefinition::Boolean { .. }
511 ));
512 }
513
514 #[test]
515 fn test_elicitation_schema() {
516 let schema = ElicitationSchema::new()
517 .with_property(
518 "name".to_string(),
519 PrimitiveSchemaDefinition::string_with_description("Your name"),
520 )
521 .with_property("age".to_string(), PrimitiveSchemaDefinition::integer())
522 .with_required(vec!["name".to_string()]);
523
524 assert_eq!(schema.schema_type, "object");
525 assert_eq!(schema.properties.len(), 2);
526 assert_eq!(schema.required, Some(vec!["name".to_string()]));
527 }
528
529 #[test]
530 fn test_elicit_create_request() {
531 let schema = ElicitationSchema::new().with_property(
532 "username".to_string(),
533 PrimitiveSchemaDefinition::string_with_description("Username"),
534 );
535
536 let request = ElicitCreateRequest::new("Please enter your username", schema);
537
538 assert_eq!(request.method, "elicitation/create");
539 assert_eq!(request.params.message, "Please enter your username");
540 }
541
542 #[test]
543 fn test_elicit_result() {
544 let mut content = HashMap::new();
545 content.insert("name".to_string(), json!("John"));
546
547 let accept_result = ElicitResult::accept(content);
548 let decline_result = ElicitResult::decline();
549 let cancel_result = ElicitResult::cancel();
550
551 assert!(matches!(accept_result.action, ElicitAction::Accept));
552 assert!(accept_result.content.is_some());
553
554 assert!(matches!(decline_result.action, ElicitAction::Decline));
555 assert!(decline_result.content.is_none());
556
557 assert!(matches!(cancel_result.action, ElicitAction::Cancel));
558 assert!(cancel_result.content.is_none());
559 }
560
561 #[test]
562 fn test_elicitation_builder() {
563 let text_request =
564 ElicitationBuilder::text_input("Enter your name", "name", "Your full name");
565
566 let confirm_request = ElicitationBuilder::confirm("Do you agree?");
567
568 assert_eq!(text_request.method, "elicitation/create");
569 assert!(
570 text_request
571 .params
572 .requested_schema
573 .properties
574 .contains_key("name")
575 );
576
577 assert_eq!(confirm_request.method, "elicitation/create");
578 assert!(
579 confirm_request
580 .params
581 .requested_schema
582 .properties
583 .contains_key("confirmed")
584 );
585 }
586
587 #[test]
588 fn test_serialization() {
589 let schema = ElicitationSchema::new()
590 .with_property("test".to_string(), PrimitiveSchemaDefinition::string());
591 let request = ElicitCreateRequest::new("Test message", schema);
592
593 let json = serde_json::to_string(&request).unwrap();
594 assert!(json.contains("elicitation/create"));
595 assert!(json.contains("Test message"));
596
597 let parsed: ElicitCreateRequest = serde_json::from_str(&json).unwrap();
598 assert_eq!(parsed.method, "elicitation/create");
599 assert_eq!(parsed.params.message, "Test message");
600 }
601
602 #[test]
603 fn test_elicit_request_matches_typescript_spec() {
604 let mut meta = HashMap::new();
606 meta.insert("requestId".to_string(), json!("req-123"));
607
608 let schema = ElicitationSchema::new()
609 .with_property(
610 "name".to_string(),
611 PrimitiveSchemaDefinition::string_with_description("Your name"),
612 )
613 .with_property("age".to_string(), PrimitiveSchemaDefinition::integer())
614 .with_required(vec!["name".to_string()]);
615
616 let request =
617 ElicitCreateRequest::new("Please provide your details", schema).with_meta(meta);
618
619 let json_value = serde_json::to_value(&request).unwrap();
620
621 assert_eq!(json_value["method"], "elicitation/create");
622 assert!(json_value["params"].is_object());
623 assert_eq!(
624 json_value["params"]["message"],
625 "Please provide your details"
626 );
627 assert!(json_value["params"]["requestedSchema"].is_object());
628 assert_eq!(json_value["params"]["requestedSchema"]["type"], "object");
629 assert!(json_value["params"]["requestedSchema"]["properties"].is_object());
630 assert_eq!(json_value["params"]["_meta"]["requestId"], "req-123");
631 }
632
633 #[test]
634 fn test_elicit_result_matches_typescript_spec() {
635 let mut meta = HashMap::new();
637 meta.insert("responseTime".to_string(), json!(1234));
638
639 let mut content = HashMap::new();
640 content.insert("name".to_string(), json!("John Doe"));
641 content.insert("age".to_string(), json!(30));
642
643 let result = ElicitResult::accept(content.clone()).with_meta(meta);
644
645 let json_value = serde_json::to_value(&result).unwrap();
646
647 assert_eq!(json_value["action"], "accept");
648 assert!(json_value["content"].is_object());
649 assert_eq!(json_value["content"]["name"], "John Doe");
650 assert_eq!(json_value["content"]["age"], 30);
651 assert_eq!(json_value["_meta"]["responseTime"], 1234);
652
653 let decline_result = ElicitResult::decline();
655 let decline_json = serde_json::to_value(&decline_result).unwrap();
656
657 assert_eq!(decline_json["action"], "decline");
658 assert!(
659 decline_json["content"].is_null()
660 || !decline_json.as_object().unwrap().contains_key("content")
661 );
662 }
663
664 #[test]
665 fn test_primitive_schema_definitions_match_typescript() {
666 let string_schema = PrimitiveSchemaDefinition::string_with_description("Enter text");
668 let string_json = serde_json::to_value(&string_schema).unwrap();
669 assert_eq!(string_json["type"], "string");
670 assert_eq!(string_json["description"], "Enter text");
671
672 let number_schema = PrimitiveSchemaDefinition::number();
674 let number_json = serde_json::to_value(&number_schema).unwrap();
675 assert_eq!(number_json["type"], "number");
676
677 let integer_schema = PrimitiveSchemaDefinition::integer();
679 let integer_json = serde_json::to_value(&integer_schema).unwrap();
680 assert_eq!(integer_json["type"], "integer");
681
682 let boolean_schema = PrimitiveSchemaDefinition::boolean();
684 let boolean_json = serde_json::to_value(&boolean_schema).unwrap();
685 assert_eq!(boolean_json["type"], "boolean");
686
687 let enum_schema = PrimitiveSchemaDefinition::enum_values(vec![
689 "red".to_string(),
690 "green".to_string(),
691 "blue".to_string(),
692 ]);
693 let enum_json = serde_json::to_value(&enum_schema).unwrap();
694 assert_eq!(enum_json["type"], "string");
695 assert!(enum_json["enum"].is_array());
696 assert_eq!(enum_json["enum"].as_array().unwrap().len(), 3);
697 }
698}