Skip to main content

mcpkit_rs/model/
elicitation_schema.rs

1//! Type-safe schema definitions for MCP elicitation requests.
2//!
3//! This module provides strongly-typed schema definitions for elicitation requests
4//! that comply with the MCP 2025-06-18 specification. Elicitation schemas must be
5//! objects with primitive-typed properties.
6//!
7//! # Example
8//!
9//! ```rust
10//! use mcpkit_rs::model::*;
11//!
12//! let schema = ElicitationSchema::builder()
13//!     .required_email("email")
14//!     .required_integer("age", 0, 150)
15//!     .optional_bool("newsletter", false)
16//!     .build();
17//! ```
18
19use std::{borrow::Cow, collections::BTreeMap, marker::PhantomData};
20
21use serde::{Deserialize, Serialize};
22
23use crate::{const_string, model::ConstString};
24
25// =============================================================================
26// CONST TYPES FOR JSON SCHEMA TYPE FIELD
27// =============================================================================
28
29const_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// =============================================================================
38// PRIMITIVE SCHEMA DEFINITIONS
39// =============================================================================
40
41/// Primitive schema definition for elicitation properties.
42///
43/// According to MCP 2025-06-18 specification, elicitation schemas must have
44/// properties of primitive types only (string, number, integer, boolean, enum).
45///
46/// Note: Put Enum as the first variant to avoid ambiguity during deserialization.
47/// This is due to the fact that EnumSchema can contain StringSchema internally and serde
48/// uses first match wins strategy when deserializing untagged enums.
49#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
50#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
51#[serde(untagged)]
52pub enum PrimitiveSchema {
53    /// Enum property (explicit enum schema)
54    Enum(EnumSchema),
55    /// String property (with optional enum constraint)
56    String(StringSchema),
57    /// Number property (with optional enum constraint)
58    Number(NumberSchema),
59    /// Integer property (with optional enum constraint)
60    Integer(IntegerSchema),
61    /// Boolean property
62    Boolean(BooleanSchema),
63}
64
65// =============================================================================
66// STRING SCHEMA
67// =============================================================================
68
69/// String format types allowed by the MCP specification.
70#[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 address format
75    Email,
76    /// URI format
77    Uri,
78    /// Date format (YYYY-MM-DD)
79    Date,
80    /// Date-time format (ISO 8601)
81    DateTime,
82}
83
84/// Schema definition for string properties.
85///
86/// Compliant with MCP 2025-06-18 specification for elicitation schemas.
87/// Supports only the fields allowed by the MCP spec:
88/// - format limited to: "email", "uri", "date", "date-time"
89#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
90#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
91#[serde(rename_all = "camelCase")]
92pub struct StringSchema {
93    /// Type discriminator
94    #[serde(rename = "type")]
95    pub type_: StringTypeConst,
96
97    /// Optional title for the schema
98    #[serde(skip_serializing_if = "Option::is_none")]
99    pub title: Option<Cow<'static, str>>,
100
101    /// Human-readable description
102    #[serde(skip_serializing_if = "Option::is_none")]
103    pub description: Option<Cow<'static, str>>,
104
105    /// Minimum string length
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub min_length: Option<u32>,
108
109    /// Maximum string length
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub max_length: Option<u32>,
112
113    /// String format - limited to: "email", "uri", "date", "date-time"
114    #[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    /// Create a new string schema
133    pub fn new() -> Self {
134        Self::default()
135    }
136
137    /// Create an email string schema
138    pub fn email() -> Self {
139        Self {
140            format: Some(StringFormat::Email),
141            ..Default::default()
142        }
143    }
144
145    /// Create a URI string schema
146    pub fn uri() -> Self {
147        Self {
148            format: Some(StringFormat::Uri),
149            ..Default::default()
150        }
151    }
152
153    /// Create a date string schema
154    pub fn date() -> Self {
155        Self {
156            format: Some(StringFormat::Date),
157            ..Default::default()
158        }
159    }
160
161    /// Create a date-time string schema
162    pub fn date_time() -> Self {
163        Self {
164            format: Some(StringFormat::DateTime),
165            ..Default::default()
166        }
167    }
168
169    /// Set title
170    pub fn title(mut self, title: impl Into<Cow<'static, str>>) -> Self {
171        self.title = Some(title.into());
172        self
173    }
174
175    /// Set description
176    pub fn description(mut self, description: impl Into<Cow<'static, str>>) -> Self {
177        self.description = Some(description.into());
178        self
179    }
180
181    /// Set minimum and maximum length
182    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    /// Set minimum and maximum length (panics on invalid input)
192    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    /// Set minimum length
200    pub fn min_length(mut self, min: u32) -> Self {
201        self.min_length = Some(min);
202        self
203    }
204
205    /// Set maximum length
206    pub fn max_length(mut self, max: u32) -> Self {
207        self.max_length = Some(max);
208        self
209    }
210
211    /// Set format (limited to: "email", "uri", "date", "date-time")
212    pub fn format(mut self, format: StringFormat) -> Self {
213        self.format = Some(format);
214        self
215    }
216}
217
218// =============================================================================
219// NUMBER SCHEMA
220// =============================================================================
221
222/// Schema definition for number properties (floating-point).
223///
224/// Compliant with MCP 2025-06-18 specification for elicitation schemas.
225/// Supports only the fields allowed by the MCP spec.
226#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
227#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
228#[serde(rename_all = "camelCase")]
229pub struct NumberSchema {
230    /// Type discriminator
231    #[serde(rename = "type")]
232    pub type_: NumberTypeConst,
233
234    /// Optional title for the schema
235    #[serde(skip_serializing_if = "Option::is_none")]
236    pub title: Option<Cow<'static, str>>,
237
238    /// Human-readable description
239    #[serde(skip_serializing_if = "Option::is_none")]
240    pub description: Option<Cow<'static, str>>,
241
242    /// Minimum value (inclusive)
243    #[serde(skip_serializing_if = "Option::is_none")]
244    pub minimum: Option<f64>,
245
246    /// Maximum value (inclusive)
247    #[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    /// Create a new number schema
265    pub fn new() -> Self {
266        Self::default()
267    }
268
269    /// Set minimum and maximum (inclusive)
270    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    /// Set minimum and maximum (panics on invalid input)
280    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    /// Set minimum (inclusive)
288    pub fn minimum(mut self, min: f64) -> Self {
289        self.minimum = Some(min);
290        self
291    }
292
293    /// Set maximum (inclusive)
294    pub fn maximum(mut self, max: f64) -> Self {
295        self.maximum = Some(max);
296        self
297    }
298
299    /// Set title
300    pub fn title(mut self, title: impl Into<Cow<'static, str>>) -> Self {
301        self.title = Some(title.into());
302        self
303    }
304
305    /// Set description
306    pub fn description(mut self, description: impl Into<Cow<'static, str>>) -> Self {
307        self.description = Some(description.into());
308        self
309    }
310}
311
312// =============================================================================
313// INTEGER SCHEMA
314// =============================================================================
315
316/// Schema definition for integer properties.
317///
318/// Compliant with MCP 2025-06-18 specification for elicitation schemas.
319/// Supports only the fields allowed by the MCP spec.
320#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
321#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
322#[serde(rename_all = "camelCase")]
323pub struct IntegerSchema {
324    /// Type discriminator
325    #[serde(rename = "type")]
326    pub type_: IntegerTypeConst,
327
328    /// Optional title for the schema
329    #[serde(skip_serializing_if = "Option::is_none")]
330    pub title: Option<Cow<'static, str>>,
331
332    /// Human-readable description
333    #[serde(skip_serializing_if = "Option::is_none")]
334    pub description: Option<Cow<'static, str>>,
335
336    /// Minimum value (inclusive)
337    #[serde(skip_serializing_if = "Option::is_none")]
338    pub minimum: Option<i64>,
339
340    /// Maximum value (inclusive)
341    #[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    /// Create a new integer schema
359    pub fn new() -> Self {
360        Self::default()
361    }
362
363    /// Set minimum and maximum (inclusive)
364    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    /// Set minimum and maximum (panics on invalid input)
374    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    /// Set minimum (inclusive)
382    pub fn minimum(mut self, min: i64) -> Self {
383        self.minimum = Some(min);
384        self
385    }
386
387    /// Set maximum (inclusive)
388    pub fn maximum(mut self, max: i64) -> Self {
389        self.maximum = Some(max);
390        self
391    }
392
393    /// Set title
394    pub fn title(mut self, title: impl Into<Cow<'static, str>>) -> Self {
395        self.title = Some(title.into());
396        self
397    }
398
399    /// Set description
400    pub fn description(mut self, description: impl Into<Cow<'static, str>>) -> Self {
401        self.description = Some(description.into());
402        self
403    }
404}
405
406// =============================================================================
407// BOOLEAN SCHEMA
408// =============================================================================
409
410/// Schema definition for boolean properties.
411#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
412#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
413#[serde(rename_all = "camelCase")]
414pub struct BooleanSchema {
415    /// Type discriminator
416    #[serde(rename = "type")]
417    pub type_: BooleanTypeConst,
418
419    /// Optional title for the schema
420    #[serde(skip_serializing_if = "Option::is_none")]
421    pub title: Option<Cow<'static, str>>,
422
423    /// Human-readable description
424    #[serde(skip_serializing_if = "Option::is_none")]
425    pub description: Option<Cow<'static, str>>,
426
427    /// Default value
428    #[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    /// Create a new boolean schema
445    pub fn new() -> Self {
446        Self::default()
447    }
448
449    /// Set title
450    pub fn title(mut self, title: impl Into<Cow<'static, str>>) -> Self {
451        self.title = Some(title.into());
452        self
453    }
454
455    /// Set description
456    pub fn description(mut self, description: impl Into<Cow<'static, str>>) -> Self {
457        self.description = Some(description.into());
458        self
459    }
460
461    /// Set default value
462    pub fn with_default(mut self, default: bool) -> Self {
463        self.default = Some(default);
464        self
465    }
466}
467
468// =============================================================================
469// ENUM SCHEMA
470// =============================================================================
471
472/// Schema definition for enum properties.
473///
474/// Represent single entry for titled item
475#[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/// Legacy enum schema, keep for backward compatibility
484#[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/// Untitled single-select
500#[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/// Titled single-select
517#[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/// Combined single-select
534#[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/// Items for untitled multi-select options
543#[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/// Items for titled multi-select options
553#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
554#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
555pub struct TitledItems {
556    // MCP spec requires "anyOf" for multi-select enums (allows any combination)
557    // Alias "oneOf" for compatibility with schemars
558    #[serde(rename = "anyOf", alias = "oneOf")]
559    pub any_of: Vec<ConstTitle>,
560}
561
562/// Multi-select untitled options
563#[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/// Multi-select titled options
583#[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/// Multi-select enum options
603#[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/// Compliant with MCP 2025-06-18 specification for elicitation schemas.
612/// Enums must have string type for values and can optionally include human-readable names.
613///
614/// # Example
615///
616/// ```rust
617/// use mcpkit_rs::model::*;
618///
619/// let enum_schema = EnumSchema::builder(vec!["US".to_string(), "UK".to_string()])
620///    .multiselect()
621///    .min_items(1u64).expect("Min items should be correct value")
622///    .max_items(4u64).expect("Max items should be correct value")
623///    .description("Country code")
624///    .build();
625/// ```
626#[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/// Marker type for single-select enum builder
636#[derive(Debug)]
637pub struct SingleSelect;
638/// Marker type for multi-select enum builder
639#[derive(Debug)]
640pub struct MultiSelect;
641/// Builder for EnumSchema
642/// Allows to create various enum schema types (single/multi select, titled/untitled)
643/// with validation of provided parameters
644///
645/// # Example
646/// ```rust
647/// use mcpkit_rs::model::*;
648/// let enum_schema = EnumSchema::builder(vec!["Red".to_string(), "Green".to_string(), "Blue".to_string()])
649///  .multiselect()
650///  .enum_titles(vec!["Red Color".to_string(), "Green Color".to_string(), "Blue Color".to_string()])
651///  .expect("Number of titles should match number of values")
652///  .min_items(1u64).expect("Min items should be correct value")
653///  .max_items(3u64).expect("Max items should be correct value")
654///  .description("Select your favorite colors")
655///  .build();
656/// ```
657#[derive(Debug)]
658pub struct EnumSchemaBuilder<T> {
659    /// Enum values
660    enum_values: Vec<String>,
661    /// If true generate Titled EnumSchema, UnTitled otherwise
662    titled: bool,
663    /// Title of EnumSchema
664    title: Option<Cow<'static, str>>,
665    /// Description of EnumSchema
666    description: Option<Cow<'static, str>>,
667    /// Titles of given enum values
668    enum_titles: Vec<String>,
669    /// Minimum number of items to choose for MultiSelect
670    min_items: Option<u64>,
671    /// Maximum number of items to choose for MultiSelect
672    max_items: Option<u64>,
673    /// Default values for enum
674    default: Vec<String>,
675    select_type: PhantomData<T>,
676}
677
678/// Default implementation for single-select enum builder
679impl 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
695/// Common enum schema builder methods
696impl<T> EnumSchemaBuilder<T> {
697    /// Set title of enum schema
698    pub fn title(mut self, value: impl Into<Cow<'static, str>>) -> Self {
699        self.title = Some(value.into());
700        self
701    }
702
703    /// Set description of enum schema
704    pub fn description(mut self, value: impl Into<Cow<'static, str>>) -> Self {
705        self.description = Some(value.into());
706        self
707    }
708
709    /// Set enum as untitled
710    /// Clears any previously set titles
711    pub fn untitled(mut self) -> Self {
712        self.enum_titles = Vec::new();
713        self.titled = false;
714        self
715    }
716
717    /// Set titles to enum values. Also, implicitly set this enum schema as titled
718    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
732/// Enum selection builder for single-select enums
733impl EnumSchemaBuilder<SingleSelect> {
734    pub fn new(values: Vec<String>) -> EnumSchemaBuilder<SingleSelect> {
735        EnumSchemaBuilder {
736            enum_values: values,
737            ..Default::default()
738        }
739    }
740
741    /// Transition to multi-select enum builder.
742    ///
743    /// Clears any previously set default values and resets min/max items.
744    /// After this transition, you can use `min_items()`, `max_items()`, and
745    /// `with_default()` for multi-select semantics.
746    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(), // Clear default for multi-select
756            select_type: PhantomData,
757        }
758    }
759
760    /// Set default value
761    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    /// Build enum schema
774    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
803/// Enum selection builder for multi-select enums
804impl EnumSchemaBuilder<MultiSelect> {
805    /// Set enum as single-select
806    /// If it was multi-select, clear default values
807    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(), // Clear default for single-select
817            select_type: PhantomData,
818        }
819    }
820
821    /// Set default values
822    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    /// Set minimal number of items for multi-select enum options
848    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    /// Set maximal number of items for multi-select enum options
859    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    /// Build enum schema
870    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    /// Creates a new `EnumSchemaBuilder` with the given enum values.
916    ///
917    /// This convenience method allows you to construct an enum schema by specifying
918    /// the possible string values for the enum. Use the returned builder to further
919    /// configure the schema before building it.
920    ///
921    /// # Arguments
922    ///
923    /// * `values` - A vector of strings representing the allowed enum values.
924    ///
925    /// # Example
926    ///
927    /// ```
928    /// use mcpkit_rs::model::*;
929    ///
930    /// let enum_schema = EnumSchema::builder(vec!["A".to_string(), "B".to_string()]).
931    ///     with_default("A").
932    ///     expect("Default value should be valid").
933    ///     enum_titles(vec!["Option A".to_string(), "Option B".to_string()]).
934    ///     expect("Number of titles should match number of values").
935    ///     build();
936    /// ```
937    pub fn builder(values: Vec<String>) -> EnumSchemaBuilder<SingleSelect> {
938        EnumSchemaBuilder::new(values)
939    }
940}
941
942// =============================================================================
943// ELICITATION SCHEMA
944// =============================================================================
945
946/// Type-safe elicitation schema for requesting structured user input.
947///
948/// This enforces the MCP 2025-06-18 specification that elicitation schemas
949/// must be objects with primitive-typed properties.
950///
951/// # Example
952///
953/// ```rust
954/// use mcpkit_rs::model::*;
955///
956/// let schema = ElicitationSchema::builder()
957///     .required_email("email")
958///     .required_integer("age", 0, 150)
959///     .optional_bool("newsletter", false)
960///     .build();
961/// ```
962#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
963#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
964#[serde(rename_all = "camelCase")]
965pub struct ElicitationSchema {
966    /// Always "object" for elicitation schemas
967    #[serde(rename = "type")]
968    pub type_: ObjectTypeConst,
969
970    /// Optional title for the schema
971    #[serde(skip_serializing_if = "Option::is_none")]
972    pub title: Option<Cow<'static, str>>,
973
974    /// Property definitions (must be primitive types)
975    pub properties: BTreeMap<String, PrimitiveSchema>,
976
977    /// List of required property names
978    #[serde(skip_serializing_if = "Option::is_none")]
979    pub required: Option<Vec<String>>,
980
981    /// Optional description of what this schema represents
982    #[serde(skip_serializing_if = "Option::is_none")]
983    pub description: Option<Cow<'static, str>>,
984}
985
986impl ElicitationSchema {
987    /// Create a new elicitation schema with the given properties
988    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    /// Convert from a JSON Schema object (typically generated by schemars)
999    ///
1000    /// This allows converting from JsonObject to ElicitationSchema, which is useful
1001    /// when working with automatically generated schemas from types.
1002    ///
1003    /// # Example
1004    ///
1005    /// ```rust,ignore
1006    /// use mcpkit_rs::model::*;
1007    ///
1008    /// let json_schema = schema_for_type::<MyType>();
1009    /// let elicitation_schema = ElicitationSchema::from_json_schema(json_schema)?;
1010    /// ```
1011    ///
1012    /// # Errors
1013    ///
1014    /// Returns a [`serde_json::Error`] if the JSON object cannot be deserialized
1015    /// into a valid ElicitationSchema.
1016    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    /// Generate an ElicitationSchema from a Rust type that implements JsonSchema
1021    ///
1022    /// This is a convenience method that combines schema generation and conversion.
1023    /// It uses the same schema generation settings as the rest of the MCP SDK.
1024    ///
1025    /// # Example
1026    ///
1027    /// ```rust,ignore
1028    /// use mcpkit_rs::model::*;
1029    /// use schemars::JsonSchema;
1030    /// use serde::{Deserialize, Serialize};
1031    ///
1032    /// #[derive(JsonSchema, Serialize, Deserialize)]
1033    /// struct UserInput {
1034    ///     name: String,
1035    ///     age: u32,
1036    /// }
1037    ///
1038    /// let schema = ElicitationSchema::from_type::<UserInput>()?;
1039    /// ```
1040    ///
1041    /// # Errors
1042    ///
1043    /// Returns a [`serde_json::Error`] if the generated schema cannot be converted
1044    /// to a valid ElicitationSchema.
1045    #[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    /// Set the required fields
1067    pub fn with_required(mut self, required: Vec<String>) -> Self {
1068        self.required = Some(required);
1069        self
1070    }
1071
1072    /// Set the title
1073    pub fn with_title(mut self, title: impl Into<Cow<'static, str>>) -> Self {
1074        self.title = Some(title.into());
1075        self
1076    }
1077
1078    /// Set the description
1079    pub fn with_description(mut self, description: impl Into<Cow<'static, str>>) -> Self {
1080        self.description = Some(description.into());
1081        self
1082    }
1083
1084    /// Create a builder for constructing elicitation schemas fluently
1085    pub fn builder() -> ElicitationSchemaBuilder {
1086        ElicitationSchemaBuilder::new()
1087    }
1088}
1089
1090// =============================================================================
1091// BUILDER
1092// =============================================================================
1093
1094/// Fluent builder for constructing elicitation schemas.
1095///
1096/// # Example
1097///
1098/// ```rust
1099/// use mcpkit_rs::model::*;
1100///
1101/// let schema = ElicitationSchema::builder()
1102///     .required_email("email")
1103///     .required_integer("age", 0, 150)
1104///     .optional_bool("newsletter", false)
1105///     .description("User registration")
1106///     .build();
1107/// ```
1108#[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    /// Create a new builder
1118    pub fn new() -> Self {
1119        Self::default()
1120    }
1121
1122    /// Add a property to the schema
1123    pub fn property(mut self, name: impl Into<String>, schema: PrimitiveSchema) -> Self {
1124        self.properties.insert(name.into(), schema);
1125        self
1126    }
1127
1128    /// Add a required property to the schema
1129    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    // ===========================================================================
1137    // TYPED PROPERTY METHODS - Cleaner API without PrimitiveSchema wrapper
1138    // ===========================================================================
1139
1140    /// Add a string property with custom builder (required)
1141    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    /// Add a required string property with custom builder
1152    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    /// Add a number property with custom builder
1165    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    /// Add a required number property with custom builder
1176    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    /// Add an integer property with custom builder
1189    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    /// Add a required integer property with custom builder
1202    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    /// Add a boolean property with custom builder
1215    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    /// Add a required boolean property with custom builder
1228    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    // ===========================================================================
1241    // CONVENIENCE METHODS - Simple common cases
1242    // ===========================================================================
1243
1244    /// Add a required string property
1245    pub fn required_string(self, name: impl Into<String>) -> Self {
1246        self.required_property(name, PrimitiveSchema::String(StringSchema::new()))
1247    }
1248
1249    /// Add an optional string property
1250    pub fn optional_string(self, name: impl Into<String>) -> Self {
1251        self.property(name, PrimitiveSchema::String(StringSchema::new()))
1252    }
1253
1254    /// Add a required email property
1255    pub fn required_email(self, name: impl Into<String>) -> Self {
1256        self.required_property(name, PrimitiveSchema::String(StringSchema::email()))
1257    }
1258
1259    /// Add an optional email property
1260    pub fn optional_email(self, name: impl Into<String>) -> Self {
1261        self.property(name, PrimitiveSchema::String(StringSchema::email()))
1262    }
1263
1264    /// Add a required string property with custom builder
1265    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    /// Add an optional string property with custom builder
1274    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    // Convenience methods for numbers
1283
1284    /// Add a required number property with range
1285    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    /// Add an optional number property with range
1293    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    /// Add a required number property with custom builder
1301    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    /// Add an optional number property with custom builder
1310    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    // Convenience methods for integers
1319
1320    /// Add a required integer property with range
1321    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    /// Add an optional integer property with range
1329    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    /// Add a required integer property with custom builder
1337    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    /// Add an optional integer property with custom builder
1346    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    // Convenience methods for booleans
1355
1356    /// Add a required boolean property
1357    pub fn required_bool(self, name: impl Into<String>) -> Self {
1358        self.required_property(name, PrimitiveSchema::Boolean(BooleanSchema::new()))
1359    }
1360
1361    /// Add an optional boolean property with default value
1362    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    /// Add a required boolean property with custom builder
1370    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    /// Add an optional boolean property with custom builder
1379    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    // Enum convenience methods
1388
1389    /// Add a required enum property using EnumSchema
1390    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    /// Add an optional enum property using EnumSchema
1395    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    /// Add a required enum property using values. Creates an untitled single-select enum.
1400    #[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    /// Add an optional enum property using values. Creates an untitled single-select enum.
1418    #[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    /// Mark an existing property as required
1436    pub fn mark_required(mut self, name: impl Into<String>) -> Self {
1437        self.required.push(name.into());
1438        self
1439    }
1440
1441    /// Set the schema title
1442    pub fn title(mut self, title: impl Into<Cow<'static, str>>) -> Self {
1443        self.title = Some(title.into());
1444        self
1445    }
1446
1447    /// Set the schema description
1448    pub fn description(mut self, description: impl Into<Cow<'static, str>>) -> Self {
1449        self.description = Some(description.into());
1450        self
1451    }
1452
1453    /// Build the elicitation schema with validation
1454    ///
1455    /// # Errors
1456    ///
1457    /// Returns an error if:
1458    /// - Required fields reference non-existent properties
1459    /// - No properties are defined (empty schema)
1460    pub fn build(self) -> Result<ElicitationSchema, &'static str> {
1461        // Validate that all required fields exist in properties
1462        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    /// Build the elicitation schema without validation (panics on invalid schema)
1484    ///
1485    /// # Panics
1486    ///
1487    /// Panics if required fields reference non-existent properties
1488    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        // Start with single-select with default
1697        let builder = EnumSchema::builder(vec!["A".to_string(), "B".to_string()])
1698            .with_default("A")
1699            .map_err(|e| anyhow!("{e}"))?;
1700
1701        // Transition to multi-select should clear the default
1702        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        // Start with multi-select with defaults
1713        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        // Transition back to single-select should clear defaults and min/max items
1721        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        // Test that enum schemas deserialize as Enum variant, not String
1800        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        // Test that string schemas deserialize as String variant
1807        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); // Should panic
1871    }
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); // Should panic
1877    }
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}