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("action".to_string(), serde_json::to_value(&self.action).unwrap_or(Value::String("cancel".to_string())));
190 if let Some(ref content) = self.content {
191 data.insert("content".to_string(), serde_json::to_value(content).unwrap_or(Value::Null));
192 }
193 data
194 }
195}
196
197impl HasMeta for ElicitResult {
198 fn meta(&self) -> Option<HashMap<String, Value>> {
199 self.meta.clone()
200 }
201}
202
203impl RpcResult for ElicitResult {}
204
205impl ElicitationSchema {
206 pub fn new() -> Self {
207 Self {
208 schema_type: "object".to_string(),
209 properties: HashMap::new(),
210 required: None,
211 }
212 }
213
214 pub fn with_property(mut self, name: impl Into<String>, schema: PrimitiveSchemaDefinition) -> Self {
215 self.properties.insert(name.into(), schema);
216 self
217 }
218
219 pub fn with_required(mut self, required: Vec<String>) -> Self {
220 self.required = Some(required);
221 self
222 }
223}
224
225#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
227#[serde(rename_all = "lowercase")]
228pub enum ElicitAction {
229 Accept,
231 Decline,
233 Cancel,
235}
236
237#[derive(Debug, Clone, Serialize, Deserialize)]
239#[serde(rename_all = "camelCase")]
240pub struct ElicitResult {
241 pub action: ElicitAction,
243 #[serde(skip_serializing_if = "Option::is_none")]
246 pub content: Option<HashMap<String, Value>>,
247 #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
249 pub meta: Option<HashMap<String, Value>>,
250}
251
252impl ElicitResult {
253 pub fn accept(content: HashMap<String, Value>) -> Self {
254 Self {
255 action: ElicitAction::Accept,
256 content: Some(content),
257 meta: None,
258 }
259 }
260
261 pub fn decline() -> Self {
262 Self {
263 action: ElicitAction::Decline,
264 content: None,
265 meta: None,
266 }
267 }
268
269 pub fn cancel() -> Self {
270 Self {
271 action: ElicitAction::Cancel,
272 content: None,
273 meta: None,
274 }
275 }
276
277 pub fn with_meta(mut self, meta: HashMap<String, Value>) -> Self {
278 self.meta = Some(meta);
279 self
280 }
281}
282
283impl StringSchema {
285 pub fn new() -> Self {
286 Self {
287 schema_type: "string".to_string(),
288 title: None,
289 description: None,
290 min_length: None,
291 max_length: None,
292 format: None,
293 }
294 }
295
296 pub fn with_description(mut self, description: impl Into<String>) -> Self {
297 self.description = Some(description.into());
298 self
299 }
300}
301
302impl NumberSchema {
303 pub fn new() -> Self {
304 Self {
305 schema_type: "number".to_string(),
306 title: None,
307 description: None,
308 minimum: None,
309 maximum: None,
310 }
311 }
312
313 pub fn integer() -> Self {
314 Self {
315 schema_type: "integer".to_string(),
316 title: None,
317 description: None,
318 minimum: None,
319 maximum: None,
320 }
321 }
322
323 pub fn with_description(mut self, description: impl Into<String>) -> Self {
324 self.description = Some(description.into());
325 self
326 }
327}
328
329impl BooleanSchema {
330 pub fn new() -> Self {
331 Self {
332 schema_type: "boolean".to_string(),
333 title: None,
334 description: None,
335 default: None,
336 }
337 }
338
339 pub fn with_description(mut self, description: impl Into<String>) -> Self {
340 self.description = Some(description.into());
341 self
342 }
343}
344
345impl EnumSchema {
346 pub fn new(enum_values: Vec<String>) -> Self {
347 Self {
348 schema_type: "string".to_string(),
349 title: None,
350 description: None,
351 enum_values,
352 enum_names: None,
353 }
354 }
355
356 pub fn with_description(mut self, description: impl Into<String>) -> Self {
357 self.description = Some(description.into());
358 self
359 }
360
361 pub fn with_enum_names(mut self, enum_names: Vec<String>) -> Self {
362 self.enum_names = Some(enum_names);
363 self
364 }
365}
366
367impl PrimitiveSchemaDefinition {
369 pub fn string() -> Self {
370 Self::String(StringSchema::new())
371 }
372
373 pub fn string_with_description(description: impl Into<String>) -> Self {
374 Self::String(StringSchema::new().with_description(description))
375 }
376
377 pub fn number() -> Self {
378 Self::Number(NumberSchema::new())
379 }
380
381 pub fn integer() -> Self {
382 Self::Number(NumberSchema::integer())
383 }
384
385 pub fn boolean() -> Self {
386 Self::Boolean(BooleanSchema::new())
387 }
388
389 pub fn enum_values(values: Vec<String>) -> Self {
390 Self::Enum(EnumSchema::new(values))
391 }
392}
393
394pub struct ElicitationBuilder;
396
397impl ElicitationBuilder {
398 pub fn text_input(
400 message: impl Into<String>,
401 field_name: impl Into<String>,
402 field_description: impl Into<String>
403 ) -> ElicitCreateRequest {
404 let field_name = field_name.into();
405 let schema = ElicitationSchema::new()
406 .with_property(field_name.clone(), PrimitiveSchemaDefinition::string_with_description(field_description))
407 .with_required(vec![field_name]);
408
409 ElicitCreateRequest::new(message, schema)
410 }
411
412 pub fn number_input(
414 message: impl Into<String>,
415 field_name: impl Into<String>,
416 field_description: impl Into<String>,
417 min: Option<f64>,
418 max: Option<f64>
419 ) -> ElicitCreateRequest {
420 let field_name = field_name.into();
421 let mut number_schema = NumberSchema::new().with_description(field_description);
422 number_schema.minimum = min;
423 number_schema.maximum = max;
424 let number_schema = PrimitiveSchemaDefinition::Number(number_schema);
425
426 let schema = ElicitationSchema::new()
427 .with_property(field_name.clone(), number_schema)
428 .with_required(vec![field_name]);
429
430 ElicitCreateRequest::new(message, schema)
431 }
432
433 pub fn confirm(message: impl Into<String>) -> ElicitCreateRequest {
435 let schema = ElicitationSchema::new()
436 .with_property("confirmed".to_string(), PrimitiveSchemaDefinition::boolean())
437 .with_required(vec!["confirmed".to_string()]);
438
439 ElicitCreateRequest::new(message, schema)
440 }
441}
442
443pub trait HasElicitationMetadata {
449 fn message(&self) -> &str;
451
452 fn title(&self) -> Option<&str> {
454 None
455 }
456}
457
458pub trait HasElicitationSchema {
460 fn requested_schema(&self) -> &ElicitationSchema;
462
463 fn validate_schema(&self) -> Result<(), String> {
465 Ok(())
467 }
468}
469
470
471pub trait HasElicitationHandling {
473 fn validate_content(&self, _content: &HashMap<String, Value>) -> Result<(), String> {
475 Ok(())
477 }
478
479 fn process_content(&self, content: HashMap<String, Value>) -> Result<HashMap<String, Value>, String> {
481 Ok(content)
482 }
483}
484
485pub trait ElicitationDefinition:
487 HasElicitationMetadata +
488 HasElicitationSchema +
489 HasElicitationHandling
490{
491 fn to_create_request(&self) -> ElicitCreateRequest {
493 ElicitCreateRequest::new(self.message(), self.requested_schema().clone())
494 }
495}
496
497impl<T> ElicitationDefinition for T
499where
500 T: HasElicitationMetadata + HasElicitationSchema + HasElicitationHandling
501{}
502
503#[cfg(test)]
504mod tests {
505 use super::*;
506 use serde_json::json;
507
508 #[test]
509 fn test_primitive_schema_creation() {
510 let string_schema = PrimitiveSchemaDefinition::string_with_description("Enter your name");
511 let number_schema = PrimitiveSchemaDefinition::number();
512 let boolean_schema = PrimitiveSchemaDefinition::boolean();
513
514 assert!(matches!(string_schema, PrimitiveSchemaDefinition::String { .. }));
515 assert!(matches!(number_schema, PrimitiveSchemaDefinition::Number { .. }));
516 assert!(matches!(boolean_schema, PrimitiveSchemaDefinition::Boolean { .. }));
517 }
518
519 #[test]
520 fn test_elicitation_schema() {
521 let schema = ElicitationSchema::new()
522 .with_property("name".to_string(), PrimitiveSchemaDefinition::string_with_description("Your name"))
523 .with_property("age".to_string(), PrimitiveSchemaDefinition::integer())
524 .with_required(vec!["name".to_string()]);
525
526 assert_eq!(schema.schema_type, "object");
527 assert_eq!(schema.properties.len(), 2);
528 assert_eq!(schema.required, Some(vec!["name".to_string()]));
529 }
530
531 #[test]
532 fn test_elicit_create_request() {
533 let schema = ElicitationSchema::new()
534 .with_property("username".to_string(), PrimitiveSchemaDefinition::string_with_description("Username"));
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 = ElicitationBuilder::text_input(
564 "Enter your name",
565 "name",
566 "Your full name"
567 );
568
569 let confirm_request = ElicitationBuilder::confirm("Do you agree?");
570
571 assert_eq!(text_request.method, "elicitation/create");
572 assert!(text_request.params.requested_schema.properties.contains_key("name"));
573
574 assert_eq!(confirm_request.method, "elicitation/create");
575 assert!(confirm_request.params.requested_schema.properties.contains_key("confirmed"));
576 }
577
578 #[test]
579 fn test_serialization() {
580 let schema = ElicitationSchema::new()
581 .with_property("test".to_string(), PrimitiveSchemaDefinition::string());
582 let request = ElicitCreateRequest::new("Test message", schema);
583
584 let json = serde_json::to_string(&request).unwrap();
585 assert!(json.contains("elicitation/create"));
586 assert!(json.contains("Test message"));
587
588 let parsed: ElicitCreateRequest = serde_json::from_str(&json).unwrap();
589 assert_eq!(parsed.method, "elicitation/create");
590 assert_eq!(parsed.params.message, "Test message");
591 }
592
593 #[test]
594 fn test_elicit_request_matches_typescript_spec() {
595 let mut meta = HashMap::new();
597 meta.insert("requestId".to_string(), json!("req-123"));
598
599 let schema = ElicitationSchema::new()
600 .with_property("name".to_string(), PrimitiveSchemaDefinition::string_with_description("Your name"))
601 .with_property("age".to_string(), PrimitiveSchemaDefinition::integer())
602 .with_required(vec!["name".to_string()]);
603
604 let request = ElicitCreateRequest::new("Please provide your details", schema)
605 .with_meta(meta);
606
607 let json_value = serde_json::to_value(&request).unwrap();
608
609 assert_eq!(json_value["method"], "elicitation/create");
610 assert!(json_value["params"].is_object());
611 assert_eq!(json_value["params"]["message"], "Please provide your details");
612 assert!(json_value["params"]["requestedSchema"].is_object());
613 assert_eq!(json_value["params"]["requestedSchema"]["type"], "object");
614 assert!(json_value["params"]["requestedSchema"]["properties"].is_object());
615 assert_eq!(json_value["params"]["_meta"]["requestId"], "req-123");
616 }
617
618 #[test]
619 fn test_elicit_result_matches_typescript_spec() {
620 let mut meta = HashMap::new();
622 meta.insert("responseTime".to_string(), json!(1234));
623
624 let mut content = HashMap::new();
625 content.insert("name".to_string(), json!("John Doe"));
626 content.insert("age".to_string(), json!(30));
627
628 let result = ElicitResult::accept(content.clone())
629 .with_meta(meta);
630
631 let json_value = serde_json::to_value(&result).unwrap();
632
633 assert_eq!(json_value["action"], "accept");
634 assert!(json_value["content"].is_object());
635 assert_eq!(json_value["content"]["name"], "John Doe");
636 assert_eq!(json_value["content"]["age"], 30);
637 assert_eq!(json_value["_meta"]["responseTime"], 1234);
638
639 let decline_result = ElicitResult::decline();
641 let decline_json = serde_json::to_value(&decline_result).unwrap();
642
643 assert_eq!(decline_json["action"], "decline");
644 assert!(decline_json["content"].is_null() || !decline_json.as_object().unwrap().contains_key("content"));
645 }
646
647 #[test]
648 fn test_primitive_schema_definitions_match_typescript() {
649 let string_schema = PrimitiveSchemaDefinition::string_with_description("Enter text");
651 let string_json = serde_json::to_value(&string_schema).unwrap();
652 assert_eq!(string_json["type"], "string");
653 assert_eq!(string_json["description"], "Enter text");
654
655 let number_schema = PrimitiveSchemaDefinition::number();
657 let number_json = serde_json::to_value(&number_schema).unwrap();
658 assert_eq!(number_json["type"], "number");
659
660 let integer_schema = PrimitiveSchemaDefinition::integer();
662 let integer_json = serde_json::to_value(&integer_schema).unwrap();
663 assert_eq!(integer_json["type"], "integer");
664
665 let boolean_schema = PrimitiveSchemaDefinition::boolean();
667 let boolean_json = serde_json::to_value(&boolean_schema).unwrap();
668 assert_eq!(boolean_json["type"], "boolean");
669
670 let enum_schema = PrimitiveSchemaDefinition::enum_values(vec!["red".to_string(), "green".to_string(), "blue".to_string()]);
672 let enum_json = serde_json::to_value(&enum_schema).unwrap();
673 assert_eq!(enum_json["type"], "string");
674 assert!(enum_json["enum"].is_array());
675 assert_eq!(enum_json["enum"].as_array().unwrap().len(), 3);
676 }
677}