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 crate::IntoOption;
16
17use super::{
18 ELICITATION_COMPLETE_NOTIFICATION, ELICITATION_CREATE_METHOD_NAME, Meta, RequestId, SessionId,
19 ToolCallId,
20};
21
22#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Hash, Display, From)]
28#[serde(transparent)]
29#[from(Arc<str>, String, &'static str)]
30#[non_exhaustive]
31pub struct ElicitationId(pub Arc<str>);
32
33impl ElicitationId {
34 #[must_use]
36 pub fn new(id: impl Into<Arc<str>>) -> Self {
37 Self(id.into())
38 }
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
43#[serde(rename_all = "kebab-case")]
44#[non_exhaustive]
45pub enum StringFormat {
46 Email,
48 Uri,
50 Date,
52 DateTime,
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
58#[serde(rename_all = "snake_case")]
59#[non_exhaustive]
60pub enum ElicitationSchemaType {
61 #[default]
63 Object,
64}
65
66#[skip_serializing_none]
68#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
69#[non_exhaustive]
70pub struct EnumOption {
71 #[serde(rename = "const")]
73 pub value: String,
74 pub title: String,
76 #[serde(rename = "_meta")]
82 pub meta: Option<Meta>,
83}
84
85impl EnumOption {
86 #[must_use]
88 pub fn new(value: impl Into<String>, title: impl Into<String>) -> Self {
89 Self {
90 value: value.into(),
91 title: title.into(),
92 meta: None,
93 }
94 }
95
96 #[must_use]
102 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
103 self.meta = meta.into_option();
104 self
105 }
106}
107
108#[skip_serializing_none]
113#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
114#[serde(rename_all = "camelCase")]
115#[non_exhaustive]
116pub struct StringPropertySchema {
117 pub title: Option<String>,
119 pub description: Option<String>,
121 pub min_length: Option<u32>,
123 pub max_length: Option<u32>,
125 pub pattern: Option<String>,
127 pub format: Option<StringFormat>,
129 pub default: Option<String>,
131 #[serde(rename = "enum")]
133 pub enum_values: Option<Vec<String>>,
134 #[serde(rename = "oneOf")]
136 pub one_of: Option<Vec<EnumOption>>,
137 #[serde(rename = "_meta")]
143 pub meta: Option<Meta>,
144}
145
146impl StringPropertySchema {
147 #[must_use]
149 pub fn new() -> Self {
150 Self::default()
151 }
152
153 #[must_use]
155 pub fn email() -> Self {
156 Self {
157 format: Some(StringFormat::Email),
158 ..Default::default()
159 }
160 }
161
162 #[must_use]
164 pub fn uri() -> Self {
165 Self {
166 format: Some(StringFormat::Uri),
167 ..Default::default()
168 }
169 }
170
171 #[must_use]
173 pub fn date() -> Self {
174 Self {
175 format: Some(StringFormat::Date),
176 ..Default::default()
177 }
178 }
179
180 #[must_use]
182 pub fn date_time() -> Self {
183 Self {
184 format: Some(StringFormat::DateTime),
185 ..Default::default()
186 }
187 }
188
189 #[must_use]
191 pub fn title(mut self, title: impl IntoOption<String>) -> Self {
192 self.title = title.into_option();
193 self
194 }
195
196 #[must_use]
198 pub fn description(mut self, description: impl IntoOption<String>) -> Self {
199 self.description = description.into_option();
200 self
201 }
202
203 #[must_use]
205 pub fn min_length(mut self, min_length: impl IntoOption<u32>) -> Self {
206 self.min_length = min_length.into_option();
207 self
208 }
209
210 #[must_use]
212 pub fn max_length(mut self, max_length: impl IntoOption<u32>) -> Self {
213 self.max_length = max_length.into_option();
214 self
215 }
216
217 #[must_use]
219 pub fn pattern(mut self, pattern: impl IntoOption<String>) -> Self {
220 self.pattern = pattern.into_option();
221 self
222 }
223
224 #[must_use]
226 pub fn format(mut self, format: impl IntoOption<StringFormat>) -> Self {
227 self.format = format.into_option();
228 self
229 }
230
231 #[must_use]
233 pub fn default_value(mut self, default: impl IntoOption<String>) -> Self {
234 self.default = default.into_option();
235 self
236 }
237
238 #[must_use]
240 pub fn enum_values(mut self, enum_values: impl IntoOption<Vec<String>>) -> Self {
241 self.enum_values = enum_values.into_option();
242 self
243 }
244
245 #[must_use]
247 pub fn one_of(mut self, one_of: impl IntoOption<Vec<EnumOption>>) -> Self {
248 self.one_of = one_of.into_option();
249 self
250 }
251
252 #[must_use]
258 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
259 self.meta = meta.into_option();
260 self
261 }
262}
263
264#[skip_serializing_none]
266#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
267#[serde(rename_all = "camelCase")]
268#[non_exhaustive]
269pub struct NumberPropertySchema {
270 pub title: Option<String>,
272 pub description: Option<String>,
274 pub minimum: Option<f64>,
276 pub maximum: Option<f64>,
278 pub default: Option<f64>,
280 #[serde(rename = "_meta")]
286 pub meta: Option<Meta>,
287}
288
289impl NumberPropertySchema {
290 #[must_use]
292 pub fn new() -> Self {
293 Self::default()
294 }
295
296 #[must_use]
298 pub fn title(mut self, title: impl IntoOption<String>) -> Self {
299 self.title = title.into_option();
300 self
301 }
302
303 #[must_use]
305 pub fn description(mut self, description: impl IntoOption<String>) -> Self {
306 self.description = description.into_option();
307 self
308 }
309
310 #[must_use]
312 pub fn minimum(mut self, minimum: impl IntoOption<f64>) -> Self {
313 self.minimum = minimum.into_option();
314 self
315 }
316
317 #[must_use]
319 pub fn maximum(mut self, maximum: impl IntoOption<f64>) -> Self {
320 self.maximum = maximum.into_option();
321 self
322 }
323
324 #[must_use]
326 pub fn default_value(mut self, default: impl IntoOption<f64>) -> Self {
327 self.default = default.into_option();
328 self
329 }
330
331 #[must_use]
337 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
338 self.meta = meta.into_option();
339 self
340 }
341}
342
343#[skip_serializing_none]
345#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
346#[serde(rename_all = "camelCase")]
347#[non_exhaustive]
348pub struct IntegerPropertySchema {
349 pub title: Option<String>,
351 pub description: Option<String>,
353 pub minimum: Option<i64>,
355 pub maximum: Option<i64>,
357 pub default: Option<i64>,
359 #[serde(rename = "_meta")]
365 pub meta: Option<Meta>,
366}
367
368impl IntegerPropertySchema {
369 #[must_use]
371 pub fn new() -> Self {
372 Self::default()
373 }
374
375 #[must_use]
377 pub fn title(mut self, title: impl IntoOption<String>) -> Self {
378 self.title = title.into_option();
379 self
380 }
381
382 #[must_use]
384 pub fn description(mut self, description: impl IntoOption<String>) -> Self {
385 self.description = description.into_option();
386 self
387 }
388
389 #[must_use]
391 pub fn minimum(mut self, minimum: impl IntoOption<i64>) -> Self {
392 self.minimum = minimum.into_option();
393 self
394 }
395
396 #[must_use]
398 pub fn maximum(mut self, maximum: impl IntoOption<i64>) -> Self {
399 self.maximum = maximum.into_option();
400 self
401 }
402
403 #[must_use]
405 pub fn default_value(mut self, default: impl IntoOption<i64>) -> Self {
406 self.default = default.into_option();
407 self
408 }
409
410 #[must_use]
416 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
417 self.meta = meta.into_option();
418 self
419 }
420}
421
422#[skip_serializing_none]
424#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
425#[serde(rename_all = "camelCase")]
426#[non_exhaustive]
427pub struct BooleanPropertySchema {
428 pub title: Option<String>,
430 pub description: Option<String>,
432 pub default: Option<bool>,
434 #[serde(rename = "_meta")]
440 pub meta: Option<Meta>,
441}
442
443impl BooleanPropertySchema {
444 #[must_use]
446 pub fn new() -> Self {
447 Self::default()
448 }
449
450 #[must_use]
452 pub fn title(mut self, title: impl IntoOption<String>) -> Self {
453 self.title = title.into_option();
454 self
455 }
456
457 #[must_use]
459 pub fn description(mut self, description: impl IntoOption<String>) -> Self {
460 self.description = description.into_option();
461 self
462 }
463
464 #[must_use]
466 pub fn default_value(mut self, default: impl IntoOption<bool>) -> Self {
467 self.default = default.into_option();
468 self
469 }
470
471 #[must_use]
477 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
478 self.meta = meta.into_option();
479 self
480 }
481}
482
483#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
485#[serde(rename_all = "snake_case")]
486#[non_exhaustive]
487pub enum ElicitationStringType {
488 #[default]
490 String,
491}
492
493#[skip_serializing_none]
495#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
496#[non_exhaustive]
497pub struct UntitledMultiSelectItems {
498 #[serde(rename = "type")]
500 pub type_: ElicitationStringType,
501 #[serde(rename = "enum")]
503 pub values: Vec<String>,
504 #[serde(rename = "_meta")]
510 pub meta: Option<Meta>,
511}
512
513impl UntitledMultiSelectItems {
514 #[must_use]
516 pub fn new(type_: ElicitationStringType, values: Vec<String>) -> Self {
517 Self {
518 type_,
519 values,
520 meta: None,
521 }
522 }
523
524 #[must_use]
530 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
531 self.meta = meta.into_option();
532 self
533 }
534}
535
536#[skip_serializing_none]
538#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
539#[non_exhaustive]
540pub struct TitledMultiSelectItems {
541 #[serde(rename = "anyOf")]
543 pub options: Vec<EnumOption>,
544 #[serde(rename = "_meta")]
550 pub meta: Option<Meta>,
551}
552
553impl TitledMultiSelectItems {
554 #[must_use]
556 pub fn new(options: Vec<EnumOption>) -> Self {
557 Self {
558 options,
559 meta: None,
560 }
561 }
562
563 #[must_use]
569 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
570 self.meta = meta.into_option();
571 self
572 }
573}
574
575#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
577#[serde(untagged)]
578#[non_exhaustive]
579pub enum MultiSelectItems {
580 Untitled(UntitledMultiSelectItems),
582 Titled(TitledMultiSelectItems),
584}
585
586#[skip_serializing_none]
588#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
589#[serde(rename_all = "camelCase")]
590#[non_exhaustive]
591pub struct MultiSelectPropertySchema {
592 pub title: Option<String>,
594 pub description: Option<String>,
596 pub min_items: Option<u64>,
598 pub max_items: Option<u64>,
600 pub items: MultiSelectItems,
602 pub default: Option<Vec<String>>,
604 #[serde(rename = "_meta")]
610 pub meta: Option<Meta>,
611}
612
613impl MultiSelectPropertySchema {
614 #[must_use]
616 pub fn new(values: Vec<String>) -> Self {
617 Self {
618 title: None,
619 description: None,
620 min_items: None,
621 max_items: None,
622 items: MultiSelectItems::Untitled(UntitledMultiSelectItems::new(
623 ElicitationStringType::String,
624 values,
625 )),
626 default: None,
627 meta: None,
628 }
629 }
630
631 #[must_use]
633 pub fn titled(options: Vec<EnumOption>) -> Self {
634 Self {
635 title: None,
636 description: None,
637 min_items: None,
638 max_items: None,
639 items: MultiSelectItems::Titled(TitledMultiSelectItems::new(options)),
640 default: None,
641 meta: None,
642 }
643 }
644
645 #[must_use]
647 pub fn title(mut self, title: impl IntoOption<String>) -> Self {
648 self.title = title.into_option();
649 self
650 }
651
652 #[must_use]
654 pub fn description(mut self, description: impl IntoOption<String>) -> Self {
655 self.description = description.into_option();
656 self
657 }
658
659 #[must_use]
661 pub fn min_items(mut self, min_items: impl IntoOption<u64>) -> Self {
662 self.min_items = min_items.into_option();
663 self
664 }
665
666 #[must_use]
668 pub fn max_items(mut self, max_items: impl IntoOption<u64>) -> Self {
669 self.max_items = max_items.into_option();
670 self
671 }
672
673 #[must_use]
675 pub fn default_value(mut self, default: impl IntoOption<Vec<String>>) -> Self {
676 self.default = default.into_option();
677 self
678 }
679
680 #[must_use]
686 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
687 self.meta = meta.into_option();
688 self
689 }
690}
691
692#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
698#[serde(tag = "type", rename_all = "snake_case")]
699#[schemars(extend("discriminator" = {"propertyName": "type"}))]
700#[non_exhaustive]
701pub enum ElicitationPropertySchema {
702 String(StringPropertySchema),
704 Number(NumberPropertySchema),
706 Integer(IntegerPropertySchema),
708 Boolean(BooleanPropertySchema),
710 Array(MultiSelectPropertySchema),
712}
713
714impl From<StringPropertySchema> for ElicitationPropertySchema {
715 fn from(schema: StringPropertySchema) -> Self {
716 Self::String(schema)
717 }
718}
719
720impl From<NumberPropertySchema> for ElicitationPropertySchema {
721 fn from(schema: NumberPropertySchema) -> Self {
722 Self::Number(schema)
723 }
724}
725
726impl From<IntegerPropertySchema> for ElicitationPropertySchema {
727 fn from(schema: IntegerPropertySchema) -> Self {
728 Self::Integer(schema)
729 }
730}
731
732impl From<BooleanPropertySchema> for ElicitationPropertySchema {
733 fn from(schema: BooleanPropertySchema) -> Self {
734 Self::Boolean(schema)
735 }
736}
737
738impl From<MultiSelectPropertySchema> for ElicitationPropertySchema {
739 fn from(schema: MultiSelectPropertySchema) -> Self {
740 Self::Array(schema)
741 }
742}
743
744fn default_object_type() -> ElicitationSchemaType {
745 ElicitationSchemaType::Object
746}
747
748#[skip_serializing_none]
753#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
754#[serde(rename_all = "camelCase")]
755#[non_exhaustive]
756pub struct ElicitationSchema {
757 #[serde(rename = "type", default = "default_object_type")]
759 pub type_: ElicitationSchemaType,
760 pub title: Option<String>,
762 #[serde(default)]
764 pub properties: BTreeMap<String, ElicitationPropertySchema>,
765 pub required: Option<Vec<String>>,
767 pub description: Option<String>,
769 #[serde(rename = "_meta")]
775 pub meta: Option<Meta>,
776}
777
778impl Default for ElicitationSchema {
779 fn default() -> Self {
780 Self {
781 type_: default_object_type(),
782 title: None,
783 properties: BTreeMap::new(),
784 required: None,
785 description: None,
786 meta: None,
787 }
788 }
789}
790
791impl ElicitationSchema {
792 #[must_use]
794 pub fn new() -> Self {
795 Self::default()
796 }
797
798 #[must_use]
800 pub fn title(mut self, title: impl IntoOption<String>) -> Self {
801 self.title = title.into_option();
802 self
803 }
804
805 #[must_use]
807 pub fn description(mut self, description: impl IntoOption<String>) -> Self {
808 self.description = description.into_option();
809 self
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 #[must_use]
825 pub fn property<S>(mut self, name: impl Into<String>, schema: S, required: bool) -> Self
826 where
827 S: Into<ElicitationPropertySchema>,
828 {
829 let name = name.into();
830 self.properties.insert(name.clone(), schema.into());
831
832 if required {
833 let required_fields = self.required.get_or_insert_with(Vec::new);
834 if !required_fields.contains(&name) {
835 required_fields.push(name);
836 }
837 } else if let Some(required_fields) = &mut self.required {
838 required_fields.retain(|field| field != &name);
839
840 if required_fields.is_empty() {
841 self.required = None;
842 }
843 }
844
845 self
846 }
847
848 #[must_use]
850 pub fn string(self, name: impl Into<String>, required: bool) -> Self {
851 self.property(name, StringPropertySchema::new(), required)
852 }
853
854 #[must_use]
856 pub fn email(self, name: impl Into<String>, required: bool) -> Self {
857 self.property(name, StringPropertySchema::email(), required)
858 }
859
860 #[must_use]
862 pub fn uri(self, name: impl Into<String>, required: bool) -> Self {
863 self.property(name, StringPropertySchema::uri(), required)
864 }
865
866 #[must_use]
868 pub fn date(self, name: impl Into<String>, required: bool) -> Self {
869 self.property(name, StringPropertySchema::date(), required)
870 }
871
872 #[must_use]
874 pub fn date_time(self, name: impl Into<String>, required: bool) -> Self {
875 self.property(name, StringPropertySchema::date_time(), required)
876 }
877
878 #[must_use]
880 pub fn number(self, name: impl Into<String>, min: f64, max: f64, required: bool) -> Self {
881 self.property(
882 name,
883 NumberPropertySchema::new().minimum(min).maximum(max),
884 required,
885 )
886 }
887
888 #[must_use]
890 pub fn integer(self, name: impl Into<String>, min: i64, max: i64, required: bool) -> Self {
891 self.property(
892 name,
893 IntegerPropertySchema::new().minimum(min).maximum(max),
894 required,
895 )
896 }
897
898 #[must_use]
900 pub fn boolean(self, name: impl Into<String>, required: bool) -> Self {
901 self.property(name, BooleanPropertySchema::new(), required)
902 }
903}
904
905#[serde_as]
911#[skip_serializing_none]
912#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
913#[serde(rename_all = "camelCase")]
914#[non_exhaustive]
915pub struct ElicitationCapabilities {
916 #[serde_as(deserialize_as = "DefaultOnError")]
918 #[schemars(extend("x-deserialize-default-on-error" = true))]
919 #[serde(default)]
920 pub form: Option<ElicitationFormCapabilities>,
921 #[serde_as(deserialize_as = "DefaultOnError")]
923 #[schemars(extend("x-deserialize-default-on-error" = true))]
924 #[serde(default)]
925 pub url: Option<ElicitationUrlCapabilities>,
926 #[serde(rename = "_meta")]
932 pub meta: Option<Meta>,
933}
934
935impl ElicitationCapabilities {
936 #[must_use]
938 pub fn new() -> Self {
939 Self::default()
940 }
941
942 #[must_use]
944 pub fn form(mut self, form: impl IntoOption<ElicitationFormCapabilities>) -> Self {
945 self.form = form.into_option();
946 self
947 }
948
949 #[must_use]
951 pub fn url(mut self, url: impl IntoOption<ElicitationUrlCapabilities>) -> Self {
952 self.url = url.into_option();
953 self
954 }
955
956 #[must_use]
962 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
963 self.meta = meta.into_option();
964 self
965 }
966}
967
968#[skip_serializing_none]
974#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
975#[serde(rename_all = "camelCase")]
976#[non_exhaustive]
977pub struct ElicitationFormCapabilities {
978 #[serde(rename = "_meta")]
984 pub meta: Option<Meta>,
985}
986
987impl ElicitationFormCapabilities {
988 #[must_use]
990 pub fn new() -> Self {
991 Self::default()
992 }
993
994 #[must_use]
1000 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1001 self.meta = meta.into_option();
1002 self
1003 }
1004}
1005
1006#[skip_serializing_none]
1012#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1013#[serde(rename_all = "camelCase")]
1014#[non_exhaustive]
1015pub struct ElicitationUrlCapabilities {
1016 #[serde(rename = "_meta")]
1022 pub meta: Option<Meta>,
1023}
1024
1025impl ElicitationUrlCapabilities {
1026 #[must_use]
1028 pub fn new() -> Self {
1029 Self::default()
1030 }
1031
1032 #[must_use]
1038 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1039 self.meta = meta.into_option();
1040 self
1041 }
1042}
1043
1044#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1050#[serde(untagged)]
1051#[non_exhaustive]
1052pub enum ElicitationScope {
1053 Session(ElicitationSessionScope),
1055 Request(ElicitationRequestScope),
1058}
1059
1060#[skip_serializing_none]
1070#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1071#[serde(rename_all = "camelCase")]
1072#[non_exhaustive]
1073pub struct ElicitationSessionScope {
1074 pub session_id: SessionId,
1076 pub tool_call_id: Option<ToolCallId>,
1078}
1079
1080impl ElicitationSessionScope {
1081 #[must_use]
1083 pub fn new(session_id: impl Into<SessionId>) -> Self {
1084 Self {
1085 session_id: session_id.into(),
1086 tool_call_id: None,
1087 }
1088 }
1089
1090 #[must_use]
1092 pub fn tool_call_id(mut self, tool_call_id: impl IntoOption<ToolCallId>) -> Self {
1093 self.tool_call_id = tool_call_id.into_option();
1094 self
1095 }
1096}
1097
1098#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1105#[serde(rename_all = "camelCase")]
1106#[non_exhaustive]
1107pub struct ElicitationRequestScope {
1108 pub request_id: RequestId,
1110}
1111
1112impl ElicitationRequestScope {
1113 #[must_use]
1115 pub fn new(request_id: impl Into<RequestId>) -> Self {
1116 Self {
1117 request_id: request_id.into(),
1118 }
1119 }
1120}
1121
1122impl From<ElicitationSessionScope> for ElicitationScope {
1123 fn from(scope: ElicitationSessionScope) -> Self {
1124 Self::Session(scope)
1125 }
1126}
1127
1128impl From<ElicitationRequestScope> for ElicitationScope {
1129 fn from(scope: ElicitationRequestScope) -> Self {
1130 Self::Request(scope)
1131 }
1132}
1133
1134#[skip_serializing_none]
1144#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1145#[schemars(extend("x-side" = "client", "x-method" = ELICITATION_CREATE_METHOD_NAME))]
1146#[serde(rename_all = "camelCase")]
1147#[non_exhaustive]
1148pub struct CreateElicitationRequest {
1149 #[serde(flatten)]
1151 pub mode: ElicitationMode,
1152 pub message: String,
1154 #[serde(rename = "_meta")]
1160 pub meta: Option<Meta>,
1161}
1162
1163impl CreateElicitationRequest {
1164 #[must_use]
1166 pub fn new(mode: impl Into<ElicitationMode>, message: impl Into<String>) -> Self {
1167 Self {
1168 mode: mode.into(),
1169 message: message.into(),
1170 meta: None,
1171 }
1172 }
1173
1174 #[must_use]
1176 pub fn scope(&self) -> &ElicitationScope {
1177 self.mode.scope()
1178 }
1179
1180 #[must_use]
1186 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1187 self.meta = meta.into_option();
1188 self
1189 }
1190}
1191
1192#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1198#[serde(tag = "mode", rename_all = "snake_case")]
1199#[schemars(extend("discriminator" = {"propertyName": "mode"}))]
1200#[non_exhaustive]
1201pub enum ElicitationMode {
1202 Form(ElicitationFormMode),
1204 Url(ElicitationUrlMode),
1206}
1207
1208impl From<ElicitationFormMode> for ElicitationMode {
1209 fn from(mode: ElicitationFormMode) -> Self {
1210 Self::Form(mode)
1211 }
1212}
1213
1214impl From<ElicitationUrlMode> for ElicitationMode {
1215 fn from(mode: ElicitationUrlMode) -> Self {
1216 Self::Url(mode)
1217 }
1218}
1219
1220impl ElicitationMode {
1221 #[must_use]
1223 pub fn scope(&self) -> &ElicitationScope {
1224 match self {
1225 Self::Form(f) => &f.scope,
1226 Self::Url(u) => &u.scope,
1227 }
1228 }
1229}
1230
1231#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1237#[serde(rename_all = "camelCase")]
1238#[non_exhaustive]
1239pub struct ElicitationFormMode {
1240 #[serde(flatten)]
1242 pub scope: ElicitationScope,
1243 pub requested_schema: ElicitationSchema,
1245}
1246
1247impl ElicitationFormMode {
1248 #[must_use]
1250 pub fn new(scope: impl Into<ElicitationScope>, requested_schema: ElicitationSchema) -> Self {
1251 Self {
1252 scope: scope.into(),
1253 requested_schema,
1254 }
1255 }
1256}
1257
1258#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1264#[serde(rename_all = "camelCase")]
1265#[non_exhaustive]
1266pub struct ElicitationUrlMode {
1267 #[serde(flatten)]
1269 pub scope: ElicitationScope,
1270 pub elicitation_id: ElicitationId,
1272 #[schemars(extend("format" = "uri"))]
1274 pub url: String,
1275}
1276
1277impl ElicitationUrlMode {
1278 #[must_use]
1280 pub fn new(
1281 scope: impl Into<ElicitationScope>,
1282 elicitation_id: impl Into<ElicitationId>,
1283 url: impl Into<String>,
1284 ) -> Self {
1285 Self {
1286 scope: scope.into(),
1287 elicitation_id: elicitation_id.into(),
1288 url: url.into(),
1289 }
1290 }
1291}
1292
1293#[skip_serializing_none]
1299#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1300#[schemars(extend("x-side" = "client", "x-method" = ELICITATION_CREATE_METHOD_NAME))]
1301#[serde(rename_all = "camelCase")]
1302#[non_exhaustive]
1303pub struct CreateElicitationResponse {
1304 #[serde(flatten)]
1306 pub action: ElicitationAction,
1307 #[serde(rename = "_meta")]
1313 pub meta: Option<Meta>,
1314}
1315
1316impl CreateElicitationResponse {
1317 #[must_use]
1319 pub fn new(action: ElicitationAction) -> Self {
1320 Self { action, meta: None }
1321 }
1322
1323 #[must_use]
1329 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1330 self.meta = meta.into_option();
1331 self
1332 }
1333}
1334
1335#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1341#[serde(tag = "action", rename_all = "snake_case")]
1342#[schemars(extend("discriminator" = {"propertyName": "action"}))]
1343#[non_exhaustive]
1344pub enum ElicitationAction {
1345 Accept(ElicitationAcceptAction),
1347 Decline,
1349 Cancel,
1351}
1352
1353#[skip_serializing_none]
1359#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1360#[serde(rename_all = "camelCase")]
1361#[non_exhaustive]
1362pub struct ElicitationAcceptAction {
1363 #[serde(default)]
1365 pub content: Option<BTreeMap<String, ElicitationContentValue>>,
1366}
1367
1368impl ElicitationAcceptAction {
1369 #[must_use]
1371 pub fn new() -> Self {
1372 Self { content: None }
1373 }
1374
1375 #[must_use]
1377 pub fn content(
1378 mut self,
1379 content: impl IntoOption<BTreeMap<String, ElicitationContentValue>>,
1380 ) -> Self {
1381 self.content = content.into_option();
1382 self
1383 }
1384}
1385
1386#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1388#[serde(untagged)]
1389#[non_exhaustive]
1390pub enum ElicitationContentValue {
1391 String(String),
1393 Integer(i64),
1395 Number(f64),
1397 Boolean(bool),
1399 StringArray(Vec<String>),
1401}
1402
1403impl From<String> for ElicitationContentValue {
1404 fn from(value: String) -> Self {
1405 Self::String(value)
1406 }
1407}
1408
1409impl From<&str> for ElicitationContentValue {
1410 fn from(value: &str) -> Self {
1411 Self::String(value.to_string())
1412 }
1413}
1414
1415impl From<i64> for ElicitationContentValue {
1416 fn from(value: i64) -> Self {
1417 Self::Integer(value)
1418 }
1419}
1420
1421impl From<i32> for ElicitationContentValue {
1422 fn from(value: i32) -> Self {
1423 Self::Integer(i64::from(value))
1424 }
1425}
1426
1427impl From<f64> for ElicitationContentValue {
1428 fn from(value: f64) -> Self {
1429 Self::Number(value)
1430 }
1431}
1432
1433impl From<bool> for ElicitationContentValue {
1434 fn from(value: bool) -> Self {
1435 Self::Boolean(value)
1436 }
1437}
1438
1439impl From<Vec<String>> for ElicitationContentValue {
1440 fn from(value: Vec<String>) -> Self {
1441 Self::StringArray(value)
1442 }
1443}
1444
1445impl From<Vec<&str>> for ElicitationContentValue {
1446 fn from(value: Vec<&str>) -> Self {
1447 Self::StringArray(value.into_iter().map(str::to_string).collect())
1448 }
1449}
1450
1451impl Default for ElicitationAcceptAction {
1452 fn default() -> Self {
1453 Self::new()
1454 }
1455}
1456
1457#[skip_serializing_none]
1463#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1464#[schemars(extend("x-side" = "client", "x-method" = ELICITATION_COMPLETE_NOTIFICATION))]
1465#[serde(rename_all = "camelCase")]
1466#[non_exhaustive]
1467pub struct CompleteElicitationNotification {
1468 pub elicitation_id: ElicitationId,
1470 #[serde(rename = "_meta")]
1476 pub meta: Option<Meta>,
1477}
1478
1479impl CompleteElicitationNotification {
1480 #[must_use]
1482 pub fn new(elicitation_id: impl Into<ElicitationId>) -> Self {
1483 Self {
1484 elicitation_id: elicitation_id.into(),
1485 meta: None,
1486 }
1487 }
1488
1489 #[must_use]
1495 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1496 self.meta = meta.into_option();
1497 self
1498 }
1499}
1500
1501#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1508#[serde(rename_all = "camelCase")]
1509#[non_exhaustive]
1510pub struct UrlElicitationRequiredData {
1511 pub elicitations: Vec<UrlElicitationRequiredItem>,
1513}
1514
1515impl UrlElicitationRequiredData {
1516 #[must_use]
1518 pub fn new(elicitations: Vec<UrlElicitationRequiredItem>) -> Self {
1519 Self { elicitations }
1520 }
1521}
1522
1523#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1529#[serde(rename_all = "camelCase")]
1530#[non_exhaustive]
1531pub struct UrlElicitationRequiredItem {
1532 pub mode: ElicitationUrlOnlyMode,
1534 pub elicitation_id: ElicitationId,
1536 #[schemars(extend("format" = "uri"))]
1538 pub url: String,
1539 pub message: String,
1541}
1542
1543#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
1545#[serde(rename_all = "snake_case")]
1546#[non_exhaustive]
1547pub enum ElicitationUrlOnlyMode {
1548 #[default]
1550 Url,
1551}
1552
1553impl UrlElicitationRequiredItem {
1554 #[must_use]
1556 pub fn new(
1557 elicitation_id: impl Into<ElicitationId>,
1558 url: impl Into<String>,
1559 message: impl Into<String>,
1560 ) -> Self {
1561 Self {
1562 mode: ElicitationUrlOnlyMode::Url,
1563 elicitation_id: elicitation_id.into(),
1564 url: url.into(),
1565 message: message.into(),
1566 }
1567 }
1568}
1569
1570#[cfg(test)]
1571mod tests {
1572 use super::*;
1573 use serde_json::json;
1574
1575 #[test]
1576 fn form_mode_request_serialization() {
1577 let schema = ElicitationSchema::new().string("name", true);
1578 let req = CreateElicitationRequest::new(
1579 ElicitationFormMode::new(ElicitationSessionScope::new("sess_1"), schema),
1580 "Please enter your name",
1581 );
1582
1583 let json = serde_json::to_value(&req).unwrap();
1584 assert_eq!(json["sessionId"], "sess_1");
1585 assert!(json.get("toolCallId").is_none());
1586 assert_eq!(json["mode"], "form");
1587 assert_eq!(json["message"], "Please enter your name");
1588 assert!(json["requestedSchema"].is_object());
1589 assert_eq!(json["requestedSchema"]["type"], "object");
1590 assert_eq!(
1591 json["requestedSchema"]["properties"]["name"]["type"],
1592 "string"
1593 );
1594
1595 let roundtripped: CreateElicitationRequest = serde_json::from_value(json).unwrap();
1596 assert_eq!(
1597 *roundtripped.scope(),
1598 ElicitationSessionScope::new("sess_1").into()
1599 );
1600 assert_eq!(roundtripped.message, "Please enter your name");
1601 assert!(matches!(roundtripped.mode, ElicitationMode::Form(_)));
1602 }
1603
1604 #[test]
1605 fn url_mode_request_serialization() {
1606 let req = CreateElicitationRequest::new(
1607 ElicitationUrlMode::new(
1608 ElicitationSessionScope::new("sess_2").tool_call_id("tc_1"),
1609 "elic_1",
1610 "https://example.com/auth",
1611 ),
1612 "Please authenticate",
1613 );
1614
1615 let json = serde_json::to_value(&req).unwrap();
1616 assert_eq!(json["sessionId"], "sess_2");
1617 assert_eq!(json["toolCallId"], "tc_1");
1618 assert_eq!(json["mode"], "url");
1619 assert_eq!(json["elicitationId"], "elic_1");
1620 assert_eq!(json["url"], "https://example.com/auth");
1621 assert_eq!(json["message"], "Please authenticate");
1622
1623 let roundtripped: CreateElicitationRequest = serde_json::from_value(json).unwrap();
1624 assert_eq!(
1625 *roundtripped.scope(),
1626 ElicitationSessionScope::new("sess_2")
1627 .tool_call_id("tc_1")
1628 .into()
1629 );
1630 assert!(matches!(roundtripped.mode, ElicitationMode::Url(_)));
1631 }
1632
1633 #[test]
1634 fn response_accept_serialization() {
1635 let resp = CreateElicitationResponse::new(ElicitationAction::Accept(
1636 ElicitationAcceptAction::new().content(BTreeMap::from([(
1637 "name".to_string(),
1638 ElicitationContentValue::from("Alice"),
1639 )])),
1640 ));
1641
1642 let json = serde_json::to_value(&resp).unwrap();
1643 assert_eq!(json["action"], "accept");
1644 assert_eq!(json["content"]["name"], "Alice");
1645
1646 let roundtripped: CreateElicitationResponse = serde_json::from_value(json).unwrap();
1647 assert!(matches!(
1648 roundtripped.action,
1649 ElicitationAction::Accept(ElicitationAcceptAction {
1650 content: Some(_),
1651 ..
1652 })
1653 ));
1654 }
1655
1656 #[test]
1657 fn response_decline_serialization() {
1658 let resp = CreateElicitationResponse::new(ElicitationAction::Decline);
1659
1660 let json = serde_json::to_value(&resp).unwrap();
1661 assert_eq!(json["action"], "decline");
1662
1663 let roundtripped: CreateElicitationResponse = serde_json::from_value(json).unwrap();
1664 assert!(matches!(roundtripped.action, ElicitationAction::Decline));
1665 }
1666
1667 #[test]
1668 fn response_cancel_serialization() {
1669 let resp = CreateElicitationResponse::new(ElicitationAction::Cancel);
1670
1671 let json = serde_json::to_value(&resp).unwrap();
1672 assert_eq!(json["action"], "cancel");
1673
1674 let roundtripped: CreateElicitationResponse = serde_json::from_value(json).unwrap();
1675 assert!(matches!(roundtripped.action, ElicitationAction::Cancel));
1676 }
1677
1678 #[test]
1679 fn url_mode_request_scope_serialization() {
1680 let req = CreateElicitationRequest::new(
1681 ElicitationUrlMode::new(
1682 ElicitationRequestScope::new(RequestId::Number(42)),
1683 "elic_2",
1684 "https://example.com/setup",
1685 ),
1686 "Please complete setup",
1687 );
1688
1689 let json = serde_json::to_value(&req).unwrap();
1690 assert_eq!(json["requestId"], 42);
1691 assert!(json.get("sessionId").is_none());
1692 assert_eq!(json["mode"], "url");
1693 assert_eq!(json["elicitationId"], "elic_2");
1694 assert_eq!(json["url"], "https://example.com/setup");
1695 assert_eq!(json["message"], "Please complete setup");
1696
1697 let roundtripped: CreateElicitationRequest = serde_json::from_value(json).unwrap();
1698 assert_eq!(
1699 *roundtripped.scope(),
1700 ElicitationRequestScope::new(RequestId::Number(42)).into()
1701 );
1702 assert!(matches!(roundtripped.mode, ElicitationMode::Url(_)));
1703 }
1704
1705 #[test]
1706 fn request_scope_request_serialization() {
1707 let req = CreateElicitationRequest::new(
1708 ElicitationFormMode::new(
1709 ElicitationRequestScope::new(RequestId::Number(99)),
1710 ElicitationSchema::new().string("workspace", true),
1711 ),
1712 "Enter workspace name",
1713 );
1714
1715 let json = serde_json::to_value(&req).unwrap();
1716 assert_eq!(json["requestId"], 99);
1717 assert!(json.get("sessionId").is_none());
1718
1719 let roundtripped: CreateElicitationRequest = serde_json::from_value(json).unwrap();
1720 assert_eq!(
1721 *roundtripped.scope(),
1722 ElicitationRequestScope::new(RequestId::Number(99)).into()
1723 );
1724 }
1725
1726 #[test]
1733 fn client_response_serialization_accept() {
1734 use crate::v1::ClientResponse;
1735
1736 let resp = ClientResponse::CreateElicitationResponse(CreateElicitationResponse::new(
1737 ElicitationAction::Accept(ElicitationAcceptAction::new().content(BTreeMap::from([(
1738 "name".to_string(),
1739 ElicitationContentValue::from("Alice"),
1740 )]))),
1741 ));
1742 let json = serde_json::to_value(&resp).unwrap();
1743 assert_eq!(json["action"], "accept");
1744 assert_eq!(json["content"]["name"], "Alice");
1745
1746 let roundtripped: CreateElicitationResponse = serde_json::from_value(json).unwrap();
1748 assert!(matches!(roundtripped.action, ElicitationAction::Accept(_)));
1749 }
1750
1751 #[test]
1752 fn client_response_serialization_decline() {
1753 use crate::v1::ClientResponse;
1754
1755 let resp = ClientResponse::CreateElicitationResponse(CreateElicitationResponse::new(
1756 ElicitationAction::Decline,
1757 ));
1758 let json = serde_json::to_value(&resp).unwrap();
1759 assert_eq!(json["action"], "decline");
1760
1761 let roundtripped: CreateElicitationResponse = serde_json::from_value(json).unwrap();
1762 assert!(matches!(roundtripped.action, ElicitationAction::Decline));
1763 }
1764
1765 #[test]
1766 fn client_response_serialization_cancel() {
1767 use crate::v1::ClientResponse;
1768
1769 let resp = ClientResponse::CreateElicitationResponse(CreateElicitationResponse::new(
1770 ElicitationAction::Cancel,
1771 ));
1772 let json = serde_json::to_value(&resp).unwrap();
1773 assert_eq!(json["action"], "cancel");
1774
1775 let roundtripped: CreateElicitationResponse = serde_json::from_value(json).unwrap();
1776 assert!(matches!(roundtripped.action, ElicitationAction::Cancel));
1777 }
1778
1779 #[test]
1782 fn request_tolerates_extra_fields() {
1783 let json = json!({
1784 "sessionId": "sess_1",
1785 "mode": "form",
1786 "message": "Enter your name",
1787 "requestedSchema": {
1788 "type": "object",
1789 "properties": {
1790 "name": { "type": "string", "title": "Name" }
1791 },
1792 "required": ["name"]
1793 },
1794 "unknownStringField": "hello",
1795 "unknownNumberField": 42
1796 });
1797
1798 let req: CreateElicitationRequest = serde_json::from_value(json).unwrap();
1799 assert_eq!(*req.scope(), ElicitationSessionScope::new("sess_1").into());
1800 assert_eq!(req.message, "Enter your name");
1801 assert!(matches!(req.mode, ElicitationMode::Form(_)));
1802 }
1803
1804 #[test]
1805 fn completion_notification_serialization() {
1806 let notif = CompleteElicitationNotification::new("elic_1");
1807
1808 let json = serde_json::to_value(¬if).unwrap();
1809 assert_eq!(json["elicitationId"], "elic_1");
1810
1811 let roundtripped: CompleteElicitationNotification = serde_json::from_value(json).unwrap();
1812 assert_eq!(roundtripped.elicitation_id, ElicitationId::new("elic_1"));
1813 }
1814
1815 #[test]
1816 fn capabilities_form_only() {
1817 let caps = ElicitationCapabilities::new().form(ElicitationFormCapabilities::new());
1818
1819 let json = serde_json::to_value(&caps).unwrap();
1820 assert!(json["form"].is_object());
1821 assert!(json.get("url").is_none());
1822
1823 let roundtripped: ElicitationCapabilities = serde_json::from_value(json).unwrap();
1824 assert!(roundtripped.form.is_some());
1825 assert!(roundtripped.url.is_none());
1826 }
1827
1828 #[test]
1829 fn capabilities_url_only() {
1830 let caps = ElicitationCapabilities::new().url(ElicitationUrlCapabilities::new());
1831
1832 let json = serde_json::to_value(&caps).unwrap();
1833 assert!(json.get("form").is_none());
1834 assert!(json["url"].is_object());
1835
1836 let roundtripped: ElicitationCapabilities = serde_json::from_value(json).unwrap();
1837 assert!(roundtripped.form.is_none());
1838 assert!(roundtripped.url.is_some());
1839 }
1840
1841 #[test]
1842 fn capabilities_both() {
1843 let caps = ElicitationCapabilities::new()
1844 .form(ElicitationFormCapabilities::new())
1845 .url(ElicitationUrlCapabilities::new());
1846
1847 let json = serde_json::to_value(&caps).unwrap();
1848 assert!(json["form"].is_object());
1849 assert!(json["url"].is_object());
1850
1851 let roundtripped: ElicitationCapabilities = serde_json::from_value(json).unwrap();
1852 assert!(roundtripped.form.is_some());
1853 assert!(roundtripped.url.is_some());
1854 }
1855
1856 #[test]
1857 fn url_elicitation_required_data_serialization() {
1858 let data = UrlElicitationRequiredData::new(vec![UrlElicitationRequiredItem::new(
1859 "elic_1",
1860 "https://example.com/auth",
1861 "Please authenticate",
1862 )]);
1863
1864 let json = serde_json::to_value(&data).unwrap();
1865 assert_eq!(json["elicitations"][0]["mode"], "url");
1866 assert_eq!(json["elicitations"][0]["elicitationId"], "elic_1");
1867 assert_eq!(json["elicitations"][0]["url"], "https://example.com/auth");
1868
1869 let roundtripped: UrlElicitationRequiredData = serde_json::from_value(json).unwrap();
1870 assert_eq!(roundtripped.elicitations.len(), 1);
1871 assert_eq!(
1872 roundtripped.elicitations[0].mode,
1873 ElicitationUrlOnlyMode::Url
1874 );
1875 }
1876
1877 #[test]
1878 fn schema_default_sets_object_type() {
1879 let schema = ElicitationSchema::default();
1880
1881 assert_eq!(schema.type_, ElicitationSchemaType::Object);
1882 assert!(schema.properties.is_empty());
1883
1884 let json = serde_json::to_value(&schema).unwrap();
1885 assert_eq!(json["type"], "object");
1886 }
1887
1888 #[test]
1889 fn schema_builder_serialization() {
1890 let schema = ElicitationSchema::new()
1891 .string("name", true)
1892 .email("email", true)
1893 .integer("age", 0, 150, true)
1894 .boolean("newsletter", false)
1895 .description("User registration");
1896
1897 let json = serde_json::to_value(&schema).unwrap();
1898 assert_eq!(json["type"], "object");
1899 assert_eq!(json["description"], "User registration");
1900 assert_eq!(json["properties"]["name"]["type"], "string");
1901 assert_eq!(json["properties"]["email"]["type"], "string");
1902 assert_eq!(json["properties"]["email"]["format"], "email");
1903 assert_eq!(json["properties"]["age"]["type"], "integer");
1904 assert_eq!(json["properties"]["age"]["minimum"], 0);
1905 assert_eq!(json["properties"]["age"]["maximum"], 150);
1906 assert_eq!(json["properties"]["newsletter"]["type"], "boolean");
1907
1908 let required = json["required"].as_array().unwrap();
1909 assert!(required.contains(&json!("name")));
1910 assert!(required.contains(&json!("email")));
1911 assert!(required.contains(&json!("age")));
1912 assert!(!required.contains(&json!("newsletter")));
1913
1914 let roundtripped: ElicitationSchema = serde_json::from_value(json).unwrap();
1915 assert_eq!(roundtripped.properties.len(), 4);
1916 assert!(roundtripped.required.unwrap().contains(&"name".to_string()));
1917 }
1918
1919 #[test]
1920 fn schema_string_enum_serialization() {
1921 let schema = ElicitationSchema::new().property(
1922 "color",
1923 StringPropertySchema::new().enum_values(vec![
1924 "red".into(),
1925 "green".into(),
1926 "blue".into(),
1927 ]),
1928 true,
1929 );
1930
1931 let json = serde_json::to_value(&schema).unwrap();
1932 assert_eq!(json["properties"]["color"]["type"], "string");
1933 let enum_vals = json["properties"]["color"]["enum"].as_array().unwrap();
1934 assert_eq!(enum_vals.len(), 3);
1935
1936 let roundtripped: ElicitationSchema = serde_json::from_value(json).unwrap();
1937 if let ElicitationPropertySchema::String(s) = roundtripped.properties.get("color").unwrap()
1938 {
1939 assert_eq!(s.enum_values.as_ref().unwrap().len(), 3);
1940 } else {
1941 panic!("expected String variant");
1942 }
1943 }
1944
1945 #[test]
1946 fn schema_multi_select_serialization() {
1947 let schema = ElicitationSchema::new().property(
1948 "colors",
1949 MultiSelectPropertySchema::new(vec!["red".into(), "green".into(), "blue".into()])
1950 .min_items(1)
1951 .max_items(3),
1952 false,
1953 );
1954
1955 let json = serde_json::to_value(&schema).unwrap();
1956 assert_eq!(json["properties"]["colors"]["type"], "array");
1957 assert_eq!(json["properties"]["colors"]["items"]["type"], "string");
1958 assert_eq!(json["properties"]["colors"]["minItems"], 1);
1959 assert_eq!(json["properties"]["colors"]["maxItems"], 3);
1960
1961 let roundtripped: ElicitationSchema = serde_json::from_value(json).unwrap();
1962 assert!(matches!(
1963 roundtripped.properties.get("colors").unwrap(),
1964 ElicitationPropertySchema::Array(_)
1965 ));
1966 }
1967
1968 #[test]
1969 fn schema_titled_enum_serialization() {
1970 let schema = ElicitationSchema::new().property(
1971 "country",
1972 StringPropertySchema::new().one_of(vec![
1973 EnumOption::new("us", "United States"),
1974 EnumOption::new("uk", "United Kingdom"),
1975 ]),
1976 true,
1977 );
1978
1979 let json = serde_json::to_value(&schema).unwrap();
1980 assert_eq!(json["properties"]["country"]["type"], "string");
1981 let one_of = json["properties"]["country"]["oneOf"].as_array().unwrap();
1982 assert_eq!(one_of.len(), 2);
1983 assert_eq!(one_of[0]["const"], "us");
1984 assert_eq!(one_of[0]["title"], "United States");
1985
1986 let roundtripped: ElicitationSchema = serde_json::from_value(json).unwrap();
1987 if let ElicitationPropertySchema::String(s) =
1988 roundtripped.properties.get("country").unwrap()
1989 {
1990 assert_eq!(s.one_of.as_ref().unwrap().len(), 2);
1991 } else {
1992 panic!("expected String variant");
1993 }
1994 }
1995
1996 #[test]
1997 fn schema_number_property_serialization() {
1998 let schema = ElicitationSchema::new().number("rating", 0.0, 5.0, true);
1999
2000 let json = serde_json::to_value(&schema).unwrap();
2001 assert_eq!(json["properties"]["rating"]["type"], "number");
2002 assert_eq!(json["properties"]["rating"]["minimum"], 0.0);
2003 assert_eq!(json["properties"]["rating"]["maximum"], 5.0);
2004
2005 let roundtripped: ElicitationSchema = serde_json::from_value(json).unwrap();
2006 if let ElicitationPropertySchema::Number(n) = roundtripped.properties.get("rating").unwrap()
2007 {
2008 assert_eq!(n.minimum, Some(0.0));
2009 assert_eq!(n.maximum, Some(5.0));
2010 } else {
2011 panic!("expected Number variant");
2012 }
2013 }
2014
2015 #[test]
2016 fn schema_string_format_serialization() {
2017 let schema = ElicitationSchema::new()
2018 .uri("website", true)
2019 .date("birthday", true)
2020 .date_time("updated_at", false);
2021
2022 let json = serde_json::to_value(&schema).unwrap();
2023 assert_eq!(json["properties"]["website"]["type"], "string");
2024 assert_eq!(json["properties"]["website"]["format"], "uri");
2025 assert_eq!(json["properties"]["birthday"]["type"], "string");
2026 assert_eq!(json["properties"]["birthday"]["format"], "date");
2027 assert_eq!(json["properties"]["updated_at"]["type"], "string");
2028 assert_eq!(json["properties"]["updated_at"]["format"], "date-time");
2029
2030 let required = json["required"].as_array().unwrap();
2031 assert!(required.contains(&json!("website")));
2032 assert!(required.contains(&json!("birthday")));
2033 assert!(!required.contains(&json!("updated_at")));
2034 }
2035
2036 #[test]
2037 fn schema_string_pattern_serialization() {
2038 let schema = ElicitationSchema::new().property(
2039 "name",
2040 StringPropertySchema::new()
2041 .min_length(1)
2042 .max_length(64)
2043 .pattern("^[a-zA-Z_][a-zA-Z0-9_]*$"),
2044 true,
2045 );
2046
2047 let json = serde_json::to_value(&schema).unwrap();
2048 assert_eq!(json["properties"]["name"]["type"], "string");
2049 assert_eq!(
2050 json["properties"]["name"]["pattern"],
2051 "^[a-zA-Z_][a-zA-Z0-9_]*$"
2052 );
2053
2054 let roundtripped: ElicitationSchema = serde_json::from_value(json).unwrap();
2055 if let ElicitationPropertySchema::String(s) = roundtripped.properties.get("name").unwrap() {
2056 assert_eq!(s.pattern.as_deref(), Some("^[a-zA-Z_][a-zA-Z0-9_]*$"));
2057 } else {
2058 panic!("expected String variant");
2059 }
2060 }
2061
2062 #[test]
2063 fn schema_property_updates_required_state() {
2064 let schema = ElicitationSchema::new()
2065 .string("name", true)
2066 .email("name", false);
2067
2068 let json = serde_json::to_value(&schema).unwrap();
2069 assert!(json.get("required").is_none());
2070 assert_eq!(json["properties"]["name"]["format"], "email");
2071 }
2072
2073 #[test]
2074 fn schema_rejects_invalid_object_type() {
2075 let err = serde_json::from_value::<ElicitationSchema>(json!({
2076 "type": "array",
2077 "properties": {
2078 "name": {
2079 "type": "string"
2080 }
2081 }
2082 }))
2083 .unwrap_err();
2084
2085 assert!(err.to_string().contains("unknown variant"));
2086 }
2087
2088 #[test]
2089 fn titled_multi_select_items_reject_one_of() {
2090 let err = serde_json::from_value::<TitledMultiSelectItems>(json!({
2091 "oneOf": [
2092 {
2093 "const": "red",
2094 "title": "Red"
2095 }
2096 ]
2097 }))
2098 .unwrap_err();
2099
2100 assert!(err.to_string().contains("missing field `anyOf`"));
2101 }
2102
2103 #[test]
2104 fn response_accept_rejects_non_object_content() {
2105 let err = serde_json::from_value::<CreateElicitationResponse>(json!({
2106 "action": "accept",
2107 "content": "Alice"
2108 }))
2109 .unwrap_err();
2110
2111 assert!(err.to_string().contains("invalid type"));
2112 }
2113
2114 #[test]
2115 fn response_accept_rejects_nested_object_content() {
2116 let err = serde_json::from_value::<CreateElicitationResponse>(json!({
2117 "action": "accept",
2118 "content": {
2119 "profile": {
2120 "name": "Alice"
2121 }
2122 }
2123 }))
2124 .unwrap_err();
2125
2126 assert!(err.to_string().contains("data did not match any variant"));
2127 }
2128
2129 #[test]
2130 fn response_accept_allows_primitive_and_string_array_content() {
2131 let response = CreateElicitationResponse::new(ElicitationAction::Accept(
2132 ElicitationAcceptAction::new().content(BTreeMap::from([
2133 ("name".to_string(), ElicitationContentValue::from("Alice")),
2134 ("age".to_string(), ElicitationContentValue::from(30_i32)),
2135 ("score".to_string(), ElicitationContentValue::from(9.5_f64)),
2136 (
2137 "subscribed".to_string(),
2138 ElicitationContentValue::from(true),
2139 ),
2140 (
2141 "tags".to_string(),
2142 ElicitationContentValue::from(vec!["rust", "acp"]),
2143 ),
2144 ])),
2145 ));
2146
2147 let json = serde_json::to_value(&response).unwrap();
2148 assert_eq!(json["action"], "accept");
2149 assert_eq!(json["content"]["name"], "Alice");
2150 assert_eq!(json["content"]["age"], 30);
2151 assert_eq!(json["content"]["score"], 9.5);
2152 assert_eq!(json["content"]["subscribed"], true);
2153 assert_eq!(json["content"]["tags"][0], "rust");
2154 assert_eq!(json["content"]["tags"][1], "acp");
2155 }
2156}