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::{
16 ELICITATION_COMPLETE_NOTIFICATION, ELICITATION_CREATE_METHOD_NAME, IntoOption, Meta, RequestId,
17 SessionId, ToolCallId,
18};
19
20#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Hash, Display, From)]
26#[serde(transparent)]
27#[from(Arc<str>, String, &'static str)]
28#[non_exhaustive]
29pub struct ElicitationId(pub Arc<str>);
30
31impl ElicitationId {
32 #[must_use]
33 pub fn new(id: impl Into<Arc<str>>) -> Self {
34 Self(id.into())
35 }
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
40#[serde(rename_all = "kebab-case")]
41#[non_exhaustive]
42pub enum StringFormat {
43 Email,
45 Uri,
47 Date,
49 DateTime,
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
55#[serde(rename_all = "snake_case")]
56#[non_exhaustive]
57pub enum ElicitationSchemaType {
58 #[default]
60 Object,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
65#[non_exhaustive]
66pub struct EnumOption {
67 #[serde(rename = "const")]
69 pub value: String,
70 pub title: String,
72}
73
74impl EnumOption {
75 #[must_use]
77 pub fn new(value: impl Into<String>, title: impl Into<String>) -> Self {
78 Self {
79 value: value.into(),
80 title: title.into(),
81 }
82 }
83}
84
85#[skip_serializing_none]
90#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
91#[serde(rename_all = "camelCase")]
92#[non_exhaustive]
93pub struct StringPropertySchema {
94 pub title: Option<String>,
96 pub description: Option<String>,
98 pub min_length: Option<u32>,
100 pub max_length: Option<u32>,
102 pub pattern: Option<String>,
104 pub format: Option<StringFormat>,
106 pub default: Option<String>,
108 #[serde(rename = "enum")]
110 pub enum_values: Option<Vec<String>>,
111 #[serde(rename = "oneOf")]
113 pub one_of: Option<Vec<EnumOption>>,
114}
115
116impl StringPropertySchema {
117 #[must_use]
119 pub fn new() -> Self {
120 Self::default()
121 }
122
123 #[must_use]
125 pub fn email() -> Self {
126 Self {
127 format: Some(StringFormat::Email),
128 ..Default::default()
129 }
130 }
131
132 #[must_use]
134 pub fn uri() -> Self {
135 Self {
136 format: Some(StringFormat::Uri),
137 ..Default::default()
138 }
139 }
140
141 #[must_use]
143 pub fn date() -> Self {
144 Self {
145 format: Some(StringFormat::Date),
146 ..Default::default()
147 }
148 }
149
150 #[must_use]
152 pub fn date_time() -> Self {
153 Self {
154 format: Some(StringFormat::DateTime),
155 ..Default::default()
156 }
157 }
158
159 #[must_use]
161 pub fn title(mut self, title: impl IntoOption<String>) -> Self {
162 self.title = title.into_option();
163 self
164 }
165
166 #[must_use]
168 pub fn description(mut self, description: impl IntoOption<String>) -> Self {
169 self.description = description.into_option();
170 self
171 }
172
173 #[must_use]
175 pub fn min_length(mut self, min_length: impl IntoOption<u32>) -> Self {
176 self.min_length = min_length.into_option();
177 self
178 }
179
180 #[must_use]
182 pub fn max_length(mut self, max_length: impl IntoOption<u32>) -> Self {
183 self.max_length = max_length.into_option();
184 self
185 }
186
187 #[must_use]
189 pub fn pattern(mut self, pattern: impl IntoOption<String>) -> Self {
190 self.pattern = pattern.into_option();
191 self
192 }
193
194 #[must_use]
196 pub fn format(mut self, format: impl IntoOption<StringFormat>) -> Self {
197 self.format = format.into_option();
198 self
199 }
200
201 #[must_use]
203 pub fn default_value(mut self, default: impl IntoOption<String>) -> Self {
204 self.default = default.into_option();
205 self
206 }
207
208 #[must_use]
210 pub fn enum_values(mut self, enum_values: impl IntoOption<Vec<String>>) -> Self {
211 self.enum_values = enum_values.into_option();
212 self
213 }
214
215 #[must_use]
217 pub fn one_of(mut self, one_of: impl IntoOption<Vec<EnumOption>>) -> Self {
218 self.one_of = one_of.into_option();
219 self
220 }
221}
222
223#[skip_serializing_none]
225#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
226#[serde(rename_all = "camelCase")]
227#[non_exhaustive]
228pub struct NumberPropertySchema {
229 pub title: Option<String>,
231 pub description: Option<String>,
233 pub minimum: Option<f64>,
235 pub maximum: Option<f64>,
237 pub default: Option<f64>,
239}
240
241impl NumberPropertySchema {
242 #[must_use]
244 pub fn new() -> Self {
245 Self::default()
246 }
247
248 #[must_use]
250 pub fn title(mut self, title: impl IntoOption<String>) -> Self {
251 self.title = title.into_option();
252 self
253 }
254
255 #[must_use]
257 pub fn description(mut self, description: impl IntoOption<String>) -> Self {
258 self.description = description.into_option();
259 self
260 }
261
262 #[must_use]
264 pub fn minimum(mut self, minimum: impl IntoOption<f64>) -> Self {
265 self.minimum = minimum.into_option();
266 self
267 }
268
269 #[must_use]
271 pub fn maximum(mut self, maximum: impl IntoOption<f64>) -> Self {
272 self.maximum = maximum.into_option();
273 self
274 }
275
276 #[must_use]
278 pub fn default_value(mut self, default: impl IntoOption<f64>) -> Self {
279 self.default = default.into_option();
280 self
281 }
282}
283
284#[skip_serializing_none]
286#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
287#[serde(rename_all = "camelCase")]
288#[non_exhaustive]
289pub struct IntegerPropertySchema {
290 pub title: Option<String>,
292 pub description: Option<String>,
294 pub minimum: Option<i64>,
296 pub maximum: Option<i64>,
298 pub default: Option<i64>,
300}
301
302impl IntegerPropertySchema {
303 #[must_use]
305 pub fn new() -> Self {
306 Self::default()
307 }
308
309 #[must_use]
311 pub fn title(mut self, title: impl IntoOption<String>) -> Self {
312 self.title = title.into_option();
313 self
314 }
315
316 #[must_use]
318 pub fn description(mut self, description: impl IntoOption<String>) -> Self {
319 self.description = description.into_option();
320 self
321 }
322
323 #[must_use]
325 pub fn minimum(mut self, minimum: impl IntoOption<i64>) -> Self {
326 self.minimum = minimum.into_option();
327 self
328 }
329
330 #[must_use]
332 pub fn maximum(mut self, maximum: impl IntoOption<i64>) -> Self {
333 self.maximum = maximum.into_option();
334 self
335 }
336
337 #[must_use]
339 pub fn default_value(mut self, default: impl IntoOption<i64>) -> Self {
340 self.default = default.into_option();
341 self
342 }
343}
344
345#[skip_serializing_none]
347#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
348#[serde(rename_all = "camelCase")]
349#[non_exhaustive]
350pub struct BooleanPropertySchema {
351 pub title: Option<String>,
353 pub description: Option<String>,
355 pub default: Option<bool>,
357}
358
359impl BooleanPropertySchema {
360 #[must_use]
362 pub fn new() -> Self {
363 Self::default()
364 }
365
366 #[must_use]
368 pub fn title(mut self, title: impl IntoOption<String>) -> Self {
369 self.title = title.into_option();
370 self
371 }
372
373 #[must_use]
375 pub fn description(mut self, description: impl IntoOption<String>) -> Self {
376 self.description = description.into_option();
377 self
378 }
379
380 #[must_use]
382 pub fn default_value(mut self, default: impl IntoOption<bool>) -> Self {
383 self.default = default.into_option();
384 self
385 }
386}
387
388#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
390#[serde(rename_all = "snake_case")]
391#[non_exhaustive]
392pub enum ElicitationStringType {
393 #[default]
395 String,
396}
397
398#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
400#[non_exhaustive]
401pub struct UntitledMultiSelectItems {
402 #[serde(rename = "type")]
404 pub type_: ElicitationStringType,
405 #[serde(rename = "enum")]
407 pub values: Vec<String>,
408}
409
410#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
412#[non_exhaustive]
413pub struct TitledMultiSelectItems {
414 #[serde(rename = "anyOf")]
416 pub options: Vec<EnumOption>,
417}
418
419impl TitledMultiSelectItems {
420 #[must_use]
422 pub fn new(options: Vec<EnumOption>) -> Self {
423 Self { options }
424 }
425}
426
427#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
429#[serde(untagged)]
430#[non_exhaustive]
431pub enum MultiSelectItems {
432 Untitled(UntitledMultiSelectItems),
434 Titled(TitledMultiSelectItems),
436}
437
438#[skip_serializing_none]
440#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
441#[serde(rename_all = "camelCase")]
442#[non_exhaustive]
443pub struct MultiSelectPropertySchema {
444 pub title: Option<String>,
446 pub description: Option<String>,
448 pub min_items: Option<u64>,
450 pub max_items: Option<u64>,
452 pub items: MultiSelectItems,
454 pub default: Option<Vec<String>>,
456}
457
458impl MultiSelectPropertySchema {
459 #[must_use]
461 pub fn new(values: Vec<String>) -> Self {
462 Self {
463 title: None,
464 description: None,
465 min_items: None,
466 max_items: None,
467 items: MultiSelectItems::Untitled(UntitledMultiSelectItems {
468 type_: ElicitationStringType::String,
469 values,
470 }),
471 default: None,
472 }
473 }
474
475 #[must_use]
477 pub fn titled(options: Vec<EnumOption>) -> Self {
478 Self {
479 title: None,
480 description: None,
481 min_items: None,
482 max_items: None,
483 items: MultiSelectItems::Titled(TitledMultiSelectItems { options }),
484 default: None,
485 }
486 }
487
488 #[must_use]
490 pub fn title(mut self, title: impl IntoOption<String>) -> Self {
491 self.title = title.into_option();
492 self
493 }
494
495 #[must_use]
497 pub fn description(mut self, description: impl IntoOption<String>) -> Self {
498 self.description = description.into_option();
499 self
500 }
501
502 #[must_use]
504 pub fn min_items(mut self, min_items: impl IntoOption<u64>) -> Self {
505 self.min_items = min_items.into_option();
506 self
507 }
508
509 #[must_use]
511 pub fn max_items(mut self, max_items: impl IntoOption<u64>) -> Self {
512 self.max_items = max_items.into_option();
513 self
514 }
515
516 #[must_use]
518 pub fn default_value(mut self, default: impl IntoOption<Vec<String>>) -> Self {
519 self.default = default.into_option();
520 self
521 }
522}
523
524#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
530#[serde(tag = "type", rename_all = "snake_case")]
531#[schemars(extend("discriminator" = {"propertyName": "type"}))]
532#[non_exhaustive]
533pub enum ElicitationPropertySchema {
534 String(StringPropertySchema),
536 Number(NumberPropertySchema),
538 Integer(IntegerPropertySchema),
540 Boolean(BooleanPropertySchema),
542 Array(MultiSelectPropertySchema),
544}
545
546impl From<StringPropertySchema> for ElicitationPropertySchema {
547 fn from(schema: StringPropertySchema) -> Self {
548 Self::String(schema)
549 }
550}
551
552impl From<NumberPropertySchema> for ElicitationPropertySchema {
553 fn from(schema: NumberPropertySchema) -> Self {
554 Self::Number(schema)
555 }
556}
557
558impl From<IntegerPropertySchema> for ElicitationPropertySchema {
559 fn from(schema: IntegerPropertySchema) -> Self {
560 Self::Integer(schema)
561 }
562}
563
564impl From<BooleanPropertySchema> for ElicitationPropertySchema {
565 fn from(schema: BooleanPropertySchema) -> Self {
566 Self::Boolean(schema)
567 }
568}
569
570impl From<MultiSelectPropertySchema> for ElicitationPropertySchema {
571 fn from(schema: MultiSelectPropertySchema) -> Self {
572 Self::Array(schema)
573 }
574}
575
576fn default_object_type() -> ElicitationSchemaType {
577 ElicitationSchemaType::Object
578}
579
580#[skip_serializing_none]
585#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
586#[serde(rename_all = "camelCase")]
587#[non_exhaustive]
588pub struct ElicitationSchema {
589 #[serde(rename = "type", default = "default_object_type")]
591 pub type_: ElicitationSchemaType,
592 pub title: Option<String>,
594 #[serde(default)]
596 pub properties: BTreeMap<String, ElicitationPropertySchema>,
597 pub required: Option<Vec<String>>,
599 pub description: Option<String>,
601}
602
603impl Default for ElicitationSchema {
604 fn default() -> Self {
605 Self {
606 type_: default_object_type(),
607 title: None,
608 properties: BTreeMap::new(),
609 required: None,
610 description: None,
611 }
612 }
613}
614
615impl ElicitationSchema {
616 #[must_use]
618 pub fn new() -> Self {
619 Self::default()
620 }
621
622 #[must_use]
624 pub fn title(mut self, title: impl IntoOption<String>) -> Self {
625 self.title = title.into_option();
626 self
627 }
628
629 #[must_use]
631 pub fn description(mut self, description: impl IntoOption<String>) -> Self {
632 self.description = description.into_option();
633 self
634 }
635
636 #[must_use]
638 pub fn property<S>(mut self, name: impl Into<String>, schema: S, required: bool) -> Self
639 where
640 S: Into<ElicitationPropertySchema>,
641 {
642 let name = name.into();
643 self.properties.insert(name.clone(), schema.into());
644
645 if required {
646 let required_fields = self.required.get_or_insert_with(Vec::new);
647 if !required_fields.contains(&name) {
648 required_fields.push(name);
649 }
650 } else if let Some(required_fields) = &mut self.required {
651 required_fields.retain(|field| field != &name);
652
653 if required_fields.is_empty() {
654 self.required = None;
655 }
656 }
657
658 self
659 }
660
661 #[must_use]
663 pub fn string(self, name: impl Into<String>, required: bool) -> Self {
664 self.property(name, StringPropertySchema::new(), required)
665 }
666
667 #[must_use]
669 pub fn email(self, name: impl Into<String>, required: bool) -> Self {
670 self.property(name, StringPropertySchema::email(), required)
671 }
672
673 #[must_use]
675 pub fn uri(self, name: impl Into<String>, required: bool) -> Self {
676 self.property(name, StringPropertySchema::uri(), required)
677 }
678
679 #[must_use]
681 pub fn date(self, name: impl Into<String>, required: bool) -> Self {
682 self.property(name, StringPropertySchema::date(), required)
683 }
684
685 #[must_use]
687 pub fn date_time(self, name: impl Into<String>, required: bool) -> Self {
688 self.property(name, StringPropertySchema::date_time(), required)
689 }
690
691 #[must_use]
693 pub fn number(self, name: impl Into<String>, min: f64, max: f64, required: bool) -> Self {
694 self.property(
695 name,
696 NumberPropertySchema::new().minimum(min).maximum(max),
697 required,
698 )
699 }
700
701 #[must_use]
703 pub fn integer(self, name: impl Into<String>, min: i64, max: i64, required: bool) -> Self {
704 self.property(
705 name,
706 IntegerPropertySchema::new().minimum(min).maximum(max),
707 required,
708 )
709 }
710
711 #[must_use]
713 pub fn boolean(self, name: impl Into<String>, required: bool) -> Self {
714 self.property(name, BooleanPropertySchema::new(), required)
715 }
716}
717
718#[serde_as]
724#[skip_serializing_none]
725#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
726#[serde(rename_all = "camelCase")]
727#[non_exhaustive]
728pub struct ElicitationCapabilities {
729 #[serde_as(deserialize_as = "DefaultOnError")]
731 #[schemars(extend("x-deserialize-default-on-error" = true))]
732 #[serde(default)]
733 pub form: Option<ElicitationFormCapabilities>,
734 #[serde_as(deserialize_as = "DefaultOnError")]
736 #[schemars(extend("x-deserialize-default-on-error" = true))]
737 #[serde(default)]
738 pub url: Option<ElicitationUrlCapabilities>,
739 #[serde(rename = "_meta")]
745 pub meta: Option<Meta>,
746}
747
748impl ElicitationCapabilities {
749 #[must_use]
750 pub fn new() -> Self {
751 Self::default()
752 }
753
754 #[must_use]
756 pub fn form(mut self, form: impl IntoOption<ElicitationFormCapabilities>) -> Self {
757 self.form = form.into_option();
758 self
759 }
760
761 #[must_use]
763 pub fn url(mut self, url: impl IntoOption<ElicitationUrlCapabilities>) -> Self {
764 self.url = url.into_option();
765 self
766 }
767
768 #[must_use]
774 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
775 self.meta = meta.into_option();
776 self
777 }
778}
779
780#[skip_serializing_none]
786#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
787#[serde(rename_all = "camelCase")]
788#[non_exhaustive]
789pub struct ElicitationFormCapabilities {
790 #[serde(rename = "_meta")]
796 pub meta: Option<Meta>,
797}
798
799impl ElicitationFormCapabilities {
800 #[must_use]
801 pub fn new() -> Self {
802 Self::default()
803 }
804
805 #[must_use]
811 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
812 self.meta = meta.into_option();
813 self
814 }
815}
816
817#[skip_serializing_none]
823#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
824#[serde(rename_all = "camelCase")]
825#[non_exhaustive]
826pub struct ElicitationUrlCapabilities {
827 #[serde(rename = "_meta")]
833 pub meta: Option<Meta>,
834}
835
836impl ElicitationUrlCapabilities {
837 #[must_use]
838 pub fn new() -> Self {
839 Self::default()
840 }
841
842 #[must_use]
848 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
849 self.meta = meta.into_option();
850 self
851 }
852}
853
854#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
860#[serde(untagged)]
861#[non_exhaustive]
862pub enum ElicitationScope {
863 Session(ElicitationSessionScope),
865 Request(ElicitationRequestScope),
868}
869
870#[skip_serializing_none]
880#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
881#[serde(rename_all = "camelCase")]
882#[non_exhaustive]
883pub struct ElicitationSessionScope {
884 pub session_id: SessionId,
886 pub tool_call_id: Option<ToolCallId>,
888}
889
890impl ElicitationSessionScope {
891 #[must_use]
892 pub fn new(session_id: impl Into<SessionId>) -> Self {
893 Self {
894 session_id: session_id.into(),
895 tool_call_id: None,
896 }
897 }
898
899 #[must_use]
900 pub fn tool_call_id(mut self, tool_call_id: impl IntoOption<ToolCallId>) -> Self {
901 self.tool_call_id = tool_call_id.into_option();
902 self
903 }
904}
905
906#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
913#[serde(rename_all = "camelCase")]
914#[non_exhaustive]
915pub struct ElicitationRequestScope {
916 pub request_id: RequestId,
918}
919
920impl ElicitationRequestScope {
921 #[must_use]
922 pub fn new(request_id: impl Into<RequestId>) -> Self {
923 Self {
924 request_id: request_id.into(),
925 }
926 }
927}
928
929impl From<ElicitationSessionScope> for ElicitationScope {
930 fn from(scope: ElicitationSessionScope) -> Self {
931 Self::Session(scope)
932 }
933}
934
935impl From<ElicitationRequestScope> for ElicitationScope {
936 fn from(scope: ElicitationRequestScope) -> Self {
937 Self::Request(scope)
938 }
939}
940
941#[skip_serializing_none]
951#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
952#[schemars(extend("x-side" = "client", "x-method" = ELICITATION_CREATE_METHOD_NAME))]
953#[serde(rename_all = "camelCase")]
954#[non_exhaustive]
955pub struct CreateElicitationRequest {
956 #[serde(flatten)]
958 pub mode: ElicitationMode,
959 pub message: String,
961 #[serde(rename = "_meta")]
967 pub meta: Option<Meta>,
968}
969
970impl CreateElicitationRequest {
971 #[must_use]
972 pub fn new(mode: impl Into<ElicitationMode>, message: impl Into<String>) -> Self {
973 Self {
974 mode: mode.into(),
975 message: message.into(),
976 meta: None,
977 }
978 }
979
980 #[must_use]
982 pub fn scope(&self) -> &ElicitationScope {
983 self.mode.scope()
984 }
985
986 #[must_use]
992 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
993 self.meta = meta.into_option();
994 self
995 }
996}
997
998#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1004#[serde(tag = "mode", rename_all = "snake_case")]
1005#[schemars(extend("discriminator" = {"propertyName": "mode"}))]
1006#[non_exhaustive]
1007pub enum ElicitationMode {
1008 Form(ElicitationFormMode),
1010 Url(ElicitationUrlMode),
1012}
1013
1014impl From<ElicitationFormMode> for ElicitationMode {
1015 fn from(mode: ElicitationFormMode) -> Self {
1016 Self::Form(mode)
1017 }
1018}
1019
1020impl From<ElicitationUrlMode> for ElicitationMode {
1021 fn from(mode: ElicitationUrlMode) -> Self {
1022 Self::Url(mode)
1023 }
1024}
1025
1026impl ElicitationMode {
1027 #[must_use]
1029 pub fn scope(&self) -> &ElicitationScope {
1030 match self {
1031 Self::Form(f) => &f.scope,
1032 Self::Url(u) => &u.scope,
1033 }
1034 }
1035}
1036
1037#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1043#[serde(rename_all = "camelCase")]
1044#[non_exhaustive]
1045pub struct ElicitationFormMode {
1046 #[serde(flatten)]
1048 pub scope: ElicitationScope,
1049 pub requested_schema: ElicitationSchema,
1051}
1052
1053impl ElicitationFormMode {
1054 #[must_use]
1055 pub fn new(scope: impl Into<ElicitationScope>, requested_schema: ElicitationSchema) -> Self {
1056 Self {
1057 scope: scope.into(),
1058 requested_schema,
1059 }
1060 }
1061}
1062
1063#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1069#[serde(rename_all = "camelCase")]
1070#[non_exhaustive]
1071pub struct ElicitationUrlMode {
1072 #[serde(flatten)]
1074 pub scope: ElicitationScope,
1075 pub elicitation_id: ElicitationId,
1077 #[schemars(extend("format" = "uri"))]
1079 pub url: String,
1080}
1081
1082impl ElicitationUrlMode {
1083 #[must_use]
1084 pub fn new(
1085 scope: impl Into<ElicitationScope>,
1086 elicitation_id: impl Into<ElicitationId>,
1087 url: impl Into<String>,
1088 ) -> Self {
1089 Self {
1090 scope: scope.into(),
1091 elicitation_id: elicitation_id.into(),
1092 url: url.into(),
1093 }
1094 }
1095}
1096
1097#[skip_serializing_none]
1103#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1104#[schemars(extend("x-side" = "client", "x-method" = ELICITATION_CREATE_METHOD_NAME))]
1105#[serde(rename_all = "camelCase")]
1106#[non_exhaustive]
1107pub struct CreateElicitationResponse {
1108 #[serde(flatten)]
1110 pub action: ElicitationAction,
1111 #[serde(rename = "_meta")]
1117 pub meta: Option<Meta>,
1118}
1119
1120impl CreateElicitationResponse {
1121 #[must_use]
1122 pub fn new(action: ElicitationAction) -> Self {
1123 Self { action, meta: None }
1124 }
1125
1126 #[must_use]
1132 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1133 self.meta = meta.into_option();
1134 self
1135 }
1136}
1137
1138#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1144#[serde(tag = "action", rename_all = "snake_case")]
1145#[schemars(extend("discriminator" = {"propertyName": "action"}))]
1146#[non_exhaustive]
1147pub enum ElicitationAction {
1148 Accept(ElicitationAcceptAction),
1150 Decline,
1152 Cancel,
1154}
1155
1156#[skip_serializing_none]
1162#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1163#[serde(rename_all = "camelCase")]
1164#[non_exhaustive]
1165pub struct ElicitationAcceptAction {
1166 #[serde(default)]
1168 pub content: Option<BTreeMap<String, ElicitationContentValue>>,
1169}
1170
1171impl ElicitationAcceptAction {
1172 #[must_use]
1173 pub fn new() -> Self {
1174 Self { content: None }
1175 }
1176
1177 #[must_use]
1179 pub fn content(
1180 mut self,
1181 content: impl IntoOption<BTreeMap<String, ElicitationContentValue>>,
1182 ) -> Self {
1183 self.content = content.into_option();
1184 self
1185 }
1186}
1187
1188#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1189#[serde(untagged)]
1190#[non_exhaustive]
1191pub enum ElicitationContentValue {
1192 String(String),
1193 Integer(i64),
1194 Number(f64),
1195 Boolean(bool),
1196 StringArray(Vec<String>),
1197}
1198
1199impl From<String> for ElicitationContentValue {
1200 fn from(value: String) -> Self {
1201 Self::String(value)
1202 }
1203}
1204
1205impl From<&str> for ElicitationContentValue {
1206 fn from(value: &str) -> Self {
1207 Self::String(value.to_string())
1208 }
1209}
1210
1211impl From<i64> for ElicitationContentValue {
1212 fn from(value: i64) -> Self {
1213 Self::Integer(value)
1214 }
1215}
1216
1217impl From<i32> for ElicitationContentValue {
1218 fn from(value: i32) -> Self {
1219 Self::Integer(i64::from(value))
1220 }
1221}
1222
1223impl From<f64> for ElicitationContentValue {
1224 fn from(value: f64) -> Self {
1225 Self::Number(value)
1226 }
1227}
1228
1229impl From<bool> for ElicitationContentValue {
1230 fn from(value: bool) -> Self {
1231 Self::Boolean(value)
1232 }
1233}
1234
1235impl From<Vec<String>> for ElicitationContentValue {
1236 fn from(value: Vec<String>) -> Self {
1237 Self::StringArray(value)
1238 }
1239}
1240
1241impl From<Vec<&str>> for ElicitationContentValue {
1242 fn from(value: Vec<&str>) -> Self {
1243 Self::StringArray(value.into_iter().map(str::to_string).collect())
1244 }
1245}
1246
1247impl Default for ElicitationAcceptAction {
1248 fn default() -> Self {
1249 Self::new()
1250 }
1251}
1252
1253#[skip_serializing_none]
1259#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1260#[schemars(extend("x-side" = "client", "x-method" = ELICITATION_COMPLETE_NOTIFICATION))]
1261#[serde(rename_all = "camelCase")]
1262#[non_exhaustive]
1263pub struct CompleteElicitationNotification {
1264 pub elicitation_id: ElicitationId,
1266 #[serde(rename = "_meta")]
1272 pub meta: Option<Meta>,
1273}
1274
1275impl CompleteElicitationNotification {
1276 #[must_use]
1277 pub fn new(elicitation_id: impl Into<ElicitationId>) -> Self {
1278 Self {
1279 elicitation_id: elicitation_id.into(),
1280 meta: None,
1281 }
1282 }
1283
1284 #[must_use]
1290 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1291 self.meta = meta.into_option();
1292 self
1293 }
1294}
1295
1296#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1303#[serde(rename_all = "camelCase")]
1304#[non_exhaustive]
1305pub struct UrlElicitationRequiredData {
1306 pub elicitations: Vec<UrlElicitationRequiredItem>,
1308}
1309
1310impl UrlElicitationRequiredData {
1311 #[must_use]
1312 pub fn new(elicitations: Vec<UrlElicitationRequiredItem>) -> Self {
1313 Self { elicitations }
1314 }
1315}
1316
1317#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
1323#[serde(rename_all = "camelCase")]
1324#[non_exhaustive]
1325pub struct UrlElicitationRequiredItem {
1326 pub mode: ElicitationUrlOnlyMode,
1328 pub elicitation_id: ElicitationId,
1330 #[schemars(extend("format" = "uri"))]
1332 pub url: String,
1333 pub message: String,
1335}
1336
1337#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
1339#[serde(rename_all = "snake_case")]
1340#[non_exhaustive]
1341pub enum ElicitationUrlOnlyMode {
1342 #[default]
1344 Url,
1345}
1346
1347impl UrlElicitationRequiredItem {
1348 #[must_use]
1349 pub fn new(
1350 elicitation_id: impl Into<ElicitationId>,
1351 url: impl Into<String>,
1352 message: impl Into<String>,
1353 ) -> Self {
1354 Self {
1355 mode: ElicitationUrlOnlyMode::Url,
1356 elicitation_id: elicitation_id.into(),
1357 url: url.into(),
1358 message: message.into(),
1359 }
1360 }
1361}
1362
1363#[cfg(test)]
1364mod tests {
1365 use super::*;
1366 use serde_json::json;
1367
1368 #[test]
1369 fn form_mode_request_serialization() {
1370 let schema = ElicitationSchema::new().string("name", true);
1371 let req = CreateElicitationRequest::new(
1372 ElicitationFormMode::new(ElicitationSessionScope::new("sess_1"), schema),
1373 "Please enter your name",
1374 );
1375
1376 let json = serde_json::to_value(&req).unwrap();
1377 assert_eq!(json["sessionId"], "sess_1");
1378 assert!(json.get("toolCallId").is_none());
1379 assert_eq!(json["mode"], "form");
1380 assert_eq!(json["message"], "Please enter your name");
1381 assert!(json["requestedSchema"].is_object());
1382 assert_eq!(json["requestedSchema"]["type"], "object");
1383 assert_eq!(
1384 json["requestedSchema"]["properties"]["name"]["type"],
1385 "string"
1386 );
1387
1388 let roundtripped: CreateElicitationRequest = serde_json::from_value(json).unwrap();
1389 assert_eq!(
1390 *roundtripped.scope(),
1391 ElicitationSessionScope::new("sess_1").into()
1392 );
1393 assert_eq!(roundtripped.message, "Please enter your name");
1394 assert!(matches!(roundtripped.mode, ElicitationMode::Form(_)));
1395 }
1396
1397 #[test]
1398 fn url_mode_request_serialization() {
1399 let req = CreateElicitationRequest::new(
1400 ElicitationUrlMode::new(
1401 ElicitationSessionScope::new("sess_2").tool_call_id("tc_1"),
1402 "elic_1",
1403 "https://example.com/auth",
1404 ),
1405 "Please authenticate",
1406 );
1407
1408 let json = serde_json::to_value(&req).unwrap();
1409 assert_eq!(json["sessionId"], "sess_2");
1410 assert_eq!(json["toolCallId"], "tc_1");
1411 assert_eq!(json["mode"], "url");
1412 assert_eq!(json["elicitationId"], "elic_1");
1413 assert_eq!(json["url"], "https://example.com/auth");
1414 assert_eq!(json["message"], "Please authenticate");
1415
1416 let roundtripped: CreateElicitationRequest = serde_json::from_value(json).unwrap();
1417 assert_eq!(
1418 *roundtripped.scope(),
1419 ElicitationSessionScope::new("sess_2")
1420 .tool_call_id("tc_1")
1421 .into()
1422 );
1423 assert!(matches!(roundtripped.mode, ElicitationMode::Url(_)));
1424 }
1425
1426 #[test]
1427 fn response_accept_serialization() {
1428 let resp = CreateElicitationResponse::new(ElicitationAction::Accept(
1429 ElicitationAcceptAction::new().content(BTreeMap::from([(
1430 "name".to_string(),
1431 ElicitationContentValue::from("Alice"),
1432 )])),
1433 ));
1434
1435 let json = serde_json::to_value(&resp).unwrap();
1436 assert_eq!(json["action"], "accept");
1437 assert_eq!(json["content"]["name"], "Alice");
1438
1439 let roundtripped: CreateElicitationResponse = serde_json::from_value(json).unwrap();
1440 assert!(matches!(
1441 roundtripped.action,
1442 ElicitationAction::Accept(ElicitationAcceptAction {
1443 content: Some(_),
1444 ..
1445 })
1446 ));
1447 }
1448
1449 #[test]
1450 fn response_decline_serialization() {
1451 let resp = CreateElicitationResponse::new(ElicitationAction::Decline);
1452
1453 let json = serde_json::to_value(&resp).unwrap();
1454 assert_eq!(json["action"], "decline");
1455
1456 let roundtripped: CreateElicitationResponse = serde_json::from_value(json).unwrap();
1457 assert!(matches!(roundtripped.action, ElicitationAction::Decline));
1458 }
1459
1460 #[test]
1461 fn response_cancel_serialization() {
1462 let resp = CreateElicitationResponse::new(ElicitationAction::Cancel);
1463
1464 let json = serde_json::to_value(&resp).unwrap();
1465 assert_eq!(json["action"], "cancel");
1466
1467 let roundtripped: CreateElicitationResponse = serde_json::from_value(json).unwrap();
1468 assert!(matches!(roundtripped.action, ElicitationAction::Cancel));
1469 }
1470
1471 #[test]
1472 fn url_mode_request_scope_serialization() {
1473 let req = CreateElicitationRequest::new(
1474 ElicitationUrlMode::new(
1475 ElicitationRequestScope::new(RequestId::Number(42)),
1476 "elic_2",
1477 "https://example.com/setup",
1478 ),
1479 "Please complete setup",
1480 );
1481
1482 let json = serde_json::to_value(&req).unwrap();
1483 assert_eq!(json["requestId"], 42);
1484 assert!(json.get("sessionId").is_none());
1485 assert_eq!(json["mode"], "url");
1486 assert_eq!(json["elicitationId"], "elic_2");
1487 assert_eq!(json["url"], "https://example.com/setup");
1488 assert_eq!(json["message"], "Please complete setup");
1489
1490 let roundtripped: CreateElicitationRequest = serde_json::from_value(json).unwrap();
1491 assert_eq!(
1492 *roundtripped.scope(),
1493 ElicitationRequestScope::new(RequestId::Number(42)).into()
1494 );
1495 assert!(matches!(roundtripped.mode, ElicitationMode::Url(_)));
1496 }
1497
1498 #[test]
1499 fn request_scope_request_serialization() {
1500 let req = CreateElicitationRequest::new(
1501 ElicitationFormMode::new(
1502 ElicitationRequestScope::new(RequestId::Number(99)),
1503 ElicitationSchema::new().string("workspace", true),
1504 ),
1505 "Enter workspace name",
1506 );
1507
1508 let json = serde_json::to_value(&req).unwrap();
1509 assert_eq!(json["requestId"], 99);
1510 assert!(json.get("sessionId").is_none());
1511
1512 let roundtripped: CreateElicitationRequest = serde_json::from_value(json).unwrap();
1513 assert_eq!(
1514 *roundtripped.scope(),
1515 ElicitationRequestScope::new(RequestId::Number(99)).into()
1516 );
1517 }
1518
1519 #[test]
1526 fn client_response_serialization_accept() {
1527 use crate::ClientResponse;
1528
1529 let resp = ClientResponse::CreateElicitationResponse(CreateElicitationResponse::new(
1530 ElicitationAction::Accept(ElicitationAcceptAction::new().content(BTreeMap::from([(
1531 "name".to_string(),
1532 ElicitationContentValue::from("Alice"),
1533 )]))),
1534 ));
1535 let json = serde_json::to_value(&resp).unwrap();
1536 assert_eq!(json["action"], "accept");
1537 assert_eq!(json["content"]["name"], "Alice");
1538
1539 let roundtripped: CreateElicitationResponse = serde_json::from_value(json).unwrap();
1541 assert!(matches!(roundtripped.action, ElicitationAction::Accept(_)));
1542 }
1543
1544 #[test]
1545 fn client_response_serialization_decline() {
1546 use crate::ClientResponse;
1547
1548 let resp = ClientResponse::CreateElicitationResponse(CreateElicitationResponse::new(
1549 ElicitationAction::Decline,
1550 ));
1551 let json = serde_json::to_value(&resp).unwrap();
1552 assert_eq!(json["action"], "decline");
1553
1554 let roundtripped: CreateElicitationResponse = serde_json::from_value(json).unwrap();
1555 assert!(matches!(roundtripped.action, ElicitationAction::Decline));
1556 }
1557
1558 #[test]
1559 fn client_response_serialization_cancel() {
1560 use crate::ClientResponse;
1561
1562 let resp = ClientResponse::CreateElicitationResponse(CreateElicitationResponse::new(
1563 ElicitationAction::Cancel,
1564 ));
1565 let json = serde_json::to_value(&resp).unwrap();
1566 assert_eq!(json["action"], "cancel");
1567
1568 let roundtripped: CreateElicitationResponse = serde_json::from_value(json).unwrap();
1569 assert!(matches!(roundtripped.action, ElicitationAction::Cancel));
1570 }
1571
1572 #[test]
1575 fn request_tolerates_extra_fields() {
1576 let json = json!({
1577 "sessionId": "sess_1",
1578 "mode": "form",
1579 "message": "Enter your name",
1580 "requestedSchema": {
1581 "type": "object",
1582 "properties": {
1583 "name": { "type": "string", "title": "Name" }
1584 },
1585 "required": ["name"]
1586 },
1587 "unknownStringField": "hello",
1588 "unknownNumberField": 42
1589 });
1590
1591 let req: CreateElicitationRequest = serde_json::from_value(json).unwrap();
1592 assert_eq!(*req.scope(), ElicitationSessionScope::new("sess_1").into());
1593 assert_eq!(req.message, "Enter your name");
1594 assert!(matches!(req.mode, ElicitationMode::Form(_)));
1595 }
1596
1597 #[test]
1598 fn completion_notification_serialization() {
1599 let notif = CompleteElicitationNotification::new("elic_1");
1600
1601 let json = serde_json::to_value(¬if).unwrap();
1602 assert_eq!(json["elicitationId"], "elic_1");
1603
1604 let roundtripped: CompleteElicitationNotification = serde_json::from_value(json).unwrap();
1605 assert_eq!(roundtripped.elicitation_id, ElicitationId::new("elic_1"));
1606 }
1607
1608 #[test]
1609 fn capabilities_form_only() {
1610 let caps = ElicitationCapabilities::new().form(ElicitationFormCapabilities::new());
1611
1612 let json = serde_json::to_value(&caps).unwrap();
1613 assert!(json["form"].is_object());
1614 assert!(json.get("url").is_none());
1615
1616 let roundtripped: ElicitationCapabilities = serde_json::from_value(json).unwrap();
1617 assert!(roundtripped.form.is_some());
1618 assert!(roundtripped.url.is_none());
1619 }
1620
1621 #[test]
1622 fn capabilities_url_only() {
1623 let caps = ElicitationCapabilities::new().url(ElicitationUrlCapabilities::new());
1624
1625 let json = serde_json::to_value(&caps).unwrap();
1626 assert!(json.get("form").is_none());
1627 assert!(json["url"].is_object());
1628
1629 let roundtripped: ElicitationCapabilities = serde_json::from_value(json).unwrap();
1630 assert!(roundtripped.form.is_none());
1631 assert!(roundtripped.url.is_some());
1632 }
1633
1634 #[test]
1635 fn capabilities_both() {
1636 let caps = ElicitationCapabilities::new()
1637 .form(ElicitationFormCapabilities::new())
1638 .url(ElicitationUrlCapabilities::new());
1639
1640 let json = serde_json::to_value(&caps).unwrap();
1641 assert!(json["form"].is_object());
1642 assert!(json["url"].is_object());
1643
1644 let roundtripped: ElicitationCapabilities = serde_json::from_value(json).unwrap();
1645 assert!(roundtripped.form.is_some());
1646 assert!(roundtripped.url.is_some());
1647 }
1648
1649 #[test]
1650 fn url_elicitation_required_data_serialization() {
1651 let data = UrlElicitationRequiredData::new(vec![UrlElicitationRequiredItem::new(
1652 "elic_1",
1653 "https://example.com/auth",
1654 "Please authenticate",
1655 )]);
1656
1657 let json = serde_json::to_value(&data).unwrap();
1658 assert_eq!(json["elicitations"][0]["mode"], "url");
1659 assert_eq!(json["elicitations"][0]["elicitationId"], "elic_1");
1660 assert_eq!(json["elicitations"][0]["url"], "https://example.com/auth");
1661
1662 let roundtripped: UrlElicitationRequiredData = serde_json::from_value(json).unwrap();
1663 assert_eq!(roundtripped.elicitations.len(), 1);
1664 assert_eq!(
1665 roundtripped.elicitations[0].mode,
1666 ElicitationUrlOnlyMode::Url
1667 );
1668 }
1669
1670 #[test]
1671 fn schema_default_sets_object_type() {
1672 let schema = ElicitationSchema::default();
1673
1674 assert_eq!(schema.type_, ElicitationSchemaType::Object);
1675 assert!(schema.properties.is_empty());
1676
1677 let json = serde_json::to_value(&schema).unwrap();
1678 assert_eq!(json["type"], "object");
1679 }
1680
1681 #[test]
1682 fn schema_builder_serialization() {
1683 let schema = ElicitationSchema::new()
1684 .string("name", true)
1685 .email("email", true)
1686 .integer("age", 0, 150, true)
1687 .boolean("newsletter", false)
1688 .description("User registration");
1689
1690 let json = serde_json::to_value(&schema).unwrap();
1691 assert_eq!(json["type"], "object");
1692 assert_eq!(json["description"], "User registration");
1693 assert_eq!(json["properties"]["name"]["type"], "string");
1694 assert_eq!(json["properties"]["email"]["type"], "string");
1695 assert_eq!(json["properties"]["email"]["format"], "email");
1696 assert_eq!(json["properties"]["age"]["type"], "integer");
1697 assert_eq!(json["properties"]["age"]["minimum"], 0);
1698 assert_eq!(json["properties"]["age"]["maximum"], 150);
1699 assert_eq!(json["properties"]["newsletter"]["type"], "boolean");
1700
1701 let required = json["required"].as_array().unwrap();
1702 assert!(required.contains(&json!("name")));
1703 assert!(required.contains(&json!("email")));
1704 assert!(required.contains(&json!("age")));
1705 assert!(!required.contains(&json!("newsletter")));
1706
1707 let roundtripped: ElicitationSchema = serde_json::from_value(json).unwrap();
1708 assert_eq!(roundtripped.properties.len(), 4);
1709 assert!(roundtripped.required.unwrap().contains(&"name".to_string()));
1710 }
1711
1712 #[test]
1713 fn schema_string_enum_serialization() {
1714 let schema = ElicitationSchema::new().property(
1715 "color",
1716 StringPropertySchema::new().enum_values(vec![
1717 "red".into(),
1718 "green".into(),
1719 "blue".into(),
1720 ]),
1721 true,
1722 );
1723
1724 let json = serde_json::to_value(&schema).unwrap();
1725 assert_eq!(json["properties"]["color"]["type"], "string");
1726 let enum_vals = json["properties"]["color"]["enum"].as_array().unwrap();
1727 assert_eq!(enum_vals.len(), 3);
1728
1729 let roundtripped: ElicitationSchema = serde_json::from_value(json).unwrap();
1730 if let ElicitationPropertySchema::String(s) = roundtripped.properties.get("color").unwrap()
1731 {
1732 assert_eq!(s.enum_values.as_ref().unwrap().len(), 3);
1733 } else {
1734 panic!("expected String variant");
1735 }
1736 }
1737
1738 #[test]
1739 fn schema_multi_select_serialization() {
1740 let schema = ElicitationSchema::new().property(
1741 "colors",
1742 MultiSelectPropertySchema::new(vec!["red".into(), "green".into(), "blue".into()])
1743 .min_items(1)
1744 .max_items(3),
1745 false,
1746 );
1747
1748 let json = serde_json::to_value(&schema).unwrap();
1749 assert_eq!(json["properties"]["colors"]["type"], "array");
1750 assert_eq!(json["properties"]["colors"]["items"]["type"], "string");
1751 assert_eq!(json["properties"]["colors"]["minItems"], 1);
1752 assert_eq!(json["properties"]["colors"]["maxItems"], 3);
1753
1754 let roundtripped: ElicitationSchema = serde_json::from_value(json).unwrap();
1755 assert!(matches!(
1756 roundtripped.properties.get("colors").unwrap(),
1757 ElicitationPropertySchema::Array(_)
1758 ));
1759 }
1760
1761 #[test]
1762 fn schema_titled_enum_serialization() {
1763 let schema = ElicitationSchema::new().property(
1764 "country",
1765 StringPropertySchema::new().one_of(vec![
1766 EnumOption::new("us", "United States"),
1767 EnumOption::new("uk", "United Kingdom"),
1768 ]),
1769 true,
1770 );
1771
1772 let json = serde_json::to_value(&schema).unwrap();
1773 assert_eq!(json["properties"]["country"]["type"], "string");
1774 let one_of = json["properties"]["country"]["oneOf"].as_array().unwrap();
1775 assert_eq!(one_of.len(), 2);
1776 assert_eq!(one_of[0]["const"], "us");
1777 assert_eq!(one_of[0]["title"], "United States");
1778
1779 let roundtripped: ElicitationSchema = serde_json::from_value(json).unwrap();
1780 if let ElicitationPropertySchema::String(s) =
1781 roundtripped.properties.get("country").unwrap()
1782 {
1783 assert_eq!(s.one_of.as_ref().unwrap().len(), 2);
1784 } else {
1785 panic!("expected String variant");
1786 }
1787 }
1788
1789 #[test]
1790 fn schema_number_property_serialization() {
1791 let schema = ElicitationSchema::new().number("rating", 0.0, 5.0, true);
1792
1793 let json = serde_json::to_value(&schema).unwrap();
1794 assert_eq!(json["properties"]["rating"]["type"], "number");
1795 assert_eq!(json["properties"]["rating"]["minimum"], 0.0);
1796 assert_eq!(json["properties"]["rating"]["maximum"], 5.0);
1797
1798 let roundtripped: ElicitationSchema = serde_json::from_value(json).unwrap();
1799 if let ElicitationPropertySchema::Number(n) = roundtripped.properties.get("rating").unwrap()
1800 {
1801 assert_eq!(n.minimum, Some(0.0));
1802 assert_eq!(n.maximum, Some(5.0));
1803 } else {
1804 panic!("expected Number variant");
1805 }
1806 }
1807
1808 #[test]
1809 fn schema_string_format_serialization() {
1810 let schema = ElicitationSchema::new()
1811 .uri("website", true)
1812 .date("birthday", true)
1813 .date_time("updated_at", false);
1814
1815 let json = serde_json::to_value(&schema).unwrap();
1816 assert_eq!(json["properties"]["website"]["type"], "string");
1817 assert_eq!(json["properties"]["website"]["format"], "uri");
1818 assert_eq!(json["properties"]["birthday"]["type"], "string");
1819 assert_eq!(json["properties"]["birthday"]["format"], "date");
1820 assert_eq!(json["properties"]["updated_at"]["type"], "string");
1821 assert_eq!(json["properties"]["updated_at"]["format"], "date-time");
1822
1823 let required = json["required"].as_array().unwrap();
1824 assert!(required.contains(&json!("website")));
1825 assert!(required.contains(&json!("birthday")));
1826 assert!(!required.contains(&json!("updated_at")));
1827 }
1828
1829 #[test]
1830 fn schema_string_pattern_serialization() {
1831 let schema = ElicitationSchema::new().property(
1832 "name",
1833 StringPropertySchema::new()
1834 .min_length(1)
1835 .max_length(64)
1836 .pattern("^[a-zA-Z_][a-zA-Z0-9_]*$"),
1837 true,
1838 );
1839
1840 let json = serde_json::to_value(&schema).unwrap();
1841 assert_eq!(json["properties"]["name"]["type"], "string");
1842 assert_eq!(
1843 json["properties"]["name"]["pattern"],
1844 "^[a-zA-Z_][a-zA-Z0-9_]*$"
1845 );
1846
1847 let roundtripped: ElicitationSchema = serde_json::from_value(json).unwrap();
1848 if let ElicitationPropertySchema::String(s) = roundtripped.properties.get("name").unwrap() {
1849 assert_eq!(s.pattern.as_deref(), Some("^[a-zA-Z_][a-zA-Z0-9_]*$"));
1850 } else {
1851 panic!("expected String variant");
1852 }
1853 }
1854
1855 #[test]
1856 fn schema_property_updates_required_state() {
1857 let schema = ElicitationSchema::new()
1858 .string("name", true)
1859 .email("name", false);
1860
1861 let json = serde_json::to_value(&schema).unwrap();
1862 assert!(json.get("required").is_none());
1863 assert_eq!(json["properties"]["name"]["format"], "email");
1864 }
1865
1866 #[test]
1867 fn schema_rejects_invalid_object_type() {
1868 let err = serde_json::from_value::<ElicitationSchema>(json!({
1869 "type": "array",
1870 "properties": {
1871 "name": {
1872 "type": "string"
1873 }
1874 }
1875 }))
1876 .unwrap_err();
1877
1878 assert!(err.to_string().contains("unknown variant"));
1879 }
1880
1881 #[test]
1882 fn titled_multi_select_items_reject_one_of() {
1883 let err = serde_json::from_value::<TitledMultiSelectItems>(json!({
1884 "oneOf": [
1885 {
1886 "const": "red",
1887 "title": "Red"
1888 }
1889 ]
1890 }))
1891 .unwrap_err();
1892
1893 assert!(err.to_string().contains("missing field `anyOf`"));
1894 }
1895
1896 #[test]
1897 fn response_accept_rejects_non_object_content() {
1898 let err = serde_json::from_value::<CreateElicitationResponse>(json!({
1899 "action": "accept",
1900 "content": "Alice"
1901 }))
1902 .unwrap_err();
1903
1904 assert!(err.to_string().contains("invalid type"));
1905 }
1906
1907 #[test]
1908 fn response_accept_rejects_nested_object_content() {
1909 let err = serde_json::from_value::<CreateElicitationResponse>(json!({
1910 "action": "accept",
1911 "content": {
1912 "profile": {
1913 "name": "Alice"
1914 }
1915 }
1916 }))
1917 .unwrap_err();
1918
1919 assert!(err.to_string().contains("data did not match any variant"));
1920 }
1921
1922 #[test]
1923 fn response_accept_allows_primitive_and_string_array_content() {
1924 let response = CreateElicitationResponse::new(ElicitationAction::Accept(
1925 ElicitationAcceptAction::new().content(BTreeMap::from([
1926 ("name".to_string(), ElicitationContentValue::from("Alice")),
1927 ("age".to_string(), ElicitationContentValue::from(30_i32)),
1928 ("score".to_string(), ElicitationContentValue::from(9.5_f64)),
1929 (
1930 "subscribed".to_string(),
1931 ElicitationContentValue::from(true),
1932 ),
1933 (
1934 "tags".to_string(),
1935 ElicitationContentValue::from(vec!["rust", "acp"]),
1936 ),
1937 ])),
1938 ));
1939
1940 let json = serde_json::to_value(&response).unwrap();
1941 assert_eq!(json["action"], "accept");
1942 assert_eq!(json["content"]["name"], "Alice");
1943 assert_eq!(json["content"]["age"], 30);
1944 assert_eq!(json["content"]["score"], 9.5);
1945 assert_eq!(json["content"]["subscribed"], true);
1946 assert_eq!(json["content"]["tags"][0], "rust");
1947 assert_eq!(json["content"]["tags"][1], "acp");
1948 }
1949}