1use std::{collections::BTreeMap, sync::Arc};
9
10use derive_more::{Display, From};
11use schemars::JsonSchema;
12use serde::{Deserialize, Serialize};
13use serde_with::{DefaultOnError, serde_as, skip_serializing_none};
14
15use super::{
16 ELICITATION_COMPLETE_NOTIFICATION, ELICITATION_CREATE_METHOD_NAME, Meta, RequestId, SessionId,
17 ToolCallId,
18};
19use crate::IntoOption;
20
21#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Hash, Display, From)]
27#[serde(transparent)]
28#[from(Arc<str>, String, &'static str)]
29#[non_exhaustive]
30pub struct ElicitationId(pub Arc<str>);
31
32impl ElicitationId {
33 #[must_use]
34 pub fn new(id: impl Into<Arc<str>>) -> Self {
35 Self(id.into())
36 }
37}
38
39#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
41#[serde(rename_all = "kebab-case")]
42#[non_exhaustive]
43pub enum StringFormat {
44 Email,
46 Uri,
48 Date,
50 DateTime,
52 #[serde(untagged)]
57 Other(String),
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
62#[serde(rename_all = "snake_case")]
63#[non_exhaustive]
64pub enum ElicitationSchemaType {
65 #[default]
67 Object,
68}
69
70#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
72#[non_exhaustive]
73pub struct EnumOption {
74 #[serde(rename = "const")]
76 pub value: String,
77 pub title: String,
79}
80
81impl EnumOption {
82 #[must_use]
84 pub fn new(value: impl Into<String>, title: impl Into<String>) -> Self {
85 Self {
86 value: value.into(),
87 title: title.into(),
88 }
89 }
90}
91
92#[skip_serializing_none]
97#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
98#[serde(rename_all = "camelCase")]
99#[non_exhaustive]
100pub struct StringPropertySchema {
101 pub title: Option<String>,
103 pub description: Option<String>,
105 pub min_length: Option<u32>,
107 pub max_length: Option<u32>,
109 pub pattern: Option<String>,
111 pub format: Option<StringFormat>,
113 pub default: Option<String>,
115 #[serde(rename = "enum")]
117 pub enum_values: Option<Vec<String>>,
118 #[serde(rename = "oneOf")]
120 pub one_of: Option<Vec<EnumOption>>,
121}
122
123impl StringPropertySchema {
124 #[must_use]
126 pub fn new() -> Self {
127 Self::default()
128 }
129
130 #[must_use]
132 pub fn email() -> Self {
133 Self {
134 format: Some(StringFormat::Email),
135 ..Default::default()
136 }
137 }
138
139 #[must_use]
141 pub fn uri() -> Self {
142 Self {
143 format: Some(StringFormat::Uri),
144 ..Default::default()
145 }
146 }
147
148 #[must_use]
150 pub fn date() -> Self {
151 Self {
152 format: Some(StringFormat::Date),
153 ..Default::default()
154 }
155 }
156
157 #[must_use]
159 pub fn date_time() -> Self {
160 Self {
161 format: Some(StringFormat::DateTime),
162 ..Default::default()
163 }
164 }
165
166 #[must_use]
168 pub fn title(mut self, title: impl IntoOption<String>) -> Self {
169 self.title = title.into_option();
170 self
171 }
172
173 #[must_use]
175 pub fn description(mut self, description: impl IntoOption<String>) -> Self {
176 self.description = description.into_option();
177 self
178 }
179
180 #[must_use]
182 pub fn min_length(mut self, min_length: impl IntoOption<u32>) -> Self {
183 self.min_length = min_length.into_option();
184 self
185 }
186
187 #[must_use]
189 pub fn max_length(mut self, max_length: impl IntoOption<u32>) -> Self {
190 self.max_length = max_length.into_option();
191 self
192 }
193
194 #[must_use]
196 pub fn pattern(mut self, pattern: impl IntoOption<String>) -> Self {
197 self.pattern = pattern.into_option();
198 self
199 }
200
201 #[must_use]
203 pub fn format(mut self, format: impl IntoOption<StringFormat>) -> Self {
204 self.format = format.into_option();
205 self
206 }
207
208 #[must_use]
210 pub fn default_value(mut self, default: impl IntoOption<String>) -> Self {
211 self.default = default.into_option();
212 self
213 }
214
215 #[must_use]
217 pub fn enum_values(mut self, enum_values: impl IntoOption<Vec<String>>) -> Self {
218 self.enum_values = enum_values.into_option();
219 self
220 }
221
222 #[must_use]
224 pub fn one_of(mut self, one_of: impl IntoOption<Vec<EnumOption>>) -> Self {
225 self.one_of = one_of.into_option();
226 self
227 }
228}
229
230#[skip_serializing_none]
232#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
233#[serde(rename_all = "camelCase")]
234#[non_exhaustive]
235pub struct NumberPropertySchema {
236 pub title: Option<String>,
238 pub description: Option<String>,
240 pub minimum: Option<f64>,
242 pub maximum: Option<f64>,
244 pub default: Option<f64>,
246}
247
248impl NumberPropertySchema {
249 #[must_use]
251 pub fn new() -> Self {
252 Self::default()
253 }
254
255 #[must_use]
257 pub fn title(mut self, title: impl IntoOption<String>) -> Self {
258 self.title = title.into_option();
259 self
260 }
261
262 #[must_use]
264 pub fn description(mut self, description: impl IntoOption<String>) -> Self {
265 self.description = description.into_option();
266 self
267 }
268
269 #[must_use]
271 pub fn minimum(mut self, minimum: impl IntoOption<f64>) -> Self {
272 self.minimum = minimum.into_option();
273 self
274 }
275
276 #[must_use]
278 pub fn maximum(mut self, maximum: impl IntoOption<f64>) -> Self {
279 self.maximum = maximum.into_option();
280 self
281 }
282
283 #[must_use]
285 pub fn default_value(mut self, default: impl IntoOption<f64>) -> Self {
286 self.default = default.into_option();
287 self
288 }
289}
290
291#[skip_serializing_none]
293#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
294#[serde(rename_all = "camelCase")]
295#[non_exhaustive]
296pub struct IntegerPropertySchema {
297 pub title: Option<String>,
299 pub description: Option<String>,
301 pub minimum: Option<i64>,
303 pub maximum: Option<i64>,
305 pub default: Option<i64>,
307}
308
309impl IntegerPropertySchema {
310 #[must_use]
312 pub fn new() -> Self {
313 Self::default()
314 }
315
316 #[must_use]
318 pub fn title(mut self, title: impl IntoOption<String>) -> Self {
319 self.title = title.into_option();
320 self
321 }
322
323 #[must_use]
325 pub fn description(mut self, description: impl IntoOption<String>) -> Self {
326 self.description = description.into_option();
327 self
328 }
329
330 #[must_use]
332 pub fn minimum(mut self, minimum: impl IntoOption<i64>) -> Self {
333 self.minimum = minimum.into_option();
334 self
335 }
336
337 #[must_use]
339 pub fn maximum(mut self, maximum: impl IntoOption<i64>) -> Self {
340 self.maximum = maximum.into_option();
341 self
342 }
343
344 #[must_use]
346 pub fn default_value(mut self, default: impl IntoOption<i64>) -> Self {
347 self.default = default.into_option();
348 self
349 }
350}
351
352#[skip_serializing_none]
354#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
355#[serde(rename_all = "camelCase")]
356#[non_exhaustive]
357pub struct BooleanPropertySchema {
358 pub title: Option<String>,
360 pub description: Option<String>,
362 pub default: Option<bool>,
364}
365
366impl BooleanPropertySchema {
367 #[must_use]
369 pub fn new() -> Self {
370 Self::default()
371 }
372
373 #[must_use]
375 pub fn title(mut self, title: impl IntoOption<String>) -> Self {
376 self.title = title.into_option();
377 self
378 }
379
380 #[must_use]
382 pub fn description(mut self, description: impl IntoOption<String>) -> Self {
383 self.description = description.into_option();
384 self
385 }
386
387 #[must_use]
389 pub fn default_value(mut self, default: impl IntoOption<bool>) -> Self {
390 self.default = default.into_option();
391 self
392 }
393}
394
395#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
397#[serde(rename_all = "snake_case")]
398#[non_exhaustive]
399pub enum ElicitationStringType {
400 #[default]
402 String,
403}
404
405#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
407#[non_exhaustive]
408pub struct UntitledMultiSelectItems {
409 #[serde(rename = "type")]
411 pub type_: ElicitationStringType,
412 #[serde(rename = "enum")]
414 pub values: Vec<String>,
415}
416
417#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
419#[non_exhaustive]
420pub struct TitledMultiSelectItems {
421 #[serde(rename = "anyOf")]
423 pub options: Vec<EnumOption>,
424}
425
426impl TitledMultiSelectItems {
427 #[must_use]
429 pub fn new(options: Vec<EnumOption>) -> Self {
430 Self { options }
431 }
432}
433
434#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
436#[serde(untagged)]
437#[non_exhaustive]
438pub enum MultiSelectItems {
439 Untitled(UntitledMultiSelectItems),
441 Titled(TitledMultiSelectItems),
443}
444
445#[skip_serializing_none]
447#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
448#[serde(rename_all = "camelCase")]
449#[non_exhaustive]
450pub struct MultiSelectPropertySchema {
451 pub title: Option<String>,
453 pub description: Option<String>,
455 pub min_items: Option<u64>,
457 pub max_items: Option<u64>,
459 pub items: MultiSelectItems,
461 pub default: Option<Vec<String>>,
463}
464
465impl MultiSelectPropertySchema {
466 #[must_use]
468 pub fn new(values: Vec<String>) -> Self {
469 Self {
470 title: None,
471 description: None,
472 min_items: None,
473 max_items: None,
474 items: MultiSelectItems::Untitled(UntitledMultiSelectItems {
475 type_: ElicitationStringType::String,
476 values,
477 }),
478 default: None,
479 }
480 }
481
482 #[must_use]
484 pub fn titled(options: Vec<EnumOption>) -> Self {
485 Self {
486 title: None,
487 description: None,
488 min_items: None,
489 max_items: None,
490 items: MultiSelectItems::Titled(TitledMultiSelectItems { options }),
491 default: None,
492 }
493 }
494
495 #[must_use]
497 pub fn title(mut self, title: impl IntoOption<String>) -> Self {
498 self.title = title.into_option();
499 self
500 }
501
502 #[must_use]
504 pub fn description(mut self, description: impl IntoOption<String>) -> Self {
505 self.description = description.into_option();
506 self
507 }
508
509 #[must_use]
511 pub fn min_items(mut self, min_items: impl IntoOption<u64>) -> Self {
512 self.min_items = min_items.into_option();
513 self
514 }
515
516 #[must_use]
518 pub fn max_items(mut self, max_items: impl IntoOption<u64>) -> Self {
519 self.max_items = max_items.into_option();
520 self
521 }
522
523 #[must_use]
525 pub fn default_value(mut self, default: impl IntoOption<Vec<String>>) -> Self {
526 self.default = default.into_option();
527 self
528 }
529}
530
531#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
537#[serde(tag = "type", rename_all = "snake_case")]
538#[schemars(extend("discriminator" = {"propertyName": "type"}))]
539#[non_exhaustive]
540pub enum ElicitationPropertySchema {
541 String(StringPropertySchema),
543 Number(NumberPropertySchema),
545 Integer(IntegerPropertySchema),
547 Boolean(BooleanPropertySchema),
549 Array(MultiSelectPropertySchema),
551}
552
553impl From<StringPropertySchema> for ElicitationPropertySchema {
554 fn from(schema: StringPropertySchema) -> Self {
555 Self::String(schema)
556 }
557}
558
559impl From<NumberPropertySchema> for ElicitationPropertySchema {
560 fn from(schema: NumberPropertySchema) -> Self {
561 Self::Number(schema)
562 }
563}
564
565impl From<IntegerPropertySchema> for ElicitationPropertySchema {
566 fn from(schema: IntegerPropertySchema) -> Self {
567 Self::Integer(schema)
568 }
569}
570
571impl From<BooleanPropertySchema> for ElicitationPropertySchema {
572 fn from(schema: BooleanPropertySchema) -> Self {
573 Self::Boolean(schema)
574 }
575}
576
577impl From<MultiSelectPropertySchema> for ElicitationPropertySchema {
578 fn from(schema: MultiSelectPropertySchema) -> Self {
579 Self::Array(schema)
580 }
581}
582
583fn default_object_type() -> ElicitationSchemaType {
584 ElicitationSchemaType::Object
585}
586
587#[skip_serializing_none]
592#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
593#[serde(rename_all = "camelCase")]
594#[non_exhaustive]
595pub struct ElicitationSchema {
596 #[serde(rename = "type", default = "default_object_type")]
598 pub type_: ElicitationSchemaType,
599 pub title: Option<String>,
601 #[serde(default)]
603 pub properties: BTreeMap<String, ElicitationPropertySchema>,
604 pub required: Option<Vec<String>>,
606 pub description: Option<String>,
608}
609
610impl Default for ElicitationSchema {
611 fn default() -> Self {
612 Self {
613 type_: default_object_type(),
614 title: None,
615 properties: BTreeMap::new(),
616 required: None,
617 description: None,
618 }
619 }
620}
621
622impl ElicitationSchema {
623 #[must_use]
625 pub fn new() -> Self {
626 Self::default()
627 }
628
629 #[must_use]
631 pub fn title(mut self, title: impl IntoOption<String>) -> Self {
632 self.title = title.into_option();
633 self
634 }
635
636 #[must_use]
638 pub fn description(mut self, description: impl IntoOption<String>) -> Self {
639 self.description = description.into_option();
640 self
641 }
642
643 #[must_use]
645 pub fn property<S>(mut self, name: impl Into<String>, schema: S, required: bool) -> Self
646 where
647 S: Into<ElicitationPropertySchema>,
648 {
649 let name = name.into();
650 self.properties.insert(name.clone(), schema.into());
651
652 if required {
653 let required_fields = self.required.get_or_insert_with(Vec::new);
654 if !required_fields.contains(&name) {
655 required_fields.push(name);
656 }
657 } else if let Some(required_fields) = &mut self.required {
658 required_fields.retain(|field| field != &name);
659
660 if required_fields.is_empty() {
661 self.required = None;
662 }
663 }
664
665 self
666 }
667
668 #[must_use]
670 pub fn string(self, name: impl Into<String>, required: bool) -> Self {
671 self.property(name, StringPropertySchema::new(), required)
672 }
673
674 #[must_use]
676 pub fn email(self, name: impl Into<String>, required: bool) -> Self {
677 self.property(name, StringPropertySchema::email(), required)
678 }
679
680 #[must_use]
682 pub fn uri(self, name: impl Into<String>, required: bool) -> Self {
683 self.property(name, StringPropertySchema::uri(), required)
684 }
685
686 #[must_use]
688 pub fn date(self, name: impl Into<String>, required: bool) -> Self {
689 self.property(name, StringPropertySchema::date(), required)
690 }
691
692 #[must_use]
694 pub fn date_time(self, name: impl Into<String>, required: bool) -> Self {
695 self.property(name, StringPropertySchema::date_time(), required)
696 }
697
698 #[must_use]
700 pub fn number(self, name: impl Into<String>, min: f64, max: f64, required: bool) -> Self {
701 self.property(
702 name,
703 NumberPropertySchema::new().minimum(min).maximum(max),
704 required,
705 )
706 }
707
708 #[must_use]
710 pub fn integer(self, name: impl Into<String>, min: i64, max: i64, required: bool) -> Self {
711 self.property(
712 name,
713 IntegerPropertySchema::new().minimum(min).maximum(max),
714 required,
715 )
716 }
717
718 #[must_use]
720 pub fn boolean(self, name: impl Into<String>, required: bool) -> Self {
721 self.property(name, BooleanPropertySchema::new(), required)
722 }
723}
724
725#[serde_as]
731#[skip_serializing_none]
732#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
733#[serde(rename_all = "camelCase")]
734#[non_exhaustive]
735pub struct ElicitationCapabilities {
736 #[serde_as(deserialize_as = "DefaultOnError")]
738 #[schemars(extend("x-deserialize-default-on-error" = true))]
739 #[serde(default)]
740 pub form: Option<ElicitationFormCapabilities>,
741 #[serde_as(deserialize_as = "DefaultOnError")]
743 #[schemars(extend("x-deserialize-default-on-error" = true))]
744 #[serde(default)]
745 pub url: Option<ElicitationUrlCapabilities>,
746 #[serde(rename = "_meta")]
752 pub meta: Option<Meta>,
753}
754
755impl ElicitationCapabilities {
756 #[must_use]
757 pub fn new() -> Self {
758 Self::default()
759 }
760
761 #[must_use]
763 pub fn form(mut self, form: impl IntoOption<ElicitationFormCapabilities>) -> Self {
764 self.form = form.into_option();
765 self
766 }
767
768 #[must_use]
770 pub fn url(mut self, url: impl IntoOption<ElicitationUrlCapabilities>) -> Self {
771 self.url = url.into_option();
772 self
773 }
774
775 #[must_use]
781 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
782 self.meta = meta.into_option();
783 self
784 }
785}
786
787#[skip_serializing_none]
793#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
794#[serde(rename_all = "camelCase")]
795#[non_exhaustive]
796pub struct ElicitationFormCapabilities {
797 #[serde(rename = "_meta")]
803 pub meta: Option<Meta>,
804}
805
806impl ElicitationFormCapabilities {
807 #[must_use]
808 pub fn new() -> Self {
809 Self::default()
810 }
811
812 #[must_use]
818 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
819 self.meta = meta.into_option();
820 self
821 }
822}
823
824#[skip_serializing_none]
830#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
831#[serde(rename_all = "camelCase")]
832#[non_exhaustive]
833pub struct ElicitationUrlCapabilities {
834 #[serde(rename = "_meta")]
840 pub meta: Option<Meta>,
841}
842
843impl ElicitationUrlCapabilities {
844 #[must_use]
845 pub fn new() -> Self {
846 Self::default()
847 }
848
849 #[must_use]
855 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
856 self.meta = meta.into_option();
857 self
858 }
859}
860
861#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
867#[serde(untagged)]
868#[non_exhaustive]
869pub enum ElicitationScope {
870 Session(ElicitationSessionScope),
872 Request(ElicitationRequestScope),
875}
876
877#[skip_serializing_none]
887#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
888#[serde(rename_all = "camelCase")]
889#[non_exhaustive]
890pub struct ElicitationSessionScope {
891 pub session_id: SessionId,
893 pub tool_call_id: Option<ToolCallId>,
895}
896
897impl ElicitationSessionScope {
898 #[must_use]
899 pub fn new(session_id: impl Into<SessionId>) -> Self {
900 Self {
901 session_id: session_id.into(),
902 tool_call_id: None,
903 }
904 }
905
906 #[must_use]
907 pub fn tool_call_id(mut self, tool_call_id: impl IntoOption<ToolCallId>) -> Self {
908 self.tool_call_id = tool_call_id.into_option();
909 self
910 }
911}
912
913#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
920#[serde(rename_all = "camelCase")]
921#[non_exhaustive]
922pub struct ElicitationRequestScope {
923 pub request_id: RequestId,
925}
926
927impl ElicitationRequestScope {
928 #[must_use]
929 pub fn new(request_id: impl Into<RequestId>) -> Self {
930 Self {
931 request_id: request_id.into(),
932 }
933 }
934}
935
936impl From<ElicitationSessionScope> for ElicitationScope {
937 fn from(scope: ElicitationSessionScope) -> Self {
938 Self::Session(scope)
939 }
940}
941
942impl From<ElicitationRequestScope> for ElicitationScope {
943 fn from(scope: ElicitationRequestScope) -> Self {
944 Self::Request(scope)
945 }
946}
947
948#[skip_serializing_none]
958#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
959#[schemars(extend("x-side" = "client", "x-method" = ELICITATION_CREATE_METHOD_NAME))]
960#[serde(rename_all = "camelCase")]
961#[non_exhaustive]
962pub struct CreateElicitationRequest {
963 #[serde(flatten)]
965 pub mode: ElicitationMode,
966 pub message: String,
968 #[serde(rename = "_meta")]
974 pub meta: Option<Meta>,
975}
976
977impl CreateElicitationRequest {
978 #[must_use]
979 pub fn new(mode: impl Into<ElicitationMode>, message: impl Into<String>) -> Self {
980 Self {
981 mode: mode.into(),
982 message: message.into(),
983 meta: None,
984 }
985 }
986
987 #[must_use]
989 pub fn scope(&self) -> &ElicitationScope {
990 self.mode.scope()
991 }
992
993 #[must_use]
999 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1000 self.meta = meta.into_option();
1001 self
1002 }
1003}
1004
1005#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1011#[serde(tag = "mode", rename_all = "snake_case")]
1012#[schemars(extend("discriminator" = {"propertyName": "mode"}))]
1013#[non_exhaustive]
1014pub enum ElicitationMode {
1015 Form(ElicitationFormMode),
1017 Url(ElicitationUrlMode),
1019}
1020
1021impl From<ElicitationFormMode> for ElicitationMode {
1022 fn from(mode: ElicitationFormMode) -> Self {
1023 Self::Form(mode)
1024 }
1025}
1026
1027impl From<ElicitationUrlMode> for ElicitationMode {
1028 fn from(mode: ElicitationUrlMode) -> Self {
1029 Self::Url(mode)
1030 }
1031}
1032
1033impl ElicitationMode {
1034 #[must_use]
1036 pub fn scope(&self) -> &ElicitationScope {
1037 match self {
1038 Self::Form(f) => &f.scope,
1039 Self::Url(u) => &u.scope,
1040 }
1041 }
1042}
1043
1044#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1050#[serde(rename_all = "camelCase")]
1051#[non_exhaustive]
1052pub struct ElicitationFormMode {
1053 #[serde(flatten)]
1055 pub scope: ElicitationScope,
1056 pub requested_schema: ElicitationSchema,
1058}
1059
1060impl ElicitationFormMode {
1061 #[must_use]
1062 pub fn new(scope: impl Into<ElicitationScope>, requested_schema: ElicitationSchema) -> Self {
1063 Self {
1064 scope: scope.into(),
1065 requested_schema,
1066 }
1067 }
1068}
1069
1070#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1076#[serde(rename_all = "camelCase")]
1077#[non_exhaustive]
1078pub struct ElicitationUrlMode {
1079 #[serde(flatten)]
1081 pub scope: ElicitationScope,
1082 pub elicitation_id: ElicitationId,
1084 #[schemars(extend("format" = "uri"))]
1086 pub url: String,
1087}
1088
1089impl ElicitationUrlMode {
1090 #[must_use]
1091 pub fn new(
1092 scope: impl Into<ElicitationScope>,
1093 elicitation_id: impl Into<ElicitationId>,
1094 url: impl Into<String>,
1095 ) -> Self {
1096 Self {
1097 scope: scope.into(),
1098 elicitation_id: elicitation_id.into(),
1099 url: url.into(),
1100 }
1101 }
1102}
1103
1104#[skip_serializing_none]
1110#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1111#[schemars(extend("x-side" = "client", "x-method" = ELICITATION_CREATE_METHOD_NAME))]
1112#[serde(rename_all = "camelCase")]
1113#[non_exhaustive]
1114pub struct CreateElicitationResponse {
1115 #[serde(flatten)]
1117 pub action: ElicitationAction,
1118 #[serde(rename = "_meta")]
1124 pub meta: Option<Meta>,
1125}
1126
1127impl CreateElicitationResponse {
1128 #[must_use]
1129 pub fn new(action: ElicitationAction) -> Self {
1130 Self { action, meta: None }
1131 }
1132
1133 #[must_use]
1139 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1140 self.meta = meta.into_option();
1141 self
1142 }
1143}
1144
1145#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1151#[serde(tag = "action", rename_all = "snake_case")]
1152#[schemars(extend("discriminator" = {"propertyName": "action"}))]
1153#[non_exhaustive]
1154pub enum ElicitationAction {
1155 Accept(ElicitationAcceptAction),
1157 Decline,
1159 Cancel,
1161}
1162
1163#[skip_serializing_none]
1169#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1170#[serde(rename_all = "camelCase")]
1171#[non_exhaustive]
1172pub struct ElicitationAcceptAction {
1173 #[serde(default)]
1175 pub content: Option<BTreeMap<String, ElicitationContentValue>>,
1176}
1177
1178impl ElicitationAcceptAction {
1179 #[must_use]
1180 pub fn new() -> Self {
1181 Self { content: None }
1182 }
1183
1184 #[must_use]
1186 pub fn content(
1187 mut self,
1188 content: impl IntoOption<BTreeMap<String, ElicitationContentValue>>,
1189 ) -> Self {
1190 self.content = content.into_option();
1191 self
1192 }
1193}
1194
1195#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1196#[serde(untagged)]
1197#[non_exhaustive]
1198pub enum ElicitationContentValue {
1199 String(String),
1200 Integer(i64),
1201 Number(f64),
1202 Boolean(bool),
1203 StringArray(Vec<String>),
1204}
1205
1206impl From<String> for ElicitationContentValue {
1207 fn from(value: String) -> Self {
1208 Self::String(value)
1209 }
1210}
1211
1212impl From<&str> for ElicitationContentValue {
1213 fn from(value: &str) -> Self {
1214 Self::String(value.to_string())
1215 }
1216}
1217
1218impl From<i64> for ElicitationContentValue {
1219 fn from(value: i64) -> Self {
1220 Self::Integer(value)
1221 }
1222}
1223
1224impl From<i32> for ElicitationContentValue {
1225 fn from(value: i32) -> Self {
1226 Self::Integer(i64::from(value))
1227 }
1228}
1229
1230impl From<f64> for ElicitationContentValue {
1231 fn from(value: f64) -> Self {
1232 Self::Number(value)
1233 }
1234}
1235
1236impl From<bool> for ElicitationContentValue {
1237 fn from(value: bool) -> Self {
1238 Self::Boolean(value)
1239 }
1240}
1241
1242impl From<Vec<String>> for ElicitationContentValue {
1243 fn from(value: Vec<String>) -> Self {
1244 Self::StringArray(value)
1245 }
1246}
1247
1248impl From<Vec<&str>> for ElicitationContentValue {
1249 fn from(value: Vec<&str>) -> Self {
1250 Self::StringArray(value.into_iter().map(str::to_string).collect())
1251 }
1252}
1253
1254impl Default for ElicitationAcceptAction {
1255 fn default() -> Self {
1256 Self::new()
1257 }
1258}
1259
1260#[skip_serializing_none]
1266#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1267#[schemars(extend("x-side" = "client", "x-method" = ELICITATION_COMPLETE_NOTIFICATION))]
1268#[serde(rename_all = "camelCase")]
1269#[non_exhaustive]
1270pub struct CompleteElicitationNotification {
1271 pub elicitation_id: ElicitationId,
1273 #[serde(rename = "_meta")]
1279 pub meta: Option<Meta>,
1280}
1281
1282impl CompleteElicitationNotification {
1283 #[must_use]
1284 pub fn new(elicitation_id: impl Into<ElicitationId>) -> Self {
1285 Self {
1286 elicitation_id: elicitation_id.into(),
1287 meta: None,
1288 }
1289 }
1290
1291 #[must_use]
1297 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1298 self.meta = meta.into_option();
1299 self
1300 }
1301}
1302
1303#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1310#[serde(rename_all = "camelCase")]
1311#[non_exhaustive]
1312pub struct UrlElicitationRequiredData {
1313 pub elicitations: Vec<UrlElicitationRequiredItem>,
1315}
1316
1317impl UrlElicitationRequiredData {
1318 #[must_use]
1319 pub fn new(elicitations: Vec<UrlElicitationRequiredItem>) -> Self {
1320 Self { elicitations }
1321 }
1322}
1323
1324#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1330#[serde(rename_all = "camelCase")]
1331#[non_exhaustive]
1332pub struct UrlElicitationRequiredItem {
1333 pub mode: ElicitationUrlOnlyMode,
1335 pub elicitation_id: ElicitationId,
1337 #[schemars(extend("format" = "uri"))]
1339 pub url: String,
1340 pub message: String,
1342}
1343
1344#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
1346#[serde(rename_all = "snake_case")]
1347#[non_exhaustive]
1348pub enum ElicitationUrlOnlyMode {
1349 #[default]
1351 Url,
1352}
1353
1354impl UrlElicitationRequiredItem {
1355 #[must_use]
1356 pub fn new(
1357 elicitation_id: impl Into<ElicitationId>,
1358 url: impl Into<String>,
1359 message: impl Into<String>,
1360 ) -> Self {
1361 Self {
1362 mode: ElicitationUrlOnlyMode::Url,
1363 elicitation_id: elicitation_id.into(),
1364 url: url.into(),
1365 message: message.into(),
1366 }
1367 }
1368}
1369
1370#[cfg(test)]
1371mod tests {
1372 use super::*;
1373 use serde_json::json;
1374
1375 #[test]
1376 fn form_mode_request_serialization() {
1377 let schema = ElicitationSchema::new().string("name", true);
1378 let req = CreateElicitationRequest::new(
1379 ElicitationFormMode::new(ElicitationSessionScope::new("sess_1"), schema),
1380 "Please enter your name",
1381 );
1382
1383 let json = serde_json::to_value(&req).unwrap();
1384 assert_eq!(json["sessionId"], "sess_1");
1385 assert!(json.get("toolCallId").is_none());
1386 assert_eq!(json["mode"], "form");
1387 assert_eq!(json["message"], "Please enter your name");
1388 assert!(json["requestedSchema"].is_object());
1389 assert_eq!(json["requestedSchema"]["type"], "object");
1390 assert_eq!(
1391 json["requestedSchema"]["properties"]["name"]["type"],
1392 "string"
1393 );
1394
1395 let roundtripped: CreateElicitationRequest = serde_json::from_value(json).unwrap();
1396 assert_eq!(
1397 *roundtripped.scope(),
1398 ElicitationSessionScope::new("sess_1").into()
1399 );
1400 assert_eq!(roundtripped.message, "Please enter your name");
1401 assert!(matches!(roundtripped.mode, ElicitationMode::Form(_)));
1402 }
1403
1404 #[test]
1405 fn url_mode_request_serialization() {
1406 let req = CreateElicitationRequest::new(
1407 ElicitationUrlMode::new(
1408 ElicitationSessionScope::new("sess_2").tool_call_id("tc_1"),
1409 "elic_1",
1410 "https://example.com/auth",
1411 ),
1412 "Please authenticate",
1413 );
1414
1415 let json = serde_json::to_value(&req).unwrap();
1416 assert_eq!(json["sessionId"], "sess_2");
1417 assert_eq!(json["toolCallId"], "tc_1");
1418 assert_eq!(json["mode"], "url");
1419 assert_eq!(json["elicitationId"], "elic_1");
1420 assert_eq!(json["url"], "https://example.com/auth");
1421 assert_eq!(json["message"], "Please authenticate");
1422
1423 let roundtripped: CreateElicitationRequest = serde_json::from_value(json).unwrap();
1424 assert_eq!(
1425 *roundtripped.scope(),
1426 ElicitationSessionScope::new("sess_2")
1427 .tool_call_id("tc_1")
1428 .into()
1429 );
1430 assert!(matches!(roundtripped.mode, ElicitationMode::Url(_)));
1431 }
1432
1433 #[test]
1434 fn response_accept_serialization() {
1435 let resp = CreateElicitationResponse::new(ElicitationAction::Accept(
1436 ElicitationAcceptAction::new().content(BTreeMap::from([(
1437 "name".to_string(),
1438 ElicitationContentValue::from("Alice"),
1439 )])),
1440 ));
1441
1442 let json = serde_json::to_value(&resp).unwrap();
1443 assert_eq!(json["action"], "accept");
1444 assert_eq!(json["content"]["name"], "Alice");
1445
1446 let roundtripped: CreateElicitationResponse = serde_json::from_value(json).unwrap();
1447 assert!(matches!(
1448 roundtripped.action,
1449 ElicitationAction::Accept(ElicitationAcceptAction {
1450 content: Some(_),
1451 ..
1452 })
1453 ));
1454 }
1455
1456 #[test]
1457 fn response_decline_serialization() {
1458 let resp = CreateElicitationResponse::new(ElicitationAction::Decline);
1459
1460 let json = serde_json::to_value(&resp).unwrap();
1461 assert_eq!(json["action"], "decline");
1462
1463 let roundtripped: CreateElicitationResponse = serde_json::from_value(json).unwrap();
1464 assert!(matches!(roundtripped.action, ElicitationAction::Decline));
1465 }
1466
1467 #[test]
1468 fn response_cancel_serialization() {
1469 let resp = CreateElicitationResponse::new(ElicitationAction::Cancel);
1470
1471 let json = serde_json::to_value(&resp).unwrap();
1472 assert_eq!(json["action"], "cancel");
1473
1474 let roundtripped: CreateElicitationResponse = serde_json::from_value(json).unwrap();
1475 assert!(matches!(roundtripped.action, ElicitationAction::Cancel));
1476 }
1477
1478 #[test]
1479 fn url_mode_request_scope_serialization() {
1480 let req = CreateElicitationRequest::new(
1481 ElicitationUrlMode::new(
1482 ElicitationRequestScope::new(RequestId::Number(42)),
1483 "elic_2",
1484 "https://example.com/setup",
1485 ),
1486 "Please complete setup",
1487 );
1488
1489 let json = serde_json::to_value(&req).unwrap();
1490 assert_eq!(json["requestId"], 42);
1491 assert!(json.get("sessionId").is_none());
1492 assert_eq!(json["mode"], "url");
1493 assert_eq!(json["elicitationId"], "elic_2");
1494 assert_eq!(json["url"], "https://example.com/setup");
1495 assert_eq!(json["message"], "Please complete setup");
1496
1497 let roundtripped: CreateElicitationRequest = serde_json::from_value(json).unwrap();
1498 assert_eq!(
1499 *roundtripped.scope(),
1500 ElicitationRequestScope::new(RequestId::Number(42)).into()
1501 );
1502 assert!(matches!(roundtripped.mode, ElicitationMode::Url(_)));
1503 }
1504
1505 #[test]
1506 fn request_scope_request_serialization() {
1507 let req = CreateElicitationRequest::new(
1508 ElicitationFormMode::new(
1509 ElicitationRequestScope::new(RequestId::Number(99)),
1510 ElicitationSchema::new().string("workspace", true),
1511 ),
1512 "Enter workspace name",
1513 );
1514
1515 let json = serde_json::to_value(&req).unwrap();
1516 assert_eq!(json["requestId"], 99);
1517 assert!(json.get("sessionId").is_none());
1518
1519 let roundtripped: CreateElicitationRequest = serde_json::from_value(json).unwrap();
1520 assert_eq!(
1521 *roundtripped.scope(),
1522 ElicitationRequestScope::new(RequestId::Number(99)).into()
1523 );
1524 }
1525
1526 #[test]
1530 fn client_response_serialization_accept() {
1531 use crate::v2::ClientResponse;
1532
1533 let resp = ClientResponse::CreateElicitationResponse(CreateElicitationResponse::new(
1534 ElicitationAction::Accept(ElicitationAcceptAction::new().content(BTreeMap::from([(
1535 "name".to_string(),
1536 ElicitationContentValue::from("Alice"),
1537 )]))),
1538 ));
1539 let json = serde_json::to_value(&resp).unwrap();
1540 assert_eq!(json["action"], "accept");
1541 assert_eq!(json["content"]["name"], "Alice");
1542
1543 let roundtripped: CreateElicitationResponse = serde_json::from_value(json).unwrap();
1545 assert!(matches!(roundtripped.action, ElicitationAction::Accept(_)));
1546 }
1547
1548 #[test]
1549 fn client_response_serialization_decline() {
1550 use crate::v2::ClientResponse;
1551
1552 let resp = ClientResponse::CreateElicitationResponse(CreateElicitationResponse::new(
1553 ElicitationAction::Decline,
1554 ));
1555 let json = serde_json::to_value(&resp).unwrap();
1556 assert_eq!(json["action"], "decline");
1557
1558 let roundtripped: CreateElicitationResponse = serde_json::from_value(json).unwrap();
1559 assert!(matches!(roundtripped.action, ElicitationAction::Decline));
1560 }
1561
1562 #[test]
1563 fn client_response_serialization_cancel() {
1564 use crate::v2::ClientResponse;
1565
1566 let resp = ClientResponse::CreateElicitationResponse(CreateElicitationResponse::new(
1567 ElicitationAction::Cancel,
1568 ));
1569 let json = serde_json::to_value(&resp).unwrap();
1570 assert_eq!(json["action"], "cancel");
1571
1572 let roundtripped: CreateElicitationResponse = serde_json::from_value(json).unwrap();
1573 assert!(matches!(roundtripped.action, ElicitationAction::Cancel));
1574 }
1575
1576 #[test]
1579 fn request_tolerates_extra_fields() {
1580 let json = json!({
1581 "sessionId": "sess_1",
1582 "mode": "form",
1583 "message": "Enter your name",
1584 "requestedSchema": {
1585 "type": "object",
1586 "properties": {
1587 "name": { "type": "string", "title": "Name" }
1588 },
1589 "required": ["name"]
1590 },
1591 "unknownStringField": "hello",
1592 "unknownNumberField": 42
1593 });
1594
1595 let req: CreateElicitationRequest = serde_json::from_value(json).unwrap();
1596 assert_eq!(*req.scope(), ElicitationSessionScope::new("sess_1").into());
1597 assert_eq!(req.message, "Enter your name");
1598 assert!(matches!(req.mode, ElicitationMode::Form(_)));
1599 }
1600
1601 #[test]
1602 fn completion_notification_serialization() {
1603 let notif = CompleteElicitationNotification::new("elic_1");
1604
1605 let json = serde_json::to_value(¬if).unwrap();
1606 assert_eq!(json["elicitationId"], "elic_1");
1607
1608 let roundtripped: CompleteElicitationNotification = serde_json::from_value(json).unwrap();
1609 assert_eq!(roundtripped.elicitation_id, ElicitationId::new("elic_1"));
1610 }
1611
1612 #[test]
1613 fn capabilities_form_only() {
1614 let caps = ElicitationCapabilities::new().form(ElicitationFormCapabilities::new());
1615
1616 let json = serde_json::to_value(&caps).unwrap();
1617 assert!(json["form"].is_object());
1618 assert!(json.get("url").is_none());
1619
1620 let roundtripped: ElicitationCapabilities = serde_json::from_value(json).unwrap();
1621 assert!(roundtripped.form.is_some());
1622 assert!(roundtripped.url.is_none());
1623 }
1624
1625 #[test]
1626 fn capabilities_url_only() {
1627 let caps = ElicitationCapabilities::new().url(ElicitationUrlCapabilities::new());
1628
1629 let json = serde_json::to_value(&caps).unwrap();
1630 assert!(json.get("form").is_none());
1631 assert!(json["url"].is_object());
1632
1633 let roundtripped: ElicitationCapabilities = serde_json::from_value(json).unwrap();
1634 assert!(roundtripped.form.is_none());
1635 assert!(roundtripped.url.is_some());
1636 }
1637
1638 #[test]
1639 fn capabilities_both() {
1640 let caps = ElicitationCapabilities::new()
1641 .form(ElicitationFormCapabilities::new())
1642 .url(ElicitationUrlCapabilities::new());
1643
1644 let json = serde_json::to_value(&caps).unwrap();
1645 assert!(json["form"].is_object());
1646 assert!(json["url"].is_object());
1647
1648 let roundtripped: ElicitationCapabilities = serde_json::from_value(json).unwrap();
1649 assert!(roundtripped.form.is_some());
1650 assert!(roundtripped.url.is_some());
1651 }
1652
1653 #[test]
1654 fn url_elicitation_required_data_serialization() {
1655 let data = UrlElicitationRequiredData::new(vec![UrlElicitationRequiredItem::new(
1656 "elic_1",
1657 "https://example.com/auth",
1658 "Please authenticate",
1659 )]);
1660
1661 let json = serde_json::to_value(&data).unwrap();
1662 assert_eq!(json["elicitations"][0]["mode"], "url");
1663 assert_eq!(json["elicitations"][0]["elicitationId"], "elic_1");
1664 assert_eq!(json["elicitations"][0]["url"], "https://example.com/auth");
1665
1666 let roundtripped: UrlElicitationRequiredData = serde_json::from_value(json).unwrap();
1667 assert_eq!(roundtripped.elicitations.len(), 1);
1668 assert_eq!(
1669 roundtripped.elicitations[0].mode,
1670 ElicitationUrlOnlyMode::Url
1671 );
1672 }
1673
1674 #[test]
1675 fn schema_default_sets_object_type() {
1676 let schema = ElicitationSchema::default();
1677
1678 assert_eq!(schema.type_, ElicitationSchemaType::Object);
1679 assert!(schema.properties.is_empty());
1680
1681 let json = serde_json::to_value(&schema).unwrap();
1682 assert_eq!(json["type"], "object");
1683 }
1684
1685 #[test]
1686 fn schema_builder_serialization() {
1687 let schema = ElicitationSchema::new()
1688 .string("name", true)
1689 .email("email", true)
1690 .integer("age", 0, 150, true)
1691 .boolean("newsletter", false)
1692 .description("User registration");
1693
1694 let json = serde_json::to_value(&schema).unwrap();
1695 assert_eq!(json["type"], "object");
1696 assert_eq!(json["description"], "User registration");
1697 assert_eq!(json["properties"]["name"]["type"], "string");
1698 assert_eq!(json["properties"]["email"]["type"], "string");
1699 assert_eq!(json["properties"]["email"]["format"], "email");
1700 assert_eq!(json["properties"]["age"]["type"], "integer");
1701 assert_eq!(json["properties"]["age"]["minimum"], 0);
1702 assert_eq!(json["properties"]["age"]["maximum"], 150);
1703 assert_eq!(json["properties"]["newsletter"]["type"], "boolean");
1704
1705 let required = json["required"].as_array().unwrap();
1706 assert!(required.contains(&json!("name")));
1707 assert!(required.contains(&json!("email")));
1708 assert!(required.contains(&json!("age")));
1709 assert!(!required.contains(&json!("newsletter")));
1710
1711 let roundtripped: ElicitationSchema = serde_json::from_value(json).unwrap();
1712 assert_eq!(roundtripped.properties.len(), 4);
1713 assert!(roundtripped.required.unwrap().contains(&"name".to_string()));
1714 }
1715
1716 #[test]
1717 fn schema_string_enum_serialization() {
1718 let schema = ElicitationSchema::new().property(
1719 "color",
1720 StringPropertySchema::new().enum_values(vec![
1721 "red".into(),
1722 "green".into(),
1723 "blue".into(),
1724 ]),
1725 true,
1726 );
1727
1728 let json = serde_json::to_value(&schema).unwrap();
1729 assert_eq!(json["properties"]["color"]["type"], "string");
1730 let enum_vals = json["properties"]["color"]["enum"].as_array().unwrap();
1731 assert_eq!(enum_vals.len(), 3);
1732
1733 let roundtripped: ElicitationSchema = serde_json::from_value(json).unwrap();
1734 if let ElicitationPropertySchema::String(s) = roundtripped.properties.get("color").unwrap()
1735 {
1736 assert_eq!(s.enum_values.as_ref().unwrap().len(), 3);
1737 } else {
1738 panic!("expected String variant");
1739 }
1740 }
1741
1742 #[test]
1743 fn schema_multi_select_serialization() {
1744 let schema = ElicitationSchema::new().property(
1745 "colors",
1746 MultiSelectPropertySchema::new(vec!["red".into(), "green".into(), "blue".into()])
1747 .min_items(1)
1748 .max_items(3),
1749 false,
1750 );
1751
1752 let json = serde_json::to_value(&schema).unwrap();
1753 assert_eq!(json["properties"]["colors"]["type"], "array");
1754 assert_eq!(json["properties"]["colors"]["items"]["type"], "string");
1755 assert_eq!(json["properties"]["colors"]["minItems"], 1);
1756 assert_eq!(json["properties"]["colors"]["maxItems"], 3);
1757
1758 let roundtripped: ElicitationSchema = serde_json::from_value(json).unwrap();
1759 assert!(matches!(
1760 roundtripped.properties.get("colors").unwrap(),
1761 ElicitationPropertySchema::Array(_)
1762 ));
1763 }
1764
1765 #[test]
1766 fn schema_titled_enum_serialization() {
1767 let schema = ElicitationSchema::new().property(
1768 "country",
1769 StringPropertySchema::new().one_of(vec![
1770 EnumOption::new("us", "United States"),
1771 EnumOption::new("uk", "United Kingdom"),
1772 ]),
1773 true,
1774 );
1775
1776 let json = serde_json::to_value(&schema).unwrap();
1777 assert_eq!(json["properties"]["country"]["type"], "string");
1778 let one_of = json["properties"]["country"]["oneOf"].as_array().unwrap();
1779 assert_eq!(one_of.len(), 2);
1780 assert_eq!(one_of[0]["const"], "us");
1781 assert_eq!(one_of[0]["title"], "United States");
1782
1783 let roundtripped: ElicitationSchema = serde_json::from_value(json).unwrap();
1784 if let ElicitationPropertySchema::String(s) =
1785 roundtripped.properties.get("country").unwrap()
1786 {
1787 assert_eq!(s.one_of.as_ref().unwrap().len(), 2);
1788 } else {
1789 panic!("expected String variant");
1790 }
1791 }
1792
1793 #[test]
1794 fn schema_number_property_serialization() {
1795 let schema = ElicitationSchema::new().number("rating", 0.0, 5.0, true);
1796
1797 let json = serde_json::to_value(&schema).unwrap();
1798 assert_eq!(json["properties"]["rating"]["type"], "number");
1799 assert_eq!(json["properties"]["rating"]["minimum"], 0.0);
1800 assert_eq!(json["properties"]["rating"]["maximum"], 5.0);
1801
1802 let roundtripped: ElicitationSchema = serde_json::from_value(json).unwrap();
1803 if let ElicitationPropertySchema::Number(n) = roundtripped.properties.get("rating").unwrap()
1804 {
1805 assert_eq!(n.minimum, Some(0.0));
1806 assert_eq!(n.maximum, Some(5.0));
1807 } else {
1808 panic!("expected Number variant");
1809 }
1810 }
1811
1812 #[test]
1813 fn schema_string_format_serialization() {
1814 let schema = ElicitationSchema::new()
1815 .uri("website", true)
1816 .date("birthday", true)
1817 .date_time("updated_at", false);
1818
1819 let json = serde_json::to_value(&schema).unwrap();
1820 assert_eq!(json["properties"]["website"]["type"], "string");
1821 assert_eq!(json["properties"]["website"]["format"], "uri");
1822 assert_eq!(json["properties"]["birthday"]["type"], "string");
1823 assert_eq!(json["properties"]["birthday"]["format"], "date");
1824 assert_eq!(json["properties"]["updated_at"]["type"], "string");
1825 assert_eq!(json["properties"]["updated_at"]["format"], "date-time");
1826
1827 let required = json["required"].as_array().unwrap();
1828 assert!(required.contains(&json!("website")));
1829 assert!(required.contains(&json!("birthday")));
1830 assert!(!required.contains(&json!("updated_at")));
1831 }
1832
1833 #[test]
1834 fn schema_string_pattern_serialization() {
1835 let schema = ElicitationSchema::new().property(
1836 "name",
1837 StringPropertySchema::new()
1838 .min_length(1)
1839 .max_length(64)
1840 .pattern("^[a-zA-Z_][a-zA-Z0-9_]*$"),
1841 true,
1842 );
1843
1844 let json = serde_json::to_value(&schema).unwrap();
1845 assert_eq!(json["properties"]["name"]["type"], "string");
1846 assert_eq!(
1847 json["properties"]["name"]["pattern"],
1848 "^[a-zA-Z_][a-zA-Z0-9_]*$"
1849 );
1850
1851 let roundtripped: ElicitationSchema = serde_json::from_value(json).unwrap();
1852 if let ElicitationPropertySchema::String(s) = roundtripped.properties.get("name").unwrap() {
1853 assert_eq!(s.pattern.as_deref(), Some("^[a-zA-Z_][a-zA-Z0-9_]*$"));
1854 } else {
1855 panic!("expected String variant");
1856 }
1857 }
1858
1859 #[test]
1860 fn schema_property_updates_required_state() {
1861 let schema = ElicitationSchema::new()
1862 .string("name", true)
1863 .email("name", false);
1864
1865 let json = serde_json::to_value(&schema).unwrap();
1866 assert!(json.get("required").is_none());
1867 assert_eq!(json["properties"]["name"]["format"], "email");
1868 }
1869
1870 #[test]
1871 fn schema_rejects_invalid_object_type() {
1872 let err = serde_json::from_value::<ElicitationSchema>(json!({
1873 "type": "array",
1874 "properties": {
1875 "name": {
1876 "type": "string"
1877 }
1878 }
1879 }))
1880 .unwrap_err();
1881
1882 assert!(err.to_string().contains("unknown variant"));
1883 }
1884
1885 #[test]
1886 fn titled_multi_select_items_reject_one_of() {
1887 let err = serde_json::from_value::<TitledMultiSelectItems>(json!({
1888 "oneOf": [
1889 {
1890 "const": "red",
1891 "title": "Red"
1892 }
1893 ]
1894 }))
1895 .unwrap_err();
1896
1897 assert!(err.to_string().contains("missing field `anyOf`"));
1898 }
1899
1900 #[test]
1901 fn response_accept_rejects_non_object_content() {
1902 let err = serde_json::from_value::<CreateElicitationResponse>(json!({
1903 "action": "accept",
1904 "content": "Alice"
1905 }))
1906 .unwrap_err();
1907
1908 assert!(err.to_string().contains("invalid type"));
1909 }
1910
1911 #[test]
1912 fn response_accept_rejects_nested_object_content() {
1913 let err = serde_json::from_value::<CreateElicitationResponse>(json!({
1914 "action": "accept",
1915 "content": {
1916 "profile": {
1917 "name": "Alice"
1918 }
1919 }
1920 }))
1921 .unwrap_err();
1922
1923 assert!(err.to_string().contains("data did not match any variant"));
1924 }
1925
1926 #[test]
1927 fn response_accept_allows_primitive_and_string_array_content() {
1928 let response = CreateElicitationResponse::new(ElicitationAction::Accept(
1929 ElicitationAcceptAction::new().content(BTreeMap::from([
1930 ("name".to_string(), ElicitationContentValue::from("Alice")),
1931 ("age".to_string(), ElicitationContentValue::from(30_i32)),
1932 ("score".to_string(), ElicitationContentValue::from(9.5_f64)),
1933 (
1934 "subscribed".to_string(),
1935 ElicitationContentValue::from(true),
1936 ),
1937 (
1938 "tags".to_string(),
1939 ElicitationContentValue::from(vec!["rust", "acp"]),
1940 ),
1941 ])),
1942 ));
1943
1944 let json = serde_json::to_value(&response).unwrap();
1945 assert_eq!(json["action"], "accept");
1946 assert_eq!(json["content"]["name"], "Alice");
1947 assert_eq!(json["content"]["age"], 30);
1948 assert_eq!(json["content"]["score"], 9.5);
1949 assert_eq!(json["content"]["subscribed"], true);
1950 assert_eq!(json["content"]["tags"][0], "rust");
1951 assert_eq!(json["content"]["tags"][1], "acp");
1952 }
1953}