1use std::{borrow::Cow, collections::BTreeMap, marker::PhantomData};
20
21use serde::{Deserialize, Serialize};
22
23use crate::{const_string, model::ConstString};
24
25const_string!(ObjectTypeConst = "object");
30const_string!(StringTypeConst = "string");
31const_string!(NumberTypeConst = "number");
32const_string!(IntegerTypeConst = "integer");
33const_string!(BooleanTypeConst = "boolean");
34const_string!(EnumTypeConst = "string");
35const_string!(ArrayTypeConst = "array");
36
37#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
50#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
51#[serde(untagged)]
52pub enum PrimitiveSchema {
53 Enum(EnumSchema),
55 String(StringSchema),
57 Number(NumberSchema),
59 Integer(IntegerSchema),
61 Boolean(BooleanSchema),
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
71#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
72#[serde(rename_all = "kebab-case")]
73pub enum StringFormat {
74 Email,
76 Uri,
78 Date,
80 DateTime,
82}
83
84#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
90#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
91#[serde(rename_all = "camelCase")]
92pub struct StringSchema {
93 #[serde(rename = "type")]
95 pub type_: StringTypeConst,
96
97 #[serde(skip_serializing_if = "Option::is_none")]
99 pub title: Option<Cow<'static, str>>,
100
101 #[serde(skip_serializing_if = "Option::is_none")]
103 pub description: Option<Cow<'static, str>>,
104
105 #[serde(skip_serializing_if = "Option::is_none")]
107 pub min_length: Option<u32>,
108
109 #[serde(skip_serializing_if = "Option::is_none")]
111 pub max_length: Option<u32>,
112
113 #[serde(skip_serializing_if = "Option::is_none")]
115 pub format: Option<StringFormat>,
116}
117
118impl Default for StringSchema {
119 fn default() -> Self {
120 Self {
121 type_: StringTypeConst,
122 title: None,
123 description: None,
124 min_length: None,
125 max_length: None,
126 format: None,
127 }
128 }
129}
130
131impl StringSchema {
132 pub fn new() -> Self {
134 Self::default()
135 }
136
137 pub fn email() -> Self {
139 Self {
140 format: Some(StringFormat::Email),
141 ..Default::default()
142 }
143 }
144
145 pub fn uri() -> Self {
147 Self {
148 format: Some(StringFormat::Uri),
149 ..Default::default()
150 }
151 }
152
153 pub fn date() -> Self {
155 Self {
156 format: Some(StringFormat::Date),
157 ..Default::default()
158 }
159 }
160
161 pub fn date_time() -> Self {
163 Self {
164 format: Some(StringFormat::DateTime),
165 ..Default::default()
166 }
167 }
168
169 pub fn title(mut self, title: impl Into<Cow<'static, str>>) -> Self {
171 self.title = Some(title.into());
172 self
173 }
174
175 pub fn description(mut self, description: impl Into<Cow<'static, str>>) -> Self {
177 self.description = Some(description.into());
178 self
179 }
180
181 pub fn with_length(mut self, min: u32, max: u32) -> Result<Self, &'static str> {
183 if min > max {
184 return Err("min_length must be <= max_length");
185 }
186 self.min_length = Some(min);
187 self.max_length = Some(max);
188 Ok(self)
189 }
190
191 pub fn length(mut self, min: u32, max: u32) -> Self {
193 assert!(min <= max, "min_length must be <= max_length");
194 self.min_length = Some(min);
195 self.max_length = Some(max);
196 self
197 }
198
199 pub fn min_length(mut self, min: u32) -> Self {
201 self.min_length = Some(min);
202 self
203 }
204
205 pub fn max_length(mut self, max: u32) -> Self {
207 self.max_length = Some(max);
208 self
209 }
210
211 pub fn format(mut self, format: StringFormat) -> Self {
213 self.format = Some(format);
214 self
215 }
216}
217
218#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
227#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
228#[serde(rename_all = "camelCase")]
229pub struct NumberSchema {
230 #[serde(rename = "type")]
232 pub type_: NumberTypeConst,
233
234 #[serde(skip_serializing_if = "Option::is_none")]
236 pub title: Option<Cow<'static, str>>,
237
238 #[serde(skip_serializing_if = "Option::is_none")]
240 pub description: Option<Cow<'static, str>>,
241
242 #[serde(skip_serializing_if = "Option::is_none")]
244 pub minimum: Option<f64>,
245
246 #[serde(skip_serializing_if = "Option::is_none")]
248 pub maximum: Option<f64>,
249}
250
251impl Default for NumberSchema {
252 fn default() -> Self {
253 Self {
254 type_: NumberTypeConst,
255 title: None,
256 description: None,
257 minimum: None,
258 maximum: None,
259 }
260 }
261}
262
263impl NumberSchema {
264 pub fn new() -> Self {
266 Self::default()
267 }
268
269 pub fn with_range(mut self, min: f64, max: f64) -> Result<Self, &'static str> {
271 if min > max {
272 return Err("minimum must be <= maximum");
273 }
274 self.minimum = Some(min);
275 self.maximum = Some(max);
276 Ok(self)
277 }
278
279 pub fn range(mut self, min: f64, max: f64) -> Self {
281 assert!(min <= max, "minimum must be <= maximum");
282 self.minimum = Some(min);
283 self.maximum = Some(max);
284 self
285 }
286
287 pub fn minimum(mut self, min: f64) -> Self {
289 self.minimum = Some(min);
290 self
291 }
292
293 pub fn maximum(mut self, max: f64) -> Self {
295 self.maximum = Some(max);
296 self
297 }
298
299 pub fn title(mut self, title: impl Into<Cow<'static, str>>) -> Self {
301 self.title = Some(title.into());
302 self
303 }
304
305 pub fn description(mut self, description: impl Into<Cow<'static, str>>) -> Self {
307 self.description = Some(description.into());
308 self
309 }
310}
311
312#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
321#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
322#[serde(rename_all = "camelCase")]
323pub struct IntegerSchema {
324 #[serde(rename = "type")]
326 pub type_: IntegerTypeConst,
327
328 #[serde(skip_serializing_if = "Option::is_none")]
330 pub title: Option<Cow<'static, str>>,
331
332 #[serde(skip_serializing_if = "Option::is_none")]
334 pub description: Option<Cow<'static, str>>,
335
336 #[serde(skip_serializing_if = "Option::is_none")]
338 pub minimum: Option<i64>,
339
340 #[serde(skip_serializing_if = "Option::is_none")]
342 pub maximum: Option<i64>,
343}
344
345impl Default for IntegerSchema {
346 fn default() -> Self {
347 Self {
348 type_: IntegerTypeConst,
349 title: None,
350 description: None,
351 minimum: None,
352 maximum: None,
353 }
354 }
355}
356
357impl IntegerSchema {
358 pub fn new() -> Self {
360 Self::default()
361 }
362
363 pub fn with_range(mut self, min: i64, max: i64) -> Result<Self, &'static str> {
365 if min > max {
366 return Err("minimum must be <= maximum");
367 }
368 self.minimum = Some(min);
369 self.maximum = Some(max);
370 Ok(self)
371 }
372
373 pub fn range(mut self, min: i64, max: i64) -> Self {
375 assert!(min <= max, "minimum must be <= maximum");
376 self.minimum = Some(min);
377 self.maximum = Some(max);
378 self
379 }
380
381 pub fn minimum(mut self, min: i64) -> Self {
383 self.minimum = Some(min);
384 self
385 }
386
387 pub fn maximum(mut self, max: i64) -> Self {
389 self.maximum = Some(max);
390 self
391 }
392
393 pub fn title(mut self, title: impl Into<Cow<'static, str>>) -> Self {
395 self.title = Some(title.into());
396 self
397 }
398
399 pub fn description(mut self, description: impl Into<Cow<'static, str>>) -> Self {
401 self.description = Some(description.into());
402 self
403 }
404}
405
406#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
412#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
413#[serde(rename_all = "camelCase")]
414pub struct BooleanSchema {
415 #[serde(rename = "type")]
417 pub type_: BooleanTypeConst,
418
419 #[serde(skip_serializing_if = "Option::is_none")]
421 pub title: Option<Cow<'static, str>>,
422
423 #[serde(skip_serializing_if = "Option::is_none")]
425 pub description: Option<Cow<'static, str>>,
426
427 #[serde(skip_serializing_if = "Option::is_none")]
429 pub default: Option<bool>,
430}
431
432impl Default for BooleanSchema {
433 fn default() -> Self {
434 Self {
435 type_: BooleanTypeConst,
436 title: None,
437 description: None,
438 default: None,
439 }
440 }
441}
442
443impl BooleanSchema {
444 pub fn new() -> Self {
446 Self::default()
447 }
448
449 pub fn title(mut self, title: impl Into<Cow<'static, str>>) -> Self {
451 self.title = Some(title.into());
452 self
453 }
454
455 pub fn description(mut self, description: impl Into<Cow<'static, str>>) -> Self {
457 self.description = Some(description.into());
458 self
459 }
460
461 pub fn with_default(mut self, default: bool) -> Self {
463 self.default = Some(default);
464 self
465 }
466}
467
468#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
476#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
477pub struct ConstTitle {
478 #[serde(rename = "const")]
479 pub const_: String,
480 pub title: String,
481}
482
483#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
485#[serde(rename_all = "camelCase")]
486#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
487pub struct LegacyEnumSchema {
488 #[serde(rename = "type")]
489 pub type_: StringTypeConst,
490 #[serde(skip_serializing_if = "Option::is_none")]
491 pub title: Option<Cow<'static, str>>,
492 #[serde(skip_serializing_if = "Option::is_none")]
493 pub description: Option<Cow<'static, str>>,
494 #[serde(rename = "enum")]
495 pub enum_: Vec<String>,
496 pub enum_names: Option<Vec<String>>,
497}
498
499#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
501#[serde(rename_all = "camelCase")]
502#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
503pub struct UntitledSingleSelectEnumSchema {
504 #[serde(rename = "type")]
505 pub type_: StringTypeConst,
506 #[serde(skip_serializing_if = "Option::is_none")]
507 pub title: Option<Cow<'static, str>>,
508 #[serde(skip_serializing_if = "Option::is_none")]
509 pub description: Option<Cow<'static, str>>,
510 #[serde(rename = "enum")]
511 pub enum_: Vec<String>,
512 #[serde(skip_serializing_if = "Option::is_none")]
513 pub default: Option<String>,
514}
515
516#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
518#[serde(rename_all = "camelCase")]
519#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
520pub struct TitledSingleSelectEnumSchema {
521 #[serde(rename = "type")]
522 pub type_: StringTypeConst,
523 #[serde(skip_serializing_if = "Option::is_none")]
524 pub title: Option<Cow<'static, str>>,
525 #[serde(skip_serializing_if = "Option::is_none")]
526 pub description: Option<Cow<'static, str>>,
527 #[serde(rename = "oneOf")]
528 pub one_of: Vec<ConstTitle>,
529 #[serde(skip_serializing_if = "Option::is_none")]
530 pub default: Option<String>,
531}
532
533#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
535#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
536#[serde(untagged)]
537pub enum SingleSelectEnumSchema {
538 Untitled(UntitledSingleSelectEnumSchema),
539 Titled(TitledSingleSelectEnumSchema),
540}
541
542#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
544#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
545pub struct UntitledItems {
546 #[serde(rename = "type")]
547 pub type_: StringTypeConst,
548 #[serde(rename = "enum")]
549 pub enum_: Vec<String>,
550}
551
552#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
554#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
555pub struct TitledItems {
556 #[serde(rename = "anyOf", alias = "oneOf")]
559 pub any_of: Vec<ConstTitle>,
560}
561
562#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
564#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
565#[serde(rename_all = "camelCase")]
566pub struct UntitledMultiSelectEnumSchema {
567 #[serde(rename = "type")]
568 pub type_: ArrayTypeConst,
569 #[serde(skip_serializing_if = "Option::is_none")]
570 pub title: Option<Cow<'static, str>>,
571 #[serde(skip_serializing_if = "Option::is_none")]
572 pub description: Option<Cow<'static, str>>,
573 #[serde(skip_serializing_if = "Option::is_none")]
574 pub min_items: Option<u64>,
575 #[serde(skip_serializing_if = "Option::is_none")]
576 pub max_items: Option<u64>,
577 pub items: UntitledItems,
578 #[serde(skip_serializing_if = "Option::is_none")]
579 pub default: Option<Vec<String>>,
580}
581
582#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
584#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
585#[serde(rename_all = "camelCase")]
586pub struct TitledMultiSelectEnumSchema {
587 #[serde(rename = "type")]
588 pub type_: ArrayTypeConst,
589 #[serde(skip_serializing_if = "Option::is_none")]
590 pub title: Option<Cow<'static, str>>,
591 #[serde(skip_serializing_if = "Option::is_none")]
592 pub description: Option<Cow<'static, str>>,
593 #[serde(skip_serializing_if = "Option::is_none")]
594 pub min_items: Option<u64>,
595 #[serde(skip_serializing_if = "Option::is_none")]
596 pub max_items: Option<u64>,
597 pub items: TitledItems,
598 #[serde(skip_serializing_if = "Option::is_none")]
599 pub default: Option<Vec<String>>,
600}
601
602#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
604#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
605#[serde(untagged)]
606pub enum MultiSelectEnumSchema {
607 Untitled(UntitledMultiSelectEnumSchema),
608 Titled(TitledMultiSelectEnumSchema),
609}
610
611#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
627#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
628#[serde(untagged)]
629pub enum EnumSchema {
630 Single(SingleSelectEnumSchema),
631 Multi(MultiSelectEnumSchema),
632 Legacy(LegacyEnumSchema),
633}
634
635#[derive(Debug)]
637pub struct SingleSelect;
638#[derive(Debug)]
640pub struct MultiSelect;
641#[derive(Debug)]
658pub struct EnumSchemaBuilder<T> {
659 enum_values: Vec<String>,
661 titled: bool,
663 title: Option<Cow<'static, str>>,
665 description: Option<Cow<'static, str>>,
667 enum_titles: Vec<String>,
669 min_items: Option<u64>,
671 max_items: Option<u64>,
673 default: Vec<String>,
675 select_type: PhantomData<T>,
676}
677
678impl Default for EnumSchemaBuilder<SingleSelect> {
680 fn default() -> Self {
681 Self {
682 title: None,
683 description: None,
684 titled: false,
685 enum_titles: Vec::new(),
686 enum_values: Vec::new(),
687 min_items: None,
688 max_items: None,
689 default: Vec::new(),
690 select_type: PhantomData,
691 }
692 }
693}
694
695impl<T> EnumSchemaBuilder<T> {
697 pub fn title(mut self, value: impl Into<Cow<'static, str>>) -> Self {
699 self.title = Some(value.into());
700 self
701 }
702
703 pub fn description(mut self, value: impl Into<Cow<'static, str>>) -> Self {
705 self.description = Some(value.into());
706 self
707 }
708
709 pub fn untitled(mut self) -> Self {
712 self.enum_titles = Vec::new();
713 self.titled = false;
714 self
715 }
716
717 pub fn enum_titles(mut self, titles: Vec<String>) -> Result<EnumSchemaBuilder<T>, String> {
719 if titles.len() != self.enum_values.len() {
720 return Err(format!(
721 "Provided number of titles do not match number of values: expected {}, but got {}",
722 self.enum_values.len(),
723 titles.len()
724 ));
725 }
726 self.titled = true;
727 self.enum_titles = titles;
728 Ok(self)
729 }
730}
731
732impl EnumSchemaBuilder<SingleSelect> {
734 pub fn new(values: Vec<String>) -> EnumSchemaBuilder<SingleSelect> {
735 EnumSchemaBuilder {
736 enum_values: values,
737 ..Default::default()
738 }
739 }
740
741 pub fn multiselect(self) -> EnumSchemaBuilder<MultiSelect> {
747 EnumSchemaBuilder {
748 enum_values: self.enum_values,
749 titled: self.titled,
750 title: self.title,
751 description: self.description,
752 enum_titles: self.enum_titles,
753 min_items: None,
754 max_items: None,
755 default: Vec::new(), select_type: PhantomData,
757 }
758 }
759
760 pub fn with_default(
762 mut self,
763 default_value: impl Into<String>,
764 ) -> Result<EnumSchemaBuilder<SingleSelect>, String> {
765 let value: String = default_value.into();
766 if !self.enum_values.contains(&value) {
767 return Err("Provided default value is not in enum values".to_string());
768 }
769 self.default = vec![value];
770 Ok(self)
771 }
772
773 pub fn build(mut self) -> EnumSchema {
775 match self.titled {
776 false => EnumSchema::Single(SingleSelectEnumSchema::Untitled(
777 UntitledSingleSelectEnumSchema {
778 type_: StringTypeConst,
779 title: self.title,
780 description: self.description,
781 enum_: self.enum_values,
782 default: self.default.pop(),
783 },
784 )),
785 true => EnumSchema::Single(SingleSelectEnumSchema::Titled(
786 TitledSingleSelectEnumSchema {
787 type_: StringTypeConst,
788 title: self.title,
789 description: self.description,
790 one_of: self
791 .enum_titles
792 .into_iter()
793 .zip(self.enum_values)
794 .map(|(title, const_)| ConstTitle { const_, title })
795 .collect(),
796 default: self.default.pop(),
797 },
798 )),
799 }
800 }
801}
802
803impl EnumSchemaBuilder<MultiSelect> {
805 pub fn single_select(self) -> EnumSchemaBuilder<SingleSelect> {
808 EnumSchemaBuilder {
809 enum_values: self.enum_values,
810 titled: self.titled,
811 title: self.title,
812 description: self.description,
813 enum_titles: self.enum_titles,
814 min_items: None,
815 max_items: None,
816 default: Vec::new(), select_type: PhantomData,
818 }
819 }
820
821 pub fn with_default(
823 mut self,
824 default_values: Vec<String>,
825 ) -> Result<EnumSchemaBuilder<MultiSelect>, String> {
826 for value in &default_values {
827 if !self.enum_values.contains(value) {
828 return Err("One of the provided default values is not in enum values".to_string());
829 }
830 }
831 if let Some(min) = self.min_items {
832 if (default_values.len() as u64) < min {
833 return Err("Number of provided default values is less than min_items".to_string());
834 }
835 }
836 if let Some(max) = self.max_items {
837 if (default_values.len() as u64) > max {
838 return Err(
839 "Number of provided default values is greater than max_items".to_string(),
840 );
841 }
842 }
843 self.default = default_values;
844 Ok(self)
845 }
846
847 pub fn min_items(mut self, value: u64) -> Result<EnumSchemaBuilder<MultiSelect>, String> {
849 if let Some(max) = self.max_items
850 && value > max
851 {
852 return Err("Provided value is greater than max_items".to_string());
853 }
854 self.min_items = Some(value);
855 Ok(self)
856 }
857
858 pub fn max_items(mut self, value: u64) -> Result<EnumSchemaBuilder<MultiSelect>, String> {
860 if let Some(min) = self.min_items
861 && value < min
862 {
863 return Err("Provided value is less than min_items".to_string());
864 }
865 self.max_items = Some(value);
866 Ok(self)
867 }
868
869 pub fn build(self) -> EnumSchema {
871 match self.titled {
872 false => EnumSchema::Multi(MultiSelectEnumSchema::Untitled(
873 UntitledMultiSelectEnumSchema {
874 type_: ArrayTypeConst,
875 title: self.title,
876 description: self.description,
877 min_items: self.min_items,
878 max_items: self.max_items,
879 items: UntitledItems {
880 type_: StringTypeConst,
881 enum_: self.enum_values,
882 },
883 default: if self.default.is_empty() {
884 None
885 } else {
886 Some(self.default)
887 },
888 },
889 )),
890 true => EnumSchema::Multi(MultiSelectEnumSchema::Titled(TitledMultiSelectEnumSchema {
891 type_: ArrayTypeConst,
892 title: self.title,
893 description: self.description,
894 min_items: self.min_items,
895 max_items: self.max_items,
896 items: TitledItems {
897 any_of: self
898 .enum_titles
899 .into_iter()
900 .zip(self.enum_values)
901 .map(|(title, const_)| ConstTitle { const_, title })
902 .collect(),
903 },
904 default: if self.default.is_empty() {
905 None
906 } else {
907 Some(self.default)
908 },
909 })),
910 }
911 }
912}
913
914impl EnumSchema {
915 pub fn builder(values: Vec<String>) -> EnumSchemaBuilder<SingleSelect> {
938 EnumSchemaBuilder::new(values)
939 }
940}
941
942#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
963#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
964#[serde(rename_all = "camelCase")]
965pub struct ElicitationSchema {
966 #[serde(rename = "type")]
968 pub type_: ObjectTypeConst,
969
970 #[serde(skip_serializing_if = "Option::is_none")]
972 pub title: Option<Cow<'static, str>>,
973
974 pub properties: BTreeMap<String, PrimitiveSchema>,
976
977 #[serde(skip_serializing_if = "Option::is_none")]
979 pub required: Option<Vec<String>>,
980
981 #[serde(skip_serializing_if = "Option::is_none")]
983 pub description: Option<Cow<'static, str>>,
984}
985
986impl ElicitationSchema {
987 pub fn new(properties: BTreeMap<String, PrimitiveSchema>) -> Self {
989 Self {
990 type_: ObjectTypeConst,
991 title: None,
992 properties,
993 required: None,
994 description: None,
995 }
996 }
997
998 pub fn from_json_schema(schema: crate::model::JsonObject) -> Result<Self, serde_json::Error> {
1017 serde_json::from_value(serde_json::Value::Object(schema))
1018 }
1019
1020 #[cfg(feature = "schemars")]
1046 pub fn from_type<T>() -> Result<Self, serde_json::Error>
1047 where
1048 T: schemars::JsonSchema,
1049 {
1050 use crate::schemars::generate::SchemaSettings;
1051
1052 let mut settings = SchemaSettings::draft07();
1053 settings.transforms = vec![Box::new(schemars::transform::AddNullable::default())];
1054 let generator = settings.into_generator();
1055 let schema = generator.into_root_schema_for::<T>();
1056 let object = serde_json::to_value(schema).expect("failed to serialize schema");
1057 match object {
1058 serde_json::Value::Object(object) => Self::from_json_schema(object),
1059 _ => panic!(
1060 "Schema serialization produced non-object value: expected JSON object but got {:?}",
1061 object
1062 ),
1063 }
1064 }
1065
1066 pub fn with_required(mut self, required: Vec<String>) -> Self {
1068 self.required = Some(required);
1069 self
1070 }
1071
1072 pub fn with_title(mut self, title: impl Into<Cow<'static, str>>) -> Self {
1074 self.title = Some(title.into());
1075 self
1076 }
1077
1078 pub fn with_description(mut self, description: impl Into<Cow<'static, str>>) -> Self {
1080 self.description = Some(description.into());
1081 self
1082 }
1083
1084 pub fn builder() -> ElicitationSchemaBuilder {
1086 ElicitationSchemaBuilder::new()
1087 }
1088}
1089
1090#[derive(Debug, Default)]
1109pub struct ElicitationSchemaBuilder {
1110 pub properties: BTreeMap<String, PrimitiveSchema>,
1111 pub required: Vec<String>,
1112 pub title: Option<Cow<'static, str>>,
1113 pub description: Option<Cow<'static, str>>,
1114}
1115
1116impl ElicitationSchemaBuilder {
1117 pub fn new() -> Self {
1119 Self::default()
1120 }
1121
1122 pub fn property(mut self, name: impl Into<String>, schema: PrimitiveSchema) -> Self {
1124 self.properties.insert(name.into(), schema);
1125 self
1126 }
1127
1128 pub fn required_property(mut self, name: impl Into<String>, schema: PrimitiveSchema) -> Self {
1130 let name_str = name.into();
1131 self.required.push(name_str.clone());
1132 self.properties.insert(name_str, schema);
1133 self
1134 }
1135
1136 pub fn string_property(
1142 mut self,
1143 name: impl Into<String>,
1144 f: impl FnOnce(StringSchema) -> StringSchema,
1145 ) -> Self {
1146 self.properties
1147 .insert(name.into(), PrimitiveSchema::String(f(StringSchema::new())));
1148 self
1149 }
1150
1151 pub fn required_string_property(
1153 mut self,
1154 name: impl Into<String>,
1155 f: impl FnOnce(StringSchema) -> StringSchema,
1156 ) -> Self {
1157 let name_str = name.into();
1158 self.required.push(name_str.clone());
1159 self.properties
1160 .insert(name_str, PrimitiveSchema::String(f(StringSchema::new())));
1161 self
1162 }
1163
1164 pub fn number_property(
1166 mut self,
1167 name: impl Into<String>,
1168 f: impl FnOnce(NumberSchema) -> NumberSchema,
1169 ) -> Self {
1170 self.properties
1171 .insert(name.into(), PrimitiveSchema::Number(f(NumberSchema::new())));
1172 self
1173 }
1174
1175 pub fn required_number_property(
1177 mut self,
1178 name: impl Into<String>,
1179 f: impl FnOnce(NumberSchema) -> NumberSchema,
1180 ) -> Self {
1181 let name_str = name.into();
1182 self.required.push(name_str.clone());
1183 self.properties
1184 .insert(name_str, PrimitiveSchema::Number(f(NumberSchema::new())));
1185 self
1186 }
1187
1188 pub fn integer_property(
1190 mut self,
1191 name: impl Into<String>,
1192 f: impl FnOnce(IntegerSchema) -> IntegerSchema,
1193 ) -> Self {
1194 self.properties.insert(
1195 name.into(),
1196 PrimitiveSchema::Integer(f(IntegerSchema::new())),
1197 );
1198 self
1199 }
1200
1201 pub fn required_integer_property(
1203 mut self,
1204 name: impl Into<String>,
1205 f: impl FnOnce(IntegerSchema) -> IntegerSchema,
1206 ) -> Self {
1207 let name_str = name.into();
1208 self.required.push(name_str.clone());
1209 self.properties
1210 .insert(name_str, PrimitiveSchema::Integer(f(IntegerSchema::new())));
1211 self
1212 }
1213
1214 pub fn bool_property(
1216 mut self,
1217 name: impl Into<String>,
1218 f: impl FnOnce(BooleanSchema) -> BooleanSchema,
1219 ) -> Self {
1220 self.properties.insert(
1221 name.into(),
1222 PrimitiveSchema::Boolean(f(BooleanSchema::new())),
1223 );
1224 self
1225 }
1226
1227 pub fn required_bool_property(
1229 mut self,
1230 name: impl Into<String>,
1231 f: impl FnOnce(BooleanSchema) -> BooleanSchema,
1232 ) -> Self {
1233 let name_str = name.into();
1234 self.required.push(name_str.clone());
1235 self.properties
1236 .insert(name_str, PrimitiveSchema::Boolean(f(BooleanSchema::new())));
1237 self
1238 }
1239
1240 pub fn required_string(self, name: impl Into<String>) -> Self {
1246 self.required_property(name, PrimitiveSchema::String(StringSchema::new()))
1247 }
1248
1249 pub fn optional_string(self, name: impl Into<String>) -> Self {
1251 self.property(name, PrimitiveSchema::String(StringSchema::new()))
1252 }
1253
1254 pub fn required_email(self, name: impl Into<String>) -> Self {
1256 self.required_property(name, PrimitiveSchema::String(StringSchema::email()))
1257 }
1258
1259 pub fn optional_email(self, name: impl Into<String>) -> Self {
1261 self.property(name, PrimitiveSchema::String(StringSchema::email()))
1262 }
1263
1264 pub fn required_string_with(
1266 self,
1267 name: impl Into<String>,
1268 f: impl FnOnce(StringSchema) -> StringSchema,
1269 ) -> Self {
1270 self.required_property(name, PrimitiveSchema::String(f(StringSchema::new())))
1271 }
1272
1273 pub fn optional_string_with(
1275 self,
1276 name: impl Into<String>,
1277 f: impl FnOnce(StringSchema) -> StringSchema,
1278 ) -> Self {
1279 self.property(name, PrimitiveSchema::String(f(StringSchema::new())))
1280 }
1281
1282 pub fn required_number(self, name: impl Into<String>, min: f64, max: f64) -> Self {
1286 self.required_property(
1287 name,
1288 PrimitiveSchema::Number(NumberSchema::new().range(min, max)),
1289 )
1290 }
1291
1292 pub fn optional_number(self, name: impl Into<String>, min: f64, max: f64) -> Self {
1294 self.property(
1295 name,
1296 PrimitiveSchema::Number(NumberSchema::new().range(min, max)),
1297 )
1298 }
1299
1300 pub fn required_number_with(
1302 self,
1303 name: impl Into<String>,
1304 f: impl FnOnce(NumberSchema) -> NumberSchema,
1305 ) -> Self {
1306 self.required_property(name, PrimitiveSchema::Number(f(NumberSchema::new())))
1307 }
1308
1309 pub fn optional_number_with(
1311 self,
1312 name: impl Into<String>,
1313 f: impl FnOnce(NumberSchema) -> NumberSchema,
1314 ) -> Self {
1315 self.property(name, PrimitiveSchema::Number(f(NumberSchema::new())))
1316 }
1317
1318 pub fn required_integer(self, name: impl Into<String>, min: i64, max: i64) -> Self {
1322 self.required_property(
1323 name,
1324 PrimitiveSchema::Integer(IntegerSchema::new().range(min, max)),
1325 )
1326 }
1327
1328 pub fn optional_integer(self, name: impl Into<String>, min: i64, max: i64) -> Self {
1330 self.property(
1331 name,
1332 PrimitiveSchema::Integer(IntegerSchema::new().range(min, max)),
1333 )
1334 }
1335
1336 pub fn required_integer_with(
1338 self,
1339 name: impl Into<String>,
1340 f: impl FnOnce(IntegerSchema) -> IntegerSchema,
1341 ) -> Self {
1342 self.required_property(name, PrimitiveSchema::Integer(f(IntegerSchema::new())))
1343 }
1344
1345 pub fn optional_integer_with(
1347 self,
1348 name: impl Into<String>,
1349 f: impl FnOnce(IntegerSchema) -> IntegerSchema,
1350 ) -> Self {
1351 self.property(name, PrimitiveSchema::Integer(f(IntegerSchema::new())))
1352 }
1353
1354 pub fn required_bool(self, name: impl Into<String>) -> Self {
1358 self.required_property(name, PrimitiveSchema::Boolean(BooleanSchema::new()))
1359 }
1360
1361 pub fn optional_bool(self, name: impl Into<String>, default: bool) -> Self {
1363 self.property(
1364 name,
1365 PrimitiveSchema::Boolean(BooleanSchema::new().with_default(default)),
1366 )
1367 }
1368
1369 pub fn required_bool_with(
1371 self,
1372 name: impl Into<String>,
1373 f: impl FnOnce(BooleanSchema) -> BooleanSchema,
1374 ) -> Self {
1375 self.required_property(name, PrimitiveSchema::Boolean(f(BooleanSchema::new())))
1376 }
1377
1378 pub fn optional_bool_with(
1380 self,
1381 name: impl Into<String>,
1382 f: impl FnOnce(BooleanSchema) -> BooleanSchema,
1383 ) -> Self {
1384 self.property(name, PrimitiveSchema::Boolean(f(BooleanSchema::new())))
1385 }
1386
1387 pub fn required_enum_schema(self, name: impl Into<String>, enum_schema: EnumSchema) -> Self {
1391 self.required_property(name, PrimitiveSchema::Enum(enum_schema))
1392 }
1393
1394 pub fn optional_enum_schema(self, name: impl Into<String>, enum_schema: EnumSchema) -> Self {
1396 self.property(name, PrimitiveSchema::Enum(enum_schema))
1397 }
1398
1399 #[deprecated(
1401 since = "0.13.0",
1402 note = "Use ElicitationSchemaBuilder::required_enum_schema with EnumSchema::builder instead"
1403 )]
1404 pub fn required_enum(self, name: impl Into<String>, values: Vec<String>) -> Self {
1405 self.required_property(
1406 name,
1407 PrimitiveSchema::Enum(EnumSchema::Legacy(LegacyEnumSchema {
1408 type_: StringTypeConst,
1409 title: None,
1410 description: None,
1411 enum_: values,
1412 enum_names: None,
1413 })),
1414 )
1415 }
1416
1417 #[deprecated(
1419 since = "0.13.0",
1420 note = "Use ElicitationSchemaBuilder::optional_enum_schema with EnumSchema::builder instead"
1421 )]
1422 pub fn optional_enum(self, name: impl Into<String>, values: Vec<String>) -> Self {
1423 self.property(
1424 name,
1425 PrimitiveSchema::Enum(EnumSchema::Legacy(LegacyEnumSchema {
1426 type_: StringTypeConst,
1427 title: None,
1428 description: None,
1429 enum_: values,
1430 enum_names: None,
1431 })),
1432 )
1433 }
1434
1435 pub fn mark_required(mut self, name: impl Into<String>) -> Self {
1437 self.required.push(name.into());
1438 self
1439 }
1440
1441 pub fn title(mut self, title: impl Into<Cow<'static, str>>) -> Self {
1443 self.title = Some(title.into());
1444 self
1445 }
1446
1447 pub fn description(mut self, description: impl Into<Cow<'static, str>>) -> Self {
1449 self.description = Some(description.into());
1450 self
1451 }
1452
1453 pub fn build(self) -> Result<ElicitationSchema, &'static str> {
1461 if !self.required.is_empty() {
1463 for field_name in &self.required {
1464 if !self.properties.contains_key(field_name) {
1465 return Err("Required field does not exist in properties");
1466 }
1467 }
1468 }
1469
1470 Ok(ElicitationSchema {
1471 type_: ObjectTypeConst,
1472 title: self.title,
1473 properties: self.properties,
1474 required: if self.required.is_empty() {
1475 None
1476 } else {
1477 Some(self.required)
1478 },
1479 description: self.description,
1480 })
1481 }
1482
1483 pub fn build_unchecked(self) -> ElicitationSchema {
1489 self.build().expect("Invalid elicitation schema")
1490 }
1491}
1492
1493#[cfg(test)]
1494mod tests {
1495 use anyhow::anyhow;
1496 use serde_json::json;
1497
1498 use super::*;
1499
1500 #[test]
1501 fn test_string_schema_serialization() {
1502 let schema = StringSchema::email().description("Email address");
1503 let json = serde_json::to_value(&schema).unwrap();
1504
1505 assert_eq!(json["type"], "string");
1506 assert_eq!(json["format"], "email");
1507 assert_eq!(json["description"], "Email address");
1508 }
1509
1510 #[test]
1511 fn test_number_schema_serialization() {
1512 let schema = NumberSchema::new()
1513 .range(0.0, 100.0)
1514 .description("Percentage");
1515 let json = serde_json::to_value(&schema).unwrap();
1516
1517 assert_eq!(json["type"], "number");
1518 assert_eq!(json["minimum"], 0.0);
1519 assert_eq!(json["maximum"], 100.0);
1520 }
1521
1522 #[test]
1523 fn test_integer_schema_serialization() {
1524 let schema = IntegerSchema::new().range(0, 150);
1525 let json = serde_json::to_value(&schema).unwrap();
1526
1527 assert_eq!(json["type"], "integer");
1528 assert_eq!(json["minimum"], 0);
1529 assert_eq!(json["maximum"], 150);
1530 }
1531
1532 #[test]
1533 fn test_boolean_schema_serialization() {
1534 let schema = BooleanSchema::new().with_default(true);
1535 let json = serde_json::to_value(&schema).unwrap();
1536
1537 assert_eq!(json["type"], "boolean");
1538 assert_eq!(json["default"], true);
1539 }
1540
1541 #[test]
1542 fn test_enum_schema_untitled_single_select_serialization() {
1543 let schema = EnumSchema::builder(vec!["US".to_string(), "UK".to_string()])
1544 .description("Country code")
1545 .build();
1546 let json = serde_json::to_value(&schema).unwrap();
1547
1548 assert_eq!(json["type"], "string");
1549 assert_eq!(json["enum"], json!(["US", "UK"]));
1550 assert_eq!(json["description"], "Country code");
1551 }
1552
1553 #[test]
1554 fn test_enum_schema_untitled_multi_select_serialization() -> anyhow::Result<()> {
1555 let schema = EnumSchema::builder(vec!["US".to_string(), "UK".to_string()])
1556 .multiselect()
1557 .min_items(1u64)
1558 .map_err(|e| anyhow!("{e}"))?
1559 .max_items(4u64)
1560 .map_err(|e| anyhow!("{e}"))?
1561 .description("Country code")
1562 .build();
1563 let json = serde_json::to_value(&schema)?;
1564
1565 assert_eq!(json["type"], "array");
1566 assert_eq!(json["minItems"], 1u64);
1567 assert_eq!(json["maxItems"], 4u64);
1568 assert_eq!(json["items"], json!({"type":"string", "enum":["US", "UK"]}));
1569 assert_eq!(json["description"], "Country code");
1570 Ok(())
1571 }
1572
1573 #[test]
1574 fn test_enum_schema_titled_single_select_serialization() -> anyhow::Result<()> {
1575 let schema = EnumSchema::builder(vec!["US".to_string(), "UK".to_string()])
1576 .enum_titles(vec![
1577 "United States".to_string(),
1578 "United Kingdom".to_string(),
1579 ])
1580 .map_err(|e| anyhow!("{e}"))?
1581 .description("Country code")
1582 .build();
1583 let json = serde_json::to_value(&schema)?;
1584
1585 assert_eq!(json["type"], "string");
1586 assert_eq!(
1587 json["oneOf"],
1588 json!([
1589 {"const": "US", "title":"United States"},
1590 {"const": "UK", "title":"United Kingdom"}
1591 ])
1592 );
1593 assert_eq!(json["description"], "Country code");
1594 Ok(())
1595 }
1596
1597 #[test]
1598 fn test_enum_schema_legacy_serialization() -> anyhow::Result<()> {
1599 let schema = EnumSchema::Legacy(LegacyEnumSchema {
1600 type_: StringTypeConst,
1601 title: Some("Legacy Enum".into()),
1602 description: Some("A legacy enum schema".into()),
1603 enum_: vec!["A".to_string(), "B".to_string()],
1604 enum_names: Some(vec!["Option A".to_string(), "Option B".to_string()]),
1605 });
1606 let json = serde_json::to_value(&schema)?;
1607
1608 assert_eq!(json["type"], "string");
1609 assert_eq!(json["title"], "Legacy Enum");
1610 assert_eq!(json["description"], "A legacy enum schema");
1611 assert_eq!(json["enum"], json!(["A", "B"]));
1612 assert_eq!(json["enumNames"], json!(["Option A", "Option B"]));
1613 Ok(())
1614 }
1615
1616 #[test]
1617 fn test_enum_schema_titled_multi_select_serialization() -> anyhow::Result<()> {
1618 let schema = EnumSchema::builder(vec!["US".to_string(), "UK".to_string()])
1619 .enum_titles(vec![
1620 "United States".to_string(),
1621 "United Kingdom".to_string(),
1622 ])
1623 .map_err(|e| anyhow!("{e}"))?
1624 .multiselect()
1625 .min_items(1u64)
1626 .map_err(|e| anyhow!("{e}"))?
1627 .max_items(4u64)
1628 .map_err(|e| anyhow!("{e}"))?
1629 .description("Country code")
1630 .build();
1631 let json = serde_json::to_value(&schema)?;
1632
1633 assert_eq!(json["type"], "array");
1634 assert_eq!(json["minItems"], 1u64);
1635 assert_eq!(json["maxItems"], 4u64);
1636 assert_eq!(
1637 json["items"],
1638 json!({"anyOf":[
1639 {"const":"US","title":"United States"},
1640 {"const":"UK","title":"United Kingdom"}
1641 ]})
1642 );
1643 assert_eq!(json["description"], "Country code");
1644 Ok(())
1645 }
1646
1647 #[test]
1648 fn test_enum_schema_single_select_with_default() -> anyhow::Result<()> {
1649 let schema = EnumSchema::builder(vec![
1650 "red".to_string(),
1651 "green".to_string(),
1652 "blue".to_string(),
1653 ])
1654 .with_default("green")
1655 .map_err(|e| anyhow!("{e}"))?
1656 .description("Favorite color")
1657 .build();
1658
1659 let json = serde_json::to_value(&schema)?;
1660
1661 assert_eq!(json["type"], "string");
1662 assert_eq!(json["enum"], json!(["red", "green", "blue"]));
1663 assert_eq!(json["default"], "green");
1664 assert_eq!(json["description"], "Favorite color");
1665 Ok(())
1666 }
1667
1668 #[test]
1669 fn test_enum_schema_multi_select_with_default() -> anyhow::Result<()> {
1670 let schema = EnumSchema::builder(vec![
1671 "red".to_string(),
1672 "green".to_string(),
1673 "blue".to_string(),
1674 ])
1675 .multiselect()
1676 .with_default(vec!["red".to_string(), "blue".to_string()])
1677 .map_err(|e| anyhow!("{e}"))?
1678 .min_items(1)
1679 .map_err(|e| anyhow!("{e}"))?
1680 .max_items(3)
1681 .map_err(|e| anyhow!("{e}"))?
1682 .build();
1683
1684 let json = serde_json::to_value(&schema)?;
1685
1686 assert_eq!(json["type"], "array");
1687 assert_eq!(json["items"]["enum"], json!(["red", "green", "blue"]));
1688 assert_eq!(json["default"], json!(["red", "blue"]));
1689 assert_eq!(json["minItems"], 1);
1690 assert_eq!(json["maxItems"], 3);
1691 Ok(())
1692 }
1693
1694 #[test]
1695 fn test_enum_schema_transition_clears_defaults() -> anyhow::Result<()> {
1696 let builder = EnumSchema::builder(vec!["A".to_string(), "B".to_string()])
1698 .with_default("A")
1699 .map_err(|e| anyhow!("{e}"))?;
1700
1701 let schema = builder.multiselect().build();
1703 let json = serde_json::to_value(&schema)?;
1704
1705 assert_eq!(json["type"], "array");
1706 assert!(json["default"].is_null());
1707 Ok(())
1708 }
1709
1710 #[test]
1711 fn test_enum_schema_multi_to_single_transition() -> anyhow::Result<()> {
1712 let builder = EnumSchema::builder(vec!["A".to_string(), "B".to_string(), "C".to_string()])
1714 .multiselect()
1715 .with_default(vec!["A".to_string(), "B".to_string()])
1716 .map_err(|e| anyhow!("{e}"))?
1717 .min_items(1)
1718 .map_err(|e| anyhow!("{e}"))?;
1719
1720 let schema = builder.single_select().build();
1722 let json = serde_json::to_value(&schema)?;
1723
1724 assert_eq!(json["type"], "string");
1725 assert!(json["default"].is_null());
1726 assert!(json["minItems"].is_null());
1727 assert!(json["maxItems"].is_null());
1728 Ok(())
1729 }
1730
1731 #[test]
1732 fn test_enum_schema_invalid_single_default() {
1733 let result = EnumSchema::builder(vec!["A".to_string(), "B".to_string()]).with_default("C");
1734
1735 assert!(result.is_err());
1736 assert_eq!(
1737 result.unwrap_err(),
1738 "Provided default value is not in enum values"
1739 );
1740 }
1741
1742 #[test]
1743 fn test_enum_schema_invalid_multi_default() {
1744 let result = EnumSchema::builder(vec!["A".to_string(), "B".to_string()])
1745 .multiselect()
1746 .with_default(vec!["A".to_string(), "C".to_string()]);
1747
1748 assert!(result.is_err());
1749 assert_eq!(
1750 result.unwrap_err(),
1751 "One of the provided default values is not in enum values"
1752 );
1753 }
1754
1755 #[test]
1756 fn test_enum_schema_titled_with_default() -> anyhow::Result<()> {
1757 let schema = EnumSchema::builder(vec!["US".to_string(), "UK".to_string()])
1758 .enum_titles(vec![
1759 "United States".to_string(),
1760 "United Kingdom".to_string(),
1761 ])
1762 .map_err(|e| anyhow!("{e}"))?
1763 .with_default("UK")
1764 .map_err(|e| anyhow!("{e}"))?
1765 .build();
1766
1767 let json = serde_json::to_value(&schema)?;
1768
1769 assert_eq!(json["type"], "string");
1770 assert_eq!(json["default"], "UK");
1771 assert_eq!(
1772 json["oneOf"],
1773 json!([
1774 {"const": "US", "title": "United States"},
1775 {"const": "UK", "title": "United Kingdom"}
1776 ])
1777 );
1778 Ok(())
1779 }
1780
1781 #[test]
1782 fn test_enum_schema_untitled_after_titled() -> anyhow::Result<()> {
1783 let schema = EnumSchema::builder(vec!["A".to_string(), "B".to_string()])
1784 .enum_titles(vec!["Option A".to_string(), "Option B".to_string()])
1785 .map_err(|e| anyhow!("{e}"))?
1786 .untitled()
1787 .build();
1788
1789 let json = serde_json::to_value(&schema)?;
1790
1791 assert_eq!(json["type"], "string");
1792 assert_eq!(json["enum"], json!(["A", "B"]));
1793 assert!(json["oneOf"].is_null());
1794 Ok(())
1795 }
1796
1797 #[test]
1798 fn test_primitive_schema_enum_deserialization() {
1799 let json = json!({
1801 "type": "string",
1802 "enum": ["a", "b"]
1803 });
1804 let schema: PrimitiveSchema = serde_json::from_value(json).unwrap();
1805 assert!(matches!(schema, PrimitiveSchema::Enum(_)));
1806 let json = json!({
1808 "type": "string"
1809 });
1810 let schema: PrimitiveSchema = serde_json::from_value(json).unwrap();
1811 assert!(matches!(schema, PrimitiveSchema::String(_)));
1812 }
1813
1814 #[test]
1815 fn test_elicitation_schema_builder_simple() {
1816 let schema = ElicitationSchema::builder()
1817 .required_email("email")
1818 .optional_bool("newsletter", false)
1819 .build()
1820 .unwrap();
1821
1822 assert_eq!(schema.properties.len(), 2);
1823 assert!(schema.properties.contains_key("email"));
1824 assert!(schema.properties.contains_key("newsletter"));
1825 assert_eq!(schema.required, Some(vec!["email".to_string()]));
1826 }
1827
1828 #[test]
1829 fn test_elicitation_schema_builder_complex() {
1830 let enum_schema =
1831 EnumSchema::builder(vec!["US".to_string(), "UK".to_string(), "CA".to_string()]).build();
1832 let schema = ElicitationSchema::builder()
1833 .required_string_with("name", |s| s.length(1, 100))
1834 .required_integer("age", 0, 150)
1835 .optional_bool("newsletter", false)
1836 .required_enum_schema("country", enum_schema)
1837 .description("User registration")
1838 .build()
1839 .unwrap();
1840
1841 assert_eq!(schema.properties.len(), 4);
1842 assert_eq!(
1843 schema.required,
1844 Some(vec![
1845 "name".to_string(),
1846 "age".to_string(),
1847 "country".to_string()
1848 ])
1849 );
1850 assert_eq!(schema.description.as_deref(), Some("User registration"));
1851 }
1852
1853 #[test]
1854 fn test_elicitation_schema_serialization() {
1855 let schema = ElicitationSchema::builder()
1856 .required_string_with("name", |s| s.length(1, 100))
1857 .build()
1858 .unwrap();
1859
1860 let json = serde_json::to_value(&schema).unwrap();
1861
1862 assert_eq!(json["type"], "object");
1863 assert!(json["properties"]["name"].is_object());
1864 assert_eq!(json["required"], json!(["name"]));
1865 }
1866
1867 #[test]
1868 #[should_panic(expected = "minimum must be <= maximum")]
1869 fn test_integer_range_validation() {
1870 IntegerSchema::new().range(10, 5); }
1872
1873 #[test]
1874 #[should_panic(expected = "min_length must be <= max_length")]
1875 fn test_string_length_validation() {
1876 StringSchema::new().length(10, 5); }
1878
1879 #[test]
1880 fn test_integer_range_validation_with_result() {
1881 let result = IntegerSchema::new().with_range(10, 5);
1882 assert!(result.is_err());
1883 assert_eq!(result.unwrap_err(), "minimum must be <= maximum");
1884 }
1885
1886 #[cfg(feature = "schemars")]
1887 mod schemars_tests {
1888 use anyhow::Result;
1889 use schemars::JsonSchema;
1890 use serde::{Deserialize, Serialize};
1891 use serde_json::json;
1892
1893 use crate::model::ElicitationSchema;
1894
1895 #[derive(Debug, Serialize, Deserialize, JsonSchema, Default)]
1896 #[schemars(inline)]
1897 #[schemars(extend("type" = "string"))]
1898 enum TitledEnum {
1899 #[schemars(title = "Title for the first value")]
1900 #[default]
1901 FirstValue,
1902 #[schemars(title = "Title for the second value")]
1903 SecondValue,
1904 }
1905
1906 #[derive(Debug, Serialize, Deserialize, JsonSchema)]
1907 #[schemars(inline)]
1908 enum UntitledEnum {
1909 First,
1910 Second,
1911 Third,
1912 }
1913
1914 fn default_untitled_multi_select() -> Vec<UntitledEnum> {
1915 vec![UntitledEnum::Second, UntitledEnum::Third]
1916 }
1917
1918 #[derive(Debug, Serialize, Deserialize, JsonSchema)]
1919 #[schemars(description = "User information")]
1920 struct UserInfo {
1921 #[schemars(description = "User's name")]
1922 pub name: String,
1923 pub single_select_untitled: UntitledEnum,
1924 #[schemars(
1925 title = "Single Select Titled",
1926 description = "Description for single select enum",
1927 default
1928 )]
1929 pub single_select_titled: TitledEnum,
1930 #[serde(default = "default_untitled_multi_select")]
1931 pub multi_select_untitled: Vec<UntitledEnum>,
1932 #[schemars(
1933 title = "Multi Select Titled",
1934 description = "Multi Select Description"
1935 )]
1936 pub multi_select_titled: Vec<TitledEnum>,
1937 }
1938
1939 #[test]
1940 fn test_schema_inference_for_enum_fields() -> Result<()> {
1941 let schema = ElicitationSchema::from_type::<UserInfo>()?;
1942
1943 let json = serde_json::to_value(&schema)?;
1944 assert_eq!(json["type"], "object");
1945 assert_eq!(json["description"], "User information");
1946 assert_eq!(
1947 json["required"],
1948 json!(["name", "single_select_untitled", "multi_select_titled"])
1949 );
1950 let properties = match json.get("properties") {
1951 Some(serde_json::Value::Object(map)) => map,
1952 _ => panic!("Schema does not have 'properties' field or it is not object type"),
1953 };
1954
1955 assert_eq!(properties.len(), 5);
1956 assert_eq!(
1957 properties["name"],
1958 json!({"description":"User's name", "type":"string"})
1959 );
1960
1961 assert_eq!(
1962 properties["single_select_untitled"],
1963 serde_json::json!({
1964 "type":"string",
1965 "enum":["First", "Second", "Third"]
1966 })
1967 );
1968
1969 assert_eq!(
1970 properties["single_select_titled"],
1971 json!({
1972 "type":"string",
1973 "title":"Single Select Titled",
1974 "description":"Description for single select enum",
1975 "oneOf":[
1976 {"const":"FirstValue", "title":"Title for the first value"},
1977 {"const":"SecondValue", "title":"Title for the second value"}
1978 ],
1979 "default":"FirstValue"
1980 })
1981 );
1982 assert_eq!(
1983 properties["multi_select_untitled"],
1984 serde_json::json!({
1985 "type":"array",
1986 "items" : {
1987 "type":"string",
1988 "enum":["First", "Second", "Third"]
1989 },
1990 "default":["Second", "Third"]
1991 })
1992 );
1993 assert_eq!(
1994 properties["multi_select_titled"],
1995 serde_json::json!({
1996 "type":"array",
1997 "title":"Multi Select Titled",
1998 "description":"Multi Select Description",
1999 "items":{
2000 "anyOf":[
2001 {"const":"FirstValue", "title":"Title for the first value"},
2002 {"const":"SecondValue", "title":"Title for the second value"}
2003 ]
2004 }
2005 })
2006 );
2007 Ok(())
2008 }
2009 }
2010}