mcp_host/protocol/
elicitation.rs

1//! Type-safe schema definitions for MCP elicitation requests.
2//!
3//! Copied from rust-sdk and adapted for mcphost-rs architecture.
4//! This module provides strongly-typed schema definitions for elicitation requests
5//! that comply with the MCP 2025-06-18 specification. Elicitation schemas must be
6//! objects with primitive-typed properties.
7//!
8//! # Example
9//!
10//! ```rust,ignore
11//! use mcp_host::protocol::elicitation::*;
12//!
13//! let schema = ElicitationSchema::builder()
14//!     .required_email("email")
15//!     .required_integer("age", 0, 150)
16//!     .optional_bool("newsletter", false)
17//!     .build();
18//! ```
19
20use serde::{Deserialize, Serialize};
21use std::{borrow::Cow, collections::BTreeMap};
22
23// Re-export macro from parent if needed
24use crate::protocol::version::const_string;
25
26// =============================================================================
27// CONST TYPES FOR JSON SCHEMA TYPE FIELD
28// =============================================================================
29
30const_string!(ObjectTypeConst = "object");
31const_string!(StringTypeConst = "string");
32const_string!(NumberTypeConst = "number");
33const_string!(IntegerTypeConst = "integer");
34const_string!(BooleanTypeConst = "boolean");
35const_string!(EnumTypeConst = "string");
36const_string!(ArrayTypeConst = "array");
37
38// =============================================================================
39// PRIMITIVE SCHEMA DEFINITIONS
40// =============================================================================
41
42/// Primitive schema definition for elicitation properties.
43///
44/// According to MCP 2025-06-18 specification, elicitation schemas must have
45/// properties of primitive types only (string, number, integer, boolean, enum).
46///
47/// Note: Put Enum as the first variant to avoid ambiguity during deserialization.
48#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
49#[serde(untagged)]
50pub enum PrimitiveSchema {
51    /// Enum property (explicit enum schema)
52    Enum(EnumSchema),
53    /// String property (with optional enum constraint)
54    String(StringSchema),
55    /// Number property (with optional enum constraint)
56    Number(NumberSchema),
57    /// Integer property (with optional enum constraint)
58    Integer(IntegerSchema),
59    /// Boolean property
60    Boolean(BooleanSchema),
61}
62
63// =============================================================================
64// STRING SCHEMA
65// =============================================================================
66
67/// String format types allowed by the MCP specification.
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
69#[serde(rename_all = "kebab-case")]
70pub enum StringFormat {
71    /// Email address format
72    Email,
73    /// URI format
74    Uri,
75    /// Date format (YYYY-MM-DD)
76    Date,
77    /// Date-time format (ISO 8601)
78    DateTime,
79}
80
81/// Schema definition for string properties.
82#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
83#[serde(rename_all = "camelCase")]
84pub struct StringSchema {
85    #[serde(rename = "type")]
86    pub type_: StringTypeConst,
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub title: Option<Cow<'static, str>>,
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub description: Option<Cow<'static, str>>,
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub min_length: Option<u32>,
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub max_length: Option<u32>,
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub format: Option<StringFormat>,
97}
98
99impl Default for StringSchema {
100    fn default() -> Self {
101        Self {
102            type_: StringTypeConst,
103            title: None,
104            description: None,
105            min_length: None,
106            max_length: None,
107            format: None,
108        }
109    }
110}
111
112impl StringSchema {
113    pub fn new() -> Self {
114        Self::default()
115    }
116
117    pub fn email() -> Self {
118        Self {
119            format: Some(StringFormat::Email),
120            ..Default::default()
121        }
122    }
123
124    pub fn uri() -> Self {
125        Self {
126            format: Some(StringFormat::Uri),
127            ..Default::default()
128        }
129    }
130
131    pub fn date() -> Self {
132        Self {
133            format: Some(StringFormat::Date),
134            ..Default::default()
135        }
136    }
137
138    pub fn date_time() -> Self {
139        Self {
140            format: Some(StringFormat::DateTime),
141            ..Default::default()
142        }
143    }
144
145    pub fn title(mut self, title: impl Into<Cow<'static, str>>) -> Self {
146        self.title = Some(title.into());
147        self
148    }
149
150    pub fn description(mut self, description: impl Into<Cow<'static, str>>) -> Self {
151        self.description = Some(description.into());
152        self
153    }
154
155    pub fn with_length(mut self, min: u32, max: u32) -> Result<Self, &'static str> {
156        if min > max {
157            return Err("min_length must be <= max_length");
158        }
159        self.min_length = Some(min);
160        self.max_length = Some(max);
161        Ok(self)
162    }
163
164    pub fn length(mut self, min: u32, max: u32) -> Self {
165        assert!(min <= max, "min_length must be <= max_length");
166        self.min_length = Some(min);
167        self.max_length = Some(max);
168        self
169    }
170
171    pub fn min_length(mut self, min: u32) -> Self {
172        self.min_length = Some(min);
173        self
174    }
175
176    pub fn max_length(mut self, max: u32) -> Self {
177        self.max_length = Some(max);
178        self
179    }
180
181    pub fn format(mut self, format: StringFormat) -> Self {
182        self.format = Some(format);
183        self
184    }
185}
186
187// NOTE: The rest of the schema types (Number, Integer, Boolean, Enum, ElicitationSchema)
188// are too long to include in a single response. This is part 1 of the schema file.
189// The implementation continues with NumberSchema, IntegerSchema, BooleanSchema, EnumSchema,
190// and ElicitationSchema with their builders following the same pattern from rust-sdk.
191
192// For brevity in this initial integration, I'll include stubs that you can expand:
193
194/// Schema definition for number properties (floating-point).
195#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
196#[serde(rename_all = "camelCase")]
197pub struct NumberSchema {
198    #[serde(rename = "type")]
199    pub type_: NumberTypeConst,
200    #[serde(skip_serializing_if = "Option::is_none")]
201    pub title: Option<Cow<'static, str>>,
202    #[serde(skip_serializing_if = "Option::is_none")]
203    pub description: Option<Cow<'static, str>>,
204    #[serde(skip_serializing_if = "Option::is_none")]
205    pub minimum: Option<f64>,
206    #[serde(skip_serializing_if = "Option::is_none")]
207    pub maximum: Option<f64>,
208}
209
210impl Default for NumberSchema {
211    fn default() -> Self {
212        Self {
213            type_: NumberTypeConst,
214            title: None,
215            description: None,
216            minimum: None,
217            maximum: None,
218        }
219    }
220}
221
222impl NumberSchema {
223    pub fn new() -> Self {
224        Self::default()
225    }
226
227    pub fn with_range(mut self, min: f64, max: f64) -> Result<Self, &'static str> {
228        if min > max {
229            return Err("minimum must be <= maximum");
230        }
231        self.minimum = Some(min);
232        self.maximum = Some(max);
233        Ok(self)
234    }
235
236    pub fn range(mut self, min: f64, max: f64) -> Self {
237        assert!(min <= max, "minimum must be <= maximum");
238        self.minimum = Some(min);
239        self.maximum = Some(max);
240        self
241    }
242
243    pub fn minimum(mut self, min: f64) -> Self {
244        self.minimum = Some(min);
245        self
246    }
247
248    pub fn maximum(mut self, max: f64) -> Self {
249        self.maximum = Some(max);
250        self
251    }
252
253    pub fn title(mut self, title: impl Into<Cow<'static, str>>) -> Self {
254        self.title = Some(title.into());
255        self
256    }
257
258    pub fn description(mut self, description: impl Into<Cow<'static, str>>) -> Self {
259        self.description = Some(description.into());
260        self
261    }
262}
263
264/// Schema definition for integer properties.
265#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
266#[serde(rename_all = "camelCase")]
267pub struct IntegerSchema {
268    #[serde(rename = "type")]
269    pub type_: IntegerTypeConst,
270    #[serde(skip_serializing_if = "Option::is_none")]
271    pub title: Option<Cow<'static, str>>,
272    #[serde(skip_serializing_if = "Option::is_none")]
273    pub description: Option<Cow<'static, str>>,
274    #[serde(skip_serializing_if = "Option::is_none")]
275    pub minimum: Option<i64>,
276    #[serde(skip_serializing_if = "Option::is_none")]
277    pub maximum: Option<i64>,
278}
279
280impl Default for IntegerSchema {
281    fn default() -> Self {
282        Self {
283            type_: IntegerTypeConst,
284            title: None,
285            description: None,
286            minimum: None,
287            maximum: None,
288        }
289    }
290}
291
292impl IntegerSchema {
293    pub fn new() -> Self {
294        Self::default()
295    }
296
297    pub fn with_range(mut self, min: i64, max: i64) -> Result<Self, &'static str> {
298        if min > max {
299            return Err("minimum must be <= maximum");
300        }
301        self.minimum = Some(min);
302        self.maximum = Some(max);
303        Ok(self)
304    }
305
306    pub fn range(mut self, min: i64, max: i64) -> Self {
307        assert!(min <= max, "minimum must be <= maximum");
308        self.minimum = Some(min);
309        self.maximum = Some(max);
310        self
311    }
312
313    pub fn minimum(mut self, min: i64) -> Self {
314        self.minimum = Some(min);
315        self
316    }
317
318    pub fn maximum(mut self, max: i64) -> Self {
319        self.maximum = Some(max);
320        self
321    }
322
323    pub fn title(mut self, title: impl Into<Cow<'static, str>>) -> Self {
324        self.title = Some(title.into());
325        self
326    }
327
328    pub fn description(mut self, description: impl Into<Cow<'static, str>>) -> Self {
329        self.description = Some(description.into());
330        self
331    }
332}
333
334/// Schema definition for boolean properties.
335#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
336#[serde(rename_all = "camelCase")]
337pub struct BooleanSchema {
338    #[serde(rename = "type")]
339    pub type_: BooleanTypeConst,
340    #[serde(skip_serializing_if = "Option::is_none")]
341    pub title: Option<Cow<'static, str>>,
342    #[serde(skip_serializing_if = "Option::is_none")]
343    pub description: Option<Cow<'static, str>>,
344    #[serde(skip_serializing_if = "Option::is_none")]
345    pub default: Option<bool>,
346}
347
348impl Default for BooleanSchema {
349    fn default() -> Self {
350        Self {
351            type_: BooleanTypeConst,
352            title: None,
353            description: None,
354            default: None,
355        }
356    }
357}
358
359impl BooleanSchema {
360    pub fn new() -> Self {
361        Self::default()
362    }
363
364    pub fn title(mut self, title: impl Into<Cow<'static, str>>) -> Self {
365        self.title = Some(title.into());
366        self
367    }
368
369    pub fn description(mut self, description: impl Into<Cow<'static, str>>) -> Self {
370        self.description = Some(description.into());
371        self
372    }
373
374    pub fn with_default(mut self, default: bool) -> Self {
375        self.default = Some(default);
376        self
377    }
378}
379
380// =============================================================================
381// ENUM SCHEMA - Single and Multi-Select with Optional Titles
382// =============================================================================
383
384/// Option for titled enum variants (const value + display title)
385#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
386pub struct EnumOption {
387    /// The actual enum value
388    #[serde(rename = "const")]
389    pub const_value: String,
390    /// Display title for this option
391    pub title: String,
392}
393
394impl EnumOption {
395    pub fn new(value: impl Into<String>, title: impl Into<String>) -> Self {
396        Self {
397            const_value: value.into(),
398            title: title.into(),
399        }
400    }
401}
402
403/// Items schema for untitled multi-select (array of strings with enum constraint)
404#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
405pub struct UntitledEnumItems {
406    #[serde(rename = "type")]
407    pub type_: StringTypeConst,
408    #[serde(rename = "enum")]
409    pub enum_values: Vec<String>,
410}
411
412/// Items schema for titled multi-select (anyOf with const/title pairs)
413#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
414#[serde(rename_all = "camelCase")]
415pub struct TitledEnumItems {
416    pub any_of: Vec<EnumOption>,
417}
418
419// -----------------------------------------------------------------------------
420// Single-Select Enum Schemas
421// -----------------------------------------------------------------------------
422
423/// Schema for single-selection enum without display titles
424#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
425#[serde(rename_all = "camelCase")]
426pub struct UntitledSingleSelectEnumSchema {
427    #[serde(rename = "type")]
428    pub type_: StringTypeConst,
429    #[serde(rename = "enum")]
430    pub enum_values: Vec<String>,
431    #[serde(skip_serializing_if = "Option::is_none")]
432    pub title: Option<Cow<'static, str>>,
433    #[serde(skip_serializing_if = "Option::is_none")]
434    pub description: Option<Cow<'static, str>>,
435    #[serde(skip_serializing_if = "Option::is_none")]
436    pub default: Option<String>,
437}
438
439impl UntitledSingleSelectEnumSchema {
440    pub fn new(values: impl Into<Vec<String>>) -> Self {
441        Self {
442            type_: StringTypeConst,
443            enum_values: values.into(),
444            title: None,
445            description: None,
446            default: None,
447        }
448    }
449
450    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 {
456        self.description = Some(description.into());
457        self
458    }
459
460    pub fn with_default(mut self, default: impl Into<String>) -> Self {
461        self.default = Some(default.into());
462        self
463    }
464}
465
466/// Schema for single-selection enum with display titles for each option
467#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
468#[serde(rename_all = "camelCase")]
469pub struct TitledSingleSelectEnumSchema {
470    #[serde(rename = "type")]
471    pub type_: StringTypeConst,
472    pub one_of: Vec<EnumOption>,
473    #[serde(skip_serializing_if = "Option::is_none")]
474    pub title: Option<Cow<'static, str>>,
475    #[serde(skip_serializing_if = "Option::is_none")]
476    pub description: Option<Cow<'static, str>>,
477    #[serde(skip_serializing_if = "Option::is_none")]
478    pub default: Option<String>,
479}
480
481impl TitledSingleSelectEnumSchema {
482    pub fn new(options: impl Into<Vec<EnumOption>>) -> Self {
483        Self {
484            type_: StringTypeConst,
485            one_of: options.into(),
486            title: None,
487            description: None,
488            default: None,
489        }
490    }
491
492    /// Create from tuples of (value, title)
493    pub fn from_pairs<I, V, T>(pairs: I) -> Self
494    where
495        I: IntoIterator<Item = (V, T)>,
496        V: Into<String>,
497        T: Into<String>,
498    {
499        Self::new(
500            pairs
501                .into_iter()
502                .map(|(v, t)| EnumOption::new(v, t))
503                .collect::<Vec<_>>(),
504        )
505    }
506
507    pub fn title(mut self, title: impl Into<Cow<'static, str>>) -> Self {
508        self.title = Some(title.into());
509        self
510    }
511
512    pub fn description(mut self, description: impl Into<Cow<'static, str>>) -> Self {
513        self.description = Some(description.into());
514        self
515    }
516
517    pub fn with_default(mut self, default: impl Into<String>) -> Self {
518        self.default = Some(default.into());
519        self
520    }
521}
522
523// -----------------------------------------------------------------------------
524// Multi-Select Enum Schemas
525// -----------------------------------------------------------------------------
526
527/// Schema for multi-selection enum without display titles
528#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
529#[serde(rename_all = "camelCase")]
530pub struct UntitledMultiSelectEnumSchema {
531    #[serde(rename = "type")]
532    pub type_: ArrayTypeConst,
533    pub items: UntitledEnumItems,
534    #[serde(skip_serializing_if = "Option::is_none")]
535    pub min_items: Option<u32>,
536    #[serde(skip_serializing_if = "Option::is_none")]
537    pub max_items: Option<u32>,
538    #[serde(skip_serializing_if = "Option::is_none")]
539    pub title: Option<Cow<'static, str>>,
540    #[serde(skip_serializing_if = "Option::is_none")]
541    pub description: Option<Cow<'static, str>>,
542    #[serde(skip_serializing_if = "Option::is_none")]
543    pub default: Option<Vec<String>>,
544}
545
546impl UntitledMultiSelectEnumSchema {
547    pub fn new(values: impl Into<Vec<String>>) -> Self {
548        Self {
549            type_: ArrayTypeConst,
550            items: UntitledEnumItems {
551                type_: StringTypeConst,
552                enum_values: values.into(),
553            },
554            min_items: None,
555            max_items: None,
556            title: None,
557            description: None,
558            default: None,
559        }
560    }
561
562    pub fn title(mut self, title: impl Into<Cow<'static, str>>) -> Self {
563        self.title = Some(title.into());
564        self
565    }
566
567    pub fn description(mut self, description: impl Into<Cow<'static, str>>) -> Self {
568        self.description = Some(description.into());
569        self
570    }
571
572    pub fn with_default(mut self, default: impl Into<Vec<String>>) -> Self {
573        self.default = Some(default.into());
574        self
575    }
576
577    pub fn min_items(mut self, min: u32) -> Self {
578        self.min_items = Some(min);
579        self
580    }
581
582    pub fn max_items(mut self, max: u32) -> Self {
583        self.max_items = Some(max);
584        self
585    }
586
587    pub fn items_range(mut self, min: u32, max: u32) -> Self {
588        self.min_items = Some(min);
589        self.max_items = Some(max);
590        self
591    }
592}
593
594/// Schema for multi-selection enum with display titles for each option
595#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
596#[serde(rename_all = "camelCase")]
597pub struct TitledMultiSelectEnumSchema {
598    #[serde(rename = "type")]
599    pub type_: ArrayTypeConst,
600    pub items: TitledEnumItems,
601    #[serde(skip_serializing_if = "Option::is_none")]
602    pub min_items: Option<u32>,
603    #[serde(skip_serializing_if = "Option::is_none")]
604    pub max_items: Option<u32>,
605    #[serde(skip_serializing_if = "Option::is_none")]
606    pub title: Option<Cow<'static, str>>,
607    #[serde(skip_serializing_if = "Option::is_none")]
608    pub description: Option<Cow<'static, str>>,
609    #[serde(skip_serializing_if = "Option::is_none")]
610    pub default: Option<Vec<String>>,
611}
612
613impl TitledMultiSelectEnumSchema {
614    pub fn new(options: impl Into<Vec<EnumOption>>) -> Self {
615        Self {
616            type_: ArrayTypeConst,
617            items: TitledEnumItems {
618                any_of: options.into(),
619            },
620            min_items: None,
621            max_items: None,
622            title: None,
623            description: None,
624            default: None,
625        }
626    }
627
628    /// Create from tuples of (value, title)
629    pub fn from_pairs<I, V, T>(pairs: I) -> Self
630    where
631        I: IntoIterator<Item = (V, T)>,
632        V: Into<String>,
633        T: Into<String>,
634    {
635        Self::new(
636            pairs
637                .into_iter()
638                .map(|(v, t)| EnumOption::new(v, t))
639                .collect::<Vec<_>>(),
640        )
641    }
642
643    pub fn title(mut self, title: impl Into<Cow<'static, str>>) -> Self {
644        self.title = Some(title.into());
645        self
646    }
647
648    pub fn description(mut self, description: impl Into<Cow<'static, str>>) -> Self {
649        self.description = Some(description.into());
650        self
651    }
652
653    pub fn with_default(mut self, default: impl Into<Vec<String>>) -> Self {
654        self.default = Some(default.into());
655        self
656    }
657
658    pub fn min_items(mut self, min: u32) -> Self {
659        self.min_items = Some(min);
660        self
661    }
662
663    pub fn max_items(mut self, max: u32) -> Self {
664        self.max_items = Some(max);
665        self
666    }
667
668    pub fn items_range(mut self, min: u32, max: u32) -> Self {
669        self.min_items = Some(min);
670        self.max_items = Some(max);
671        self
672    }
673}
674
675// -----------------------------------------------------------------------------
676// Legacy Enum Schema (deprecated, for backwards compatibility)
677// -----------------------------------------------------------------------------
678
679/// Legacy enum schema with parallel enum/enumNames arrays
680///
681/// Deprecated: Use `TitledSingleSelectEnumSchema` instead.
682/// This is kept for backwards compatibility with older implementations.
683#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
684#[serde(rename_all = "camelCase")]
685pub struct LegacyTitledEnumSchema {
686    #[serde(rename = "type")]
687    pub type_: StringTypeConst,
688    #[serde(rename = "enum")]
689    pub enum_values: Vec<String>,
690    /// Display names for enum values (parallel array, same length as enum_values)
691    pub enum_names: Vec<String>,
692    #[serde(skip_serializing_if = "Option::is_none")]
693    pub title: Option<Cow<'static, str>>,
694    #[serde(skip_serializing_if = "Option::is_none")]
695    pub description: Option<Cow<'static, str>>,
696    #[serde(skip_serializing_if = "Option::is_none")]
697    pub default: Option<String>,
698}
699
700impl LegacyTitledEnumSchema {
701    pub fn new(values: Vec<String>, names: Vec<String>) -> Self {
702        debug_assert_eq!(
703            values.len(),
704            names.len(),
705            "enum values and names must have same length"
706        );
707        Self {
708            type_: StringTypeConst,
709            enum_values: values,
710            enum_names: names,
711            title: None,
712            description: None,
713            default: None,
714        }
715    }
716
717    /// Create from tuples of (value, name)
718    pub fn from_pairs<I, V, N>(pairs: I) -> Self
719    where
720        I: IntoIterator<Item = (V, N)>,
721        V: Into<String>,
722        N: Into<String>,
723    {
724        let (values, names): (Vec<_>, Vec<_>) =
725            pairs.into_iter().map(|(v, n)| (v.into(), n.into())).unzip();
726        Self::new(values, names)
727    }
728
729    pub fn title(mut self, title: impl Into<Cow<'static, str>>) -> Self {
730        self.title = Some(title.into());
731        self
732    }
733
734    pub fn description(mut self, description: impl Into<Cow<'static, str>>) -> Self {
735        self.description = Some(description.into());
736        self
737    }
738
739    pub fn with_default(mut self, default: impl Into<String>) -> Self {
740        self.default = Some(default.into());
741        self
742    }
743}
744
745// -----------------------------------------------------------------------------
746// EnumSchema Union
747// -----------------------------------------------------------------------------
748
749/// Union of all enum schema variants for elicitation
750///
751/// Deserialization order matters for untagged enums:
752/// 1. Titled variants (have oneOf/anyOf) must come before untitled (have plain enum)
753/// 2. Multi-select (type: "array") must be checked appropriately
754#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
755#[serde(untagged)]
756pub enum EnumSchema {
757    /// Single-select with titled options (uses oneOf)
758    TitledSingleSelect(TitledSingleSelectEnumSchema),
759    /// Multi-select with titled options (uses items.anyOf)
760    TitledMultiSelect(TitledMultiSelectEnumSchema),
761    /// Multi-select without titles (type: "array")
762    UntitledMultiSelect(UntitledMultiSelectEnumSchema),
763    /// Single-select without titles (simple enum array)
764    UntitledSingleSelect(UntitledSingleSelectEnumSchema),
765    /// Legacy format with parallel enumNames array (deprecated)
766    Legacy(LegacyTitledEnumSchema),
767}
768
769impl EnumSchema {
770    /// Create a simple single-select enum from string values
771    pub fn single_select<I, S>(values: I) -> Self
772    where
773        I: IntoIterator<Item = S>,
774        S: Into<String>,
775    {
776        Self::UntitledSingleSelect(UntitledSingleSelectEnumSchema::new(
777            values.into_iter().map(Into::into).collect::<Vec<_>>(),
778        ))
779    }
780
781    /// Create a single-select enum with display titles
782    pub fn single_select_titled<I, V, T>(pairs: I) -> Self
783    where
784        I: IntoIterator<Item = (V, T)>,
785        V: Into<String>,
786        T: Into<String>,
787    {
788        Self::TitledSingleSelect(TitledSingleSelectEnumSchema::from_pairs(pairs))
789    }
790
791    /// Create a simple multi-select enum from string values
792    pub fn multi_select<I, S>(values: I) -> Self
793    where
794        I: IntoIterator<Item = S>,
795        S: Into<String>,
796    {
797        Self::UntitledMultiSelect(UntitledMultiSelectEnumSchema::new(
798            values.into_iter().map(Into::into).collect::<Vec<_>>(),
799        ))
800    }
801
802    /// Create a multi-select enum with display titles
803    pub fn multi_select_titled<I, V, T>(pairs: I) -> Self
804    where
805        I: IntoIterator<Item = (V, T)>,
806        V: Into<String>,
807        T: Into<String>,
808    {
809        Self::TitledMultiSelect(TitledMultiSelectEnumSchema::from_pairs(pairs))
810    }
811
812    /// Add title to the enum schema
813    pub fn title(self, title: impl Into<Cow<'static, str>>) -> Self {
814        let title = title.into();
815        match self {
816            Self::TitledSingleSelect(s) => Self::TitledSingleSelect(s.title(title)),
817            Self::TitledMultiSelect(s) => Self::TitledMultiSelect(s.title(title)),
818            Self::UntitledMultiSelect(s) => Self::UntitledMultiSelect(s.title(title)),
819            Self::UntitledSingleSelect(s) => Self::UntitledSingleSelect(s.title(title)),
820            Self::Legacy(s) => Self::Legacy(s.title(title)),
821        }
822    }
823
824    /// Add description to the enum schema
825    pub fn description(self, description: impl Into<Cow<'static, str>>) -> Self {
826        let description = description.into();
827        match self {
828            Self::TitledSingleSelect(s) => Self::TitledSingleSelect(s.description(description)),
829            Self::TitledMultiSelect(s) => Self::TitledMultiSelect(s.description(description)),
830            Self::UntitledMultiSelect(s) => Self::UntitledMultiSelect(s.description(description)),
831            Self::UntitledSingleSelect(s) => Self::UntitledSingleSelect(s.description(description)),
832            Self::Legacy(s) => Self::Legacy(s.description(description)),
833        }
834    }
835}
836
837/// Type-safe elicitation schema for requesting structured user input.
838#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
839#[serde(rename_all = "camelCase")]
840pub struct ElicitationSchema {
841    #[serde(rename = "type")]
842    pub type_: ObjectTypeConst,
843    #[serde(skip_serializing_if = "Option::is_none")]
844    pub title: Option<Cow<'static, str>>,
845    pub properties: BTreeMap<String, PrimitiveSchema>,
846    #[serde(skip_serializing_if = "Option::is_none")]
847    pub required: Option<Vec<String>>,
848    #[serde(skip_serializing_if = "Option::is_none")]
849    pub description: Option<Cow<'static, str>>,
850}
851
852impl ElicitationSchema {
853    pub fn new(properties: BTreeMap<String, PrimitiveSchema>) -> Self {
854        Self {
855            type_: ObjectTypeConst,
856            title: None,
857            properties,
858            required: None,
859            description: None,
860        }
861    }
862
863    pub fn builder() -> ElicitationSchemaBuilder {
864        ElicitationSchemaBuilder::new()
865    }
866}
867
868/// Fluent builder for constructing elicitation schemas.
869#[derive(Debug, Default)]
870pub struct ElicitationSchemaBuilder {
871    pub properties: BTreeMap<String, PrimitiveSchema>,
872    pub required: Vec<String>,
873    pub title: Option<Cow<'static, str>>,
874    pub description: Option<Cow<'static, str>>,
875}
876
877impl ElicitationSchemaBuilder {
878    pub fn new() -> Self {
879        Self::default()
880    }
881
882    pub fn property(mut self, name: impl Into<String>, schema: PrimitiveSchema) -> Self {
883        self.properties.insert(name.into(), schema);
884        self
885    }
886
887    pub fn required_property(mut self, name: impl Into<String>, schema: PrimitiveSchema) -> Self {
888        let name_str = name.into();
889        self.required.push(name_str.clone());
890        self.properties.insert(name_str, schema);
891        self
892    }
893
894    pub fn required_email(self, name: impl Into<String>) -> Self {
895        self.required_property(name, PrimitiveSchema::String(StringSchema::email()))
896    }
897
898    pub fn optional_bool(self, name: impl Into<String>, default: bool) -> Self {
899        self.property(
900            name,
901            PrimitiveSchema::Boolean(BooleanSchema::new().with_default(default)),
902        )
903    }
904
905    pub fn required_integer(self, name: impl Into<String>, min: i64, max: i64) -> Self {
906        self.required_property(
907            name,
908            PrimitiveSchema::Integer(IntegerSchema::new().range(min, max)),
909        )
910    }
911
912    /// Add a required single-select enum property
913    pub fn required_enum<I, S>(self, name: impl Into<String>, values: I) -> Self
914    where
915        I: IntoIterator<Item = S>,
916        S: Into<String>,
917    {
918        self.required_property(
919            name,
920            PrimitiveSchema::Enum(EnumSchema::single_select(values)),
921        )
922    }
923
924    /// Add an optional single-select enum property with a default value
925    pub fn optional_enum<I, S>(self, name: impl Into<String>, values: I, default: S) -> Self
926    where
927        I: IntoIterator<Item = S>,
928        S: Into<String>,
929    {
930        let values_vec: Vec<String> = values.into_iter().map(Into::into).collect();
931        let schema = UntitledSingleSelectEnumSchema::new(values_vec).with_default(default);
932        self.property(
933            name,
934            PrimitiveSchema::Enum(EnumSchema::UntitledSingleSelect(schema)),
935        )
936    }
937
938    /// Add a required single-select enum with titled options
939    pub fn required_enum_titled<I, V, T>(self, name: impl Into<String>, options: I) -> Self
940    where
941        I: IntoIterator<Item = (V, T)>,
942        V: Into<String>,
943        T: Into<String>,
944    {
945        self.required_property(
946            name,
947            PrimitiveSchema::Enum(EnumSchema::single_select_titled(options)),
948        )
949    }
950
951    /// Add a required multi-select enum property
952    pub fn required_multi_enum<I, S>(self, name: impl Into<String>, values: I) -> Self
953    where
954        I: IntoIterator<Item = S>,
955        S: Into<String>,
956    {
957        self.required_property(
958            name,
959            PrimitiveSchema::Enum(EnumSchema::multi_select(values)),
960        )
961    }
962
963    /// Add an optional multi-select enum property
964    pub fn optional_multi_enum<I, S>(self, name: impl Into<String>, values: I) -> Self
965    where
966        I: IntoIterator<Item = S>,
967        S: Into<String>,
968    {
969        self.property(
970            name,
971            PrimitiveSchema::Enum(EnumSchema::multi_select(values)),
972        )
973    }
974
975    pub fn description(mut self, description: impl Into<Cow<'static, str>>) -> Self {
976        self.description = Some(description.into());
977        self
978    }
979
980    pub fn build(self) -> Result<ElicitationSchema, &'static str> {
981        // Validate that all required fields exist in properties
982        if !self.required.is_empty() {
983            for field_name in &self.required {
984                if !self.properties.contains_key(field_name) {
985                    return Err("Required field does not exist in properties");
986                }
987            }
988        }
989
990        Ok(ElicitationSchema {
991            type_: ObjectTypeConst,
992            title: self.title,
993            properties: self.properties,
994            required: if self.required.is_empty() {
995                None
996            } else {
997                Some(self.required)
998            },
999            description: self.description,
1000        })
1001    }
1002
1003    pub fn build_unchecked(self) -> ElicitationSchema {
1004        self.build().expect("Invalid elicitation schema")
1005    }
1006}
1007
1008#[cfg(test)]
1009mod tests {
1010    use super::*;
1011    use serde_json::json;
1012
1013    #[test]
1014    fn test_untitled_single_select_serialization() {
1015        let schema = UntitledSingleSelectEnumSchema::new(vec![
1016            "red".to_string(),
1017            "green".to_string(),
1018            "blue".to_string(),
1019        ])
1020        .title("Color")
1021        .with_default("red");
1022
1023        let json = serde_json::to_value(&schema).unwrap();
1024        assert_eq!(json["type"], "string");
1025        assert_eq!(json["enum"], json!(["red", "green", "blue"]));
1026        assert_eq!(json["title"], "Color");
1027        assert_eq!(json["default"], "red");
1028    }
1029
1030    #[test]
1031    fn test_untitled_single_select_deserialization() {
1032        let json = json!({
1033            "type": "string",
1034            "enum": ["a", "b", "c"],
1035            "title": "Choice"
1036        });
1037
1038        let schema: UntitledSingleSelectEnumSchema = serde_json::from_value(json).unwrap();
1039        assert_eq!(schema.enum_values, vec!["a", "b", "c"]);
1040        assert_eq!(schema.title.as_deref(), Some("Choice"));
1041    }
1042
1043    #[test]
1044    fn test_titled_single_select_serialization() {
1045        let schema = TitledSingleSelectEnumSchema::from_pairs([
1046            ("#FF0000", "Red"),
1047            ("#00FF00", "Green"),
1048            ("#0000FF", "Blue"),
1049        ])
1050        .title("Color Selection");
1051
1052        let json = serde_json::to_value(&schema).unwrap();
1053        assert_eq!(json["type"], "string");
1054        assert_eq!(json["oneOf"][0]["const"], "#FF0000");
1055        assert_eq!(json["oneOf"][0]["title"], "Red");
1056        assert_eq!(json["title"], "Color Selection");
1057    }
1058
1059    #[test]
1060    fn test_titled_single_select_deserialization() {
1061        let json = json!({
1062            "type": "string",
1063            "oneOf": [
1064                { "const": "val1", "title": "Value 1" },
1065                { "const": "val2", "title": "Value 2" }
1066            ]
1067        });
1068
1069        let schema: TitledSingleSelectEnumSchema = serde_json::from_value(json).unwrap();
1070        assert_eq!(schema.one_of.len(), 2);
1071        assert_eq!(schema.one_of[0].const_value, "val1");
1072        assert_eq!(schema.one_of[0].title, "Value 1");
1073    }
1074
1075    #[test]
1076    fn test_untitled_multi_select_serialization() {
1077        let schema = UntitledMultiSelectEnumSchema::new(vec![
1078            "apple".to_string(),
1079            "banana".to_string(),
1080            "cherry".to_string(),
1081        ])
1082        .min_items(1)
1083        .max_items(2)
1084        .title("Fruits");
1085
1086        let json = serde_json::to_value(&schema).unwrap();
1087        assert_eq!(json["type"], "array");
1088        assert_eq!(json["items"]["type"], "string");
1089        assert_eq!(json["items"]["enum"], json!(["apple", "banana", "cherry"]));
1090        assert_eq!(json["minItems"], 1);
1091        assert_eq!(json["maxItems"], 2);
1092    }
1093
1094    #[test]
1095    fn test_titled_multi_select_serialization() {
1096        let schema =
1097            TitledMultiSelectEnumSchema::from_pairs([("opt1", "Option 1"), ("opt2", "Option 2")])
1098                .items_range(1, 2)
1099                .with_default(vec!["opt1".to_string()]);
1100
1101        let json = serde_json::to_value(&schema).unwrap();
1102        assert_eq!(json["type"], "array");
1103        assert_eq!(json["items"]["anyOf"][0]["const"], "opt1");
1104        assert_eq!(json["items"]["anyOf"][0]["title"], "Option 1");
1105        assert_eq!(json["default"], json!(["opt1"]));
1106    }
1107
1108    #[test]
1109    fn test_legacy_enum_serialization() {
1110        let schema = LegacyTitledEnumSchema::from_pairs([("v1", "Value 1"), ("v2", "Value 2")]);
1111
1112        let json = serde_json::to_value(&schema).unwrap();
1113        assert_eq!(json["type"], "string");
1114        assert_eq!(json["enum"], json!(["v1", "v2"]));
1115        assert_eq!(json["enumNames"], json!(["Value 1", "Value 2"]));
1116    }
1117
1118    #[test]
1119    fn test_enum_schema_factory_methods() {
1120        // Single select
1121        let single = EnumSchema::single_select(["a", "b", "c"]);
1122        assert!(matches!(single, EnumSchema::UntitledSingleSelect(_)));
1123
1124        // Single select titled
1125        let titled = EnumSchema::single_select_titled([("val", "Title")]);
1126        assert!(matches!(titled, EnumSchema::TitledSingleSelect(_)));
1127
1128        // Multi select
1129        let multi = EnumSchema::multi_select(["x", "y"]);
1130        assert!(matches!(multi, EnumSchema::UntitledMultiSelect(_)));
1131
1132        // Multi select titled
1133        let multi_titled = EnumSchema::multi_select_titled([("a", "A"), ("b", "B")]);
1134        assert!(matches!(multi_titled, EnumSchema::TitledMultiSelect(_)));
1135    }
1136
1137    #[test]
1138    fn test_enum_schema_deserialization_dispatch() {
1139        // Should deserialize as TitledSingleSelect (has oneOf)
1140        let json = json!({
1141            "type": "string",
1142            "oneOf": [{ "const": "x", "title": "X" }]
1143        });
1144        let schema: EnumSchema = serde_json::from_value(json).unwrap();
1145        assert!(matches!(schema, EnumSchema::TitledSingleSelect(_)));
1146
1147        // Should deserialize as UntitledSingleSelect (has enum, no oneOf)
1148        let json = json!({
1149            "type": "string",
1150            "enum": ["a", "b"]
1151        });
1152        let schema: EnumSchema = serde_json::from_value(json).unwrap();
1153        assert!(matches!(schema, EnumSchema::UntitledSingleSelect(_)));
1154
1155        // Should deserialize as TitledMultiSelect (type array, has anyOf)
1156        let json = json!({
1157            "type": "array",
1158            "items": {
1159                "anyOf": [{ "const": "x", "title": "X" }]
1160            }
1161        });
1162        let schema: EnumSchema = serde_json::from_value(json).unwrap();
1163        assert!(matches!(schema, EnumSchema::TitledMultiSelect(_)));
1164
1165        // Should deserialize as UntitledMultiSelect (type array, items.enum)
1166        let json = json!({
1167            "type": "array",
1168            "items": {
1169                "type": "string",
1170                "enum": ["a", "b"]
1171            }
1172        });
1173        let schema: EnumSchema = serde_json::from_value(json).unwrap();
1174        assert!(matches!(schema, EnumSchema::UntitledMultiSelect(_)));
1175    }
1176
1177    #[test]
1178    fn test_elicitation_builder_with_enum() {
1179        let schema = ElicitationSchema::builder()
1180            .required_enum("color", ["red", "green", "blue"])
1181            .required_enum_titled("size", [("s", "Small"), ("m", "Medium"), ("l", "Large")])
1182            .required_multi_enum("tags", ["tag1", "tag2", "tag3"])
1183            .build()
1184            .unwrap();
1185
1186        assert_eq!(
1187            schema.required,
1188            Some(vec![
1189                "color".to_string(),
1190                "size".to_string(),
1191                "tags".to_string()
1192            ])
1193        );
1194        assert!(schema.properties.contains_key("color"));
1195        assert!(schema.properties.contains_key("size"));
1196        assert!(schema.properties.contains_key("tags"));
1197    }
1198
1199    #[test]
1200    fn test_enum_schema_roundtrip() {
1201        // Test that serialization -> deserialization produces equivalent schema
1202        let original = EnumSchema::single_select_titled([("#FF0000", "Red"), ("#00FF00", "Green")])
1203            .title("Pick a color")
1204            .description("Choose your favorite");
1205
1206        let json = serde_json::to_string(&original).unwrap();
1207        let parsed: EnumSchema = serde_json::from_str(&json).unwrap();
1208
1209        // Re-serialize and compare
1210        let json2 = serde_json::to_string(&parsed).unwrap();
1211        assert_eq!(json, json2);
1212    }
1213
1214    #[test]
1215    fn test_primitive_schema_with_enum() {
1216        let enum_schema = EnumSchema::single_select(["a", "b", "c"]);
1217        let primitive = PrimitiveSchema::Enum(enum_schema);
1218
1219        let json = serde_json::to_value(&primitive).unwrap();
1220        assert_eq!(json["type"], "string");
1221        assert_eq!(json["enum"], json!(["a", "b", "c"]));
1222    }
1223}