//! Type-safe schema definitions for MCP elicitation requests. //! //! This module provides strongly-typed schema definitions for elicitation requests //! that comply with the MCP 2025-06-18 specification. Elicitation schemas must be //! objects with primitive-typed properties. //! //! # Example //! //! ```rust //! use rmcp::model::*; //! //! let schema = ElicitationSchema::builder() //! .required_email("email") //! .required_integer("age", 0, 150) //! .optional_bool("newsletter", false) //! .build(); //! ``` use std::{borrow::Cow, collections::BTreeMap, marker::PhantomData}; use serde::{Deserialize, Serialize}; use crate::{const_string, model::ConstString}; // ============================================================================= // CONST TYPES FOR JSON SCHEMA TYPE FIELD // ============================================================================= const_string!(ObjectTypeConst = "object"); const_string!(StringTypeConst = "string"); const_string!(NumberTypeConst = "number"); const_string!(IntegerTypeConst = "integer"); const_string!(BooleanTypeConst = "boolean"); const_string!(EnumTypeConst = "string"); const_string!(ArrayTypeConst = "array"); // ============================================================================= // PRIMITIVE SCHEMA DEFINITIONS // ============================================================================= /// Primitive schema definition for elicitation properties. /// /// According to MCP 2025-06-18 specification, elicitation schemas must have /// properties of primitive types only (string, number, integer, boolean, enum). /// /// Note: Put Enum as the first variant to avoid ambiguity during deserialization. /// This is due to the fact that EnumSchema can contain StringSchema internally and serde /// uses first match wins strategy when deserializing untagged enums. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(untagged)] #[non_exhaustive] pub enum PrimitiveSchemaDefinition { /// Enum property (explicit enum schema) Enum(EnumSchema), /// String property (with optional enum constraint) String(StringSchema), /// Number property (with optional enum constraint) Number(NumberSchema), /// Integer property (with optional enum constraint) Integer(IntegerSchema), /// Boolean property Boolean(BooleanSchema), } #[deprecated(since = "2.0.0", note = "Renamed to PrimitiveSchemaDefinition")] pub type PrimitiveSchema = PrimitiveSchemaDefinition; // ============================================================================= // STRING SCHEMA // ============================================================================= /// String format types allowed by the MCP specification. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(rename_all = "kebab-case")] #[non_exhaustive] pub enum StringFormat { /// Email address format Email, /// URI format Uri, /// Date format (YYYY-MM-DD) Date, /// Date-time format (ISO 8601) DateTime, } /// Schema definition for string properties. /// /// Compliant with MCP 2025-06-18 specification for elicitation schemas. /// Supports only the fields allowed by the MCP spec: /// - format limited to: "email", "uri", "date", "date-time" #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase")] #[non_exhaustive] pub struct StringSchema { /// Type discriminator #[serde(rename = "type")] pub type_: StringTypeConst, /// Optional title for the schema #[serde(skip_serializing_if = "Option::is_none")] pub title: Option>, /// Human-readable description #[serde(skip_serializing_if = "Option::is_none")] pub description: Option>, /// Minimum string length #[serde(skip_serializing_if = "Option::is_none")] pub min_length: Option, /// Maximum string length #[serde(skip_serializing_if = "Option::is_none")] pub max_length: Option, /// String format - limited to: "email", "uri", "date", "date-time" #[serde(skip_serializing_if = "Option::is_none")] pub format: Option, /// Default value #[serde(skip_serializing_if = "Option::is_none")] pub default: Option, } impl Default for StringSchema { fn default() -> Self { Self { type_: StringTypeConst, title: None, description: None, min_length: None, max_length: None, format: None, default: None, } } } impl StringSchema { /// Create a new string schema pub fn new() -> Self { Self::default() } /// Create an email string schema pub fn email() -> Self { Self { format: Some(StringFormat::Email), ..Default::default() } } /// Create a URI string schema pub fn uri() -> Self { Self { format: Some(StringFormat::Uri), ..Default::default() } } /// Create a date string schema pub fn date() -> Self { Self { format: Some(StringFormat::Date), ..Default::default() } } /// Create a date-time string schema pub fn date_time() -> Self { Self { format: Some(StringFormat::DateTime), ..Default::default() } } /// Set title pub fn title(mut self, title: impl Into>) -> Self { self.title = Some(title.into()); self } /// Set description pub fn description(mut self, description: impl Into>) -> Self { self.description = Some(description.into()); self } /// Set minimum and maximum length pub fn with_length(mut self, min: u32, max: u32) -> Result { if min > max { return Err("min_length must be <= max_length"); } self.min_length = Some(min); self.max_length = Some(max); Ok(self) } /// Set minimum and maximum length (panics on invalid input) pub fn length(mut self, min: u32, max: u32) -> Self { assert!(min <= max, "min_length must be <= max_length"); self.min_length = Some(min); self.max_length = Some(max); self } /// Set minimum length pub fn min_length(mut self, min: u32) -> Self { self.min_length = Some(min); self } /// Set maximum length pub fn max_length(mut self, max: u32) -> Self { self.max_length = Some(max); self } /// Set format (limited to: "email", "uri", "date", "date-time") pub fn format(mut self, format: StringFormat) -> Self { self.format = Some(format); self } /// Set default value pub fn with_default(mut self, default: impl Into) -> Self { self.default = Some(default.into()); self } } // ============================================================================= // NUMBER SCHEMA // ============================================================================= /// Schema definition for number properties (floating-point). /// /// Compliant with MCP 2025-06-18 specification for elicitation schemas. /// Supports only the fields allowed by the MCP spec. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase")] #[non_exhaustive] pub struct NumberSchema { /// Type discriminator #[serde(rename = "type")] pub type_: NumberTypeConst, /// Optional title for the schema #[serde(skip_serializing_if = "Option::is_none")] pub title: Option>, /// Human-readable description #[serde(skip_serializing_if = "Option::is_none")] pub description: Option>, /// Minimum value (inclusive) #[serde(skip_serializing_if = "Option::is_none")] pub minimum: Option, /// Maximum value (inclusive) #[serde(skip_serializing_if = "Option::is_none")] pub maximum: Option, /// Default value #[serde(skip_serializing_if = "Option::is_none")] pub default: Option, } impl Default for NumberSchema { fn default() -> Self { Self { type_: NumberTypeConst, title: None, description: None, minimum: None, maximum: None, default: None, } } } impl NumberSchema { /// Create a new number schema pub fn new() -> Self { Self::default() } /// Set minimum and maximum (inclusive) pub fn with_range(mut self, min: f64, max: f64) -> Result { if min > max { return Err("minimum must be <= maximum"); } self.minimum = Some(min); self.maximum = Some(max); Ok(self) } /// Set minimum and maximum (panics on invalid input) pub fn range(mut self, min: f64, max: f64) -> Self { assert!(min <= max, "minimum must be <= maximum"); self.minimum = Some(min); self.maximum = Some(max); self } /// Set minimum (inclusive) pub fn minimum(mut self, min: f64) -> Self { self.minimum = Some(min); self } /// Set maximum (inclusive) pub fn maximum(mut self, max: f64) -> Self { self.maximum = Some(max); self } /// Set title pub fn title(mut self, title: impl Into>) -> Self { self.title = Some(title.into()); self } /// Set description pub fn description(mut self, description: impl Into>) -> Self { self.description = Some(description.into()); self } /// Set default value pub fn with_default(mut self, default: f64) -> Self { self.default = Some(default); self } } // ============================================================================= // INTEGER SCHEMA // ============================================================================= /// Schema definition for integer properties. /// /// Compliant with MCP 2025-06-18 specification for elicitation schemas. /// Supports only the fields allowed by the MCP spec. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase")] #[non_exhaustive] pub struct IntegerSchema { /// Type discriminator #[serde(rename = "type")] pub type_: IntegerTypeConst, /// Optional title for the schema #[serde(skip_serializing_if = "Option::is_none")] pub title: Option>, /// Human-readable description #[serde(skip_serializing_if = "Option::is_none")] pub description: Option>, /// Minimum value (inclusive) #[serde(skip_serializing_if = "Option::is_none")] pub minimum: Option, /// Maximum value (inclusive) #[serde(skip_serializing_if = "Option::is_none")] pub maximum: Option, /// Default value #[serde(skip_serializing_if = "Option::is_none")] pub default: Option, } impl Default for IntegerSchema { fn default() -> Self { Self { type_: IntegerTypeConst, title: None, description: None, minimum: None, maximum: None, default: None, } } } impl IntegerSchema { /// Create a new integer schema pub fn new() -> Self { Self::default() } /// Set minimum and maximum (inclusive) pub fn with_range(mut self, min: i64, max: i64) -> Result { if min > max { return Err("minimum must be <= maximum"); } self.minimum = Some(min); self.maximum = Some(max); Ok(self) } /// Set minimum and maximum (panics on invalid input) pub fn range(mut self, min: i64, max: i64) -> Self { assert!(min <= max, "minimum must be <= maximum"); self.minimum = Some(min); self.maximum = Some(max); self } /// Set minimum (inclusive) pub fn minimum(mut self, min: i64) -> Self { self.minimum = Some(min); self } /// Set maximum (inclusive) pub fn maximum(mut self, max: i64) -> Self { self.maximum = Some(max); self } /// Set title pub fn title(mut self, title: impl Into>) -> Self { self.title = Some(title.into()); self } /// Set description pub fn description(mut self, description: impl Into>) -> Self { self.description = Some(description.into()); self } /// Set default value pub fn with_default(mut self, default: i64) -> Self { self.default = Some(default); self } } // ============================================================================= // BOOLEAN SCHEMA // ============================================================================= /// Schema definition for boolean properties. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase")] #[non_exhaustive] pub struct BooleanSchema { /// Type discriminator #[serde(rename = "type")] pub type_: BooleanTypeConst, /// Optional title for the schema #[serde(skip_serializing_if = "Option::is_none")] pub title: Option>, /// Human-readable description #[serde(skip_serializing_if = "Option::is_none")] pub description: Option>, /// Default value #[serde(skip_serializing_if = "Option::is_none")] pub default: Option, } impl Default for BooleanSchema { fn default() -> Self { Self { type_: BooleanTypeConst, title: None, description: None, default: None, } } } impl BooleanSchema { /// Create a new boolean schema pub fn new() -> Self { Self::default() } /// Set title pub fn title(mut self, title: impl Into>) -> Self { self.title = Some(title.into()); self } /// Set description pub fn description(mut self, description: impl Into>) -> Self { self.description = Some(description.into()); self } /// Set default value pub fn with_default(mut self, default: bool) -> Self { self.default = Some(default); self } } // ============================================================================= // ENUM SCHEMA // ============================================================================= /// Schema definition for enum properties. /// /// Represent single entry for titled item #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[non_exhaustive] pub struct ConstTitle { #[serde(rename = "const")] pub const_: String, pub title: String, } impl ConstTitle { /// Create a new ConstTitle. pub fn new(const_: impl Into, title: impl Into) -> Self { Self { const_: const_.into(), title: title.into(), } } } /// Legacy enum schema, keep for backward compatibility #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[non_exhaustive] pub struct LegacyEnumSchema { #[serde(rename = "type")] pub type_: StringTypeConst, #[serde(skip_serializing_if = "Option::is_none")] pub title: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub description: Option>, #[serde(rename = "enum")] pub enum_: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub enum_names: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub default: Option, } impl LegacyEnumSchema { pub fn new(enum_values: Vec) -> Self { Self { type_: StringTypeConst, title: None, description: None, enum_: enum_values, enum_names: None, default: None, } } } /// Untitled single-select #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[non_exhaustive] pub struct UntitledSingleSelectEnumSchema { #[serde(rename = "type")] pub type_: StringTypeConst, #[serde(skip_serializing_if = "Option::is_none")] pub title: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub description: Option>, #[serde(rename = "enum")] pub enum_: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub default: Option, } /// Titled single-select #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[non_exhaustive] pub struct TitledSingleSelectEnumSchema { #[serde(rename = "type")] pub type_: StringTypeConst, #[serde(skip_serializing_if = "Option::is_none")] pub title: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub description: Option>, #[serde(rename = "oneOf")] pub one_of: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub default: Option, } impl TitledSingleSelectEnumSchema { /// Create a new TitledSingleSelectEnumSchema. pub fn new(one_of: Vec) -> Self { Self { type_: StringTypeConst, title: None, description: None, one_of, default: None, } } } /// Combined single-select #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(untagged)] #[non_exhaustive] pub enum SingleSelectEnumSchema { Untitled(UntitledSingleSelectEnumSchema), Titled(TitledSingleSelectEnumSchema), } /// Items for untitled multi-select options #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[non_exhaustive] pub struct UntitledItems { #[serde(rename = "type")] pub type_: StringTypeConst, #[serde(rename = "enum")] pub enum_: Vec, } impl UntitledItems { pub fn new(enum_values: Vec) -> Self { Self { type_: StringTypeConst, enum_: enum_values, } } } /// Items for titled multi-select options #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[non_exhaustive] pub struct TitledItems { // MCP spec requires "anyOf" for multi-select enums (allows any combination) // Alias "oneOf" for compatibility with schemars #[serde(rename = "anyOf", alias = "oneOf")] pub any_of: Vec, } impl TitledItems { /// Create a new TitledItems. pub fn new(any_of: Vec) -> Self { Self { any_of } } } /// Multi-select untitled options #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase")] #[non_exhaustive] pub struct UntitledMultiSelectEnumSchema { #[serde(rename = "type")] pub type_: ArrayTypeConst, #[serde(skip_serializing_if = "Option::is_none")] pub title: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub description: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub min_items: Option, #[serde(skip_serializing_if = "Option::is_none")] pub max_items: Option, pub items: UntitledItems, #[serde(skip_serializing_if = "Option::is_none")] pub default: Option>, } /// Multi-select titled options #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase")] #[non_exhaustive] pub struct TitledMultiSelectEnumSchema { #[serde(rename = "type")] pub type_: ArrayTypeConst, #[serde(skip_serializing_if = "Option::is_none")] pub title: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub description: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub min_items: Option, #[serde(skip_serializing_if = "Option::is_none")] pub max_items: Option, pub items: TitledItems, #[serde(skip_serializing_if = "Option::is_none")] pub default: Option>, } impl TitledMultiSelectEnumSchema { /// Create a new TitledMultiSelectEnumSchema. pub fn new(items: TitledItems) -> Self { Self { type_: ArrayTypeConst, title: None, description: None, min_items: None, max_items: None, items, default: None, } } /// Set the title. pub fn with_title(mut self, title: impl Into>) -> Self { self.title = Some(title.into()); self } /// Set the description. pub fn with_description(mut self, description: impl Into>) -> Self { self.description = Some(description.into()); self } /// Set the minimum number of items. pub fn with_min_items(mut self, min_items: u64) -> Self { self.min_items = Some(min_items); self } /// Set the maximum number of items. pub fn with_max_items(mut self, max_items: u64) -> Self { self.max_items = Some(max_items); self } /// Set the default values. pub fn with_default(mut self, default: Vec) -> Self { self.default = Some(default); self } } /// Multi-select enum options #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(untagged)] #[non_exhaustive] pub enum MultiSelectEnumSchema { Untitled(UntitledMultiSelectEnumSchema), Titled(TitledMultiSelectEnumSchema), } /// Compliant with MCP 2025-06-18 specification for elicitation schemas. /// Enums must have string type for values and can optionally include human-readable names. /// /// # Example /// /// ```rust /// use rmcp::model::*; /// /// let enum_schema = EnumSchema::builder(vec!["US".to_string(), "UK".to_string()]) /// .multiselect() /// .min_items(1u64).expect("Min items should be correct value") /// .max_items(4u64).expect("Max items should be correct value") /// .description("Country code") /// .build(); /// ``` #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(untagged)] #[non_exhaustive] pub enum EnumSchema { Single(SingleSelectEnumSchema), Multi(MultiSelectEnumSchema), Legacy(LegacyEnumSchema), } /// Marker type for single-select enum builder #[derive(Debug)] #[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct SingleSelect; /// Marker type for multi-select enum builder #[derive(Debug)] #[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct MultiSelect; /// Builder for EnumSchema /// Allows to create various enum schema types (single/multi select, titled/untitled) /// with validation of provided parameters /// /// # Example /// ```rust /// use rmcp::model::*; /// let enum_schema = EnumSchema::builder(vec!["Red".to_string(), "Green".to_string(), "Blue".to_string()]) /// .multiselect() /// .enum_titles(vec!["Red Color".to_string(), "Green Color".to_string(), "Blue Color".to_string()]) /// .expect("Number of titles should match number of values") /// .min_items(1u64).expect("Min items should be correct value") /// .max_items(3u64).expect("Max items should be correct value") /// .description("Select your favorite colors") /// .build(); /// ``` #[derive(Debug)] pub struct EnumSchemaBuilder { /// Enum values enum_values: Vec, /// If true generate Titled EnumSchema, UnTitled otherwise titled: bool, /// Title of EnumSchema title: Option>, /// Description of EnumSchema description: Option>, /// Titles of given enum values enum_titles: Vec, /// Minimum number of items to choose for MultiSelect min_items: Option, /// Maximum number of items to choose for MultiSelect max_items: Option, /// Default values for enum default: Vec, select_type: PhantomData, } /// Default implementation for single-select enum builder impl Default for EnumSchemaBuilder { fn default() -> Self { Self { title: None, description: None, titled: false, enum_titles: Vec::new(), enum_values: Vec::new(), min_items: None, max_items: None, default: Vec::new(), select_type: PhantomData, } } } /// Common enum schema builder methods impl EnumSchemaBuilder { /// Set title of enum schema pub fn title(mut self, value: impl Into>) -> Self { self.title = Some(value.into()); self } /// Set description of enum schema pub fn description(mut self, value: impl Into>) -> Self { self.description = Some(value.into()); self } /// Set enum as untitled /// Clears any previously set titles pub fn untitled(mut self) -> Self { self.enum_titles = Vec::new(); self.titled = false; self } /// Set titles to enum values. Also, implicitly set this enum schema as titled pub fn enum_titles(mut self, titles: Vec) -> Result, String> { if titles.len() != self.enum_values.len() { return Err(format!( "Provided number of titles do not match number of values: expected {}, but got {}", self.enum_values.len(), titles.len() )); } self.titled = true; self.enum_titles = titles; Ok(self) } } /// Enum selection builder for single-select enums impl EnumSchemaBuilder { pub fn new(values: Vec) -> EnumSchemaBuilder { EnumSchemaBuilder { enum_values: values, ..Default::default() } } /// Transition to multi-select enum builder. /// /// Clears any previously set default values and resets min/max items. /// After this transition, you can use `min_items()`, `max_items()`, and /// `with_default()` for multi-select semantics. pub fn multiselect(self) -> EnumSchemaBuilder { EnumSchemaBuilder { enum_values: self.enum_values, titled: self.titled, title: self.title, description: self.description, enum_titles: self.enum_titles, min_items: None, max_items: None, default: Vec::new(), // Clear default for multi-select select_type: PhantomData, } } /// Set default value pub fn with_default( mut self, default_value: impl Into, ) -> Result, String> { let value: String = default_value.into(); if !self.enum_values.contains(&value) { return Err("Provided default value is not in enum values".to_string()); } self.default = vec![value]; Ok(self) } /// Build enum schema pub fn build(mut self) -> EnumSchema { match self.titled { false => EnumSchema::Single(SingleSelectEnumSchema::Untitled( UntitledSingleSelectEnumSchema { type_: StringTypeConst, title: self.title, description: self.description, enum_: self.enum_values, default: self.default.pop(), }, )), true => EnumSchema::Single(SingleSelectEnumSchema::Titled( TitledSingleSelectEnumSchema { type_: StringTypeConst, title: self.title, description: self.description, one_of: self .enum_titles .into_iter() .zip(self.enum_values) .map(|(title, const_)| ConstTitle { const_, title }) .collect(), default: self.default.pop(), }, )), } } } /// Enum selection builder for multi-select enums impl EnumSchemaBuilder { /// Set enum as single-select /// If it was multi-select, clear default values pub fn single_select(self) -> EnumSchemaBuilder { EnumSchemaBuilder { enum_values: self.enum_values, titled: self.titled, title: self.title, description: self.description, enum_titles: self.enum_titles, min_items: None, max_items: None, default: Vec::new(), // Clear default for single-select select_type: PhantomData, } } /// Set default values pub fn with_default( mut self, default_values: Vec, ) -> Result, String> { for value in &default_values { if !self.enum_values.contains(value) { return Err("One of the provided default values is not in enum values".to_string()); } } if let Some(min) = self.min_items { if (default_values.len() as u64) < min { return Err("Number of provided default values is less than min_items".to_string()); } } if let Some(max) = self.max_items { if (default_values.len() as u64) > max { return Err( "Number of provided default values is greater than max_items".to_string(), ); } } self.default = default_values; Ok(self) } /// Set minimal number of items for multi-select enum options pub fn min_items(mut self, value: u64) -> Result, String> { if let Some(max) = self.max_items && value > max { return Err("Provided value is greater than max_items".to_string()); } self.min_items = Some(value); Ok(self) } /// Set maximal number of items for multi-select enum options pub fn max_items(mut self, value: u64) -> Result, String> { if let Some(min) = self.min_items && value < min { return Err("Provided value is less than min_items".to_string()); } self.max_items = Some(value); Ok(self) } /// Build enum schema pub fn build(self) -> EnumSchema { match self.titled { false => EnumSchema::Multi(MultiSelectEnumSchema::Untitled( UntitledMultiSelectEnumSchema { type_: ArrayTypeConst, title: self.title, description: self.description, min_items: self.min_items, max_items: self.max_items, items: UntitledItems { type_: StringTypeConst, enum_: self.enum_values, }, default: if self.default.is_empty() { None } else { Some(self.default) }, }, )), true => EnumSchema::Multi(MultiSelectEnumSchema::Titled(TitledMultiSelectEnumSchema { type_: ArrayTypeConst, title: self.title, description: self.description, min_items: self.min_items, max_items: self.max_items, items: TitledItems { any_of: self .enum_titles .into_iter() .zip(self.enum_values) .map(|(title, const_)| ConstTitle { const_, title }) .collect(), }, default: if self.default.is_empty() { None } else { Some(self.default) }, })), } } } impl EnumSchema { /// Creates a new `EnumSchemaBuilder` with the given enum values. /// /// This convenience method allows you to construct an enum schema by specifying /// the possible string values for the enum. Use the returned builder to further /// configure the schema before building it. /// /// # Arguments /// /// * `values` - A vector of strings representing the allowed enum values. /// /// # Example /// /// ``` /// use rmcp::model::*; /// /// let enum_schema = EnumSchema::builder(vec!["A".to_string(), "B".to_string()]). /// with_default("A"). /// expect("Default value should be valid"). /// enum_titles(vec!["Option A".to_string(), "Option B".to_string()]). /// expect("Number of titles should match number of values"). /// build(); /// ``` pub fn builder(values: Vec) -> EnumSchemaBuilder { EnumSchemaBuilder::new(values) } } // ============================================================================= // ELICITATION SCHEMA // ============================================================================= /// Type-safe elicitation schema for requesting structured user input. /// /// This enforces the MCP 2025-06-18 specification that elicitation schemas /// must be objects with primitive-typed properties. /// /// # Example /// /// ```rust /// use rmcp::model::*; /// /// let schema = ElicitationSchema::builder() /// .required_email("email") /// .required_integer("age", 0, 150) /// .optional_bool("newsletter", false) /// .build(); /// ``` #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase")] #[non_exhaustive] pub struct ElicitationSchema { /// Always "object" for elicitation schemas #[serde(rename = "type")] pub type_: ObjectTypeConst, /// Optional title for the schema #[serde(skip_serializing_if = "Option::is_none")] pub title: Option>, /// Property definitions (must be primitive types) pub properties: BTreeMap, /// List of required property names #[serde(skip_serializing_if = "Option::is_none")] pub required: Option>, /// Optional description of what this schema represents #[serde(skip_serializing_if = "Option::is_none")] pub description: Option>, } impl ElicitationSchema { /// Create a new elicitation schema with the given properties pub fn new(properties: BTreeMap) -> Self { Self { type_: ObjectTypeConst, title: None, properties, required: None, description: None, } } /// Convert from a JSON Schema object (typically generated by schemars) /// /// This allows converting from JsonObject to ElicitationSchema, which is useful /// when working with automatically generated schemas from types. /// /// # Example /// /// ```rust,ignore /// use rmcp::model::*; /// /// let json_schema = schema_for_type::(); /// let elicitation_schema = ElicitationSchema::from_json_schema(json_schema)?; /// ``` /// /// # Errors /// /// Returns a [`serde_json::Error`] if the JSON object cannot be deserialized /// into a valid ElicitationSchema. pub fn from_json_schema(schema: crate::model::JsonObject) -> Result { serde_json::from_value(serde_json::Value::Object(schema)) } /// Generate an ElicitationSchema from a Rust type that implements JsonSchema /// /// This is a convenience method that combines schema generation and conversion. /// It uses the same schema generation settings as the rest of the MCP SDK. /// /// # Example /// /// ```rust,ignore /// use rmcp::model::*; /// use schemars::JsonSchema; /// use serde::{Deserialize, Serialize}; /// /// #[derive(JsonSchema, Serialize, Deserialize)] /// struct UserInput { /// name: String, /// age: u32, /// } /// /// let schema = ElicitationSchema::from_type::()?; /// ``` /// /// # Errors /// /// Returns a [`serde_json::Error`] if the generated schema cannot be converted /// to a valid ElicitationSchema. #[cfg(feature = "schemars")] pub fn from_type() -> Result where T: schemars::JsonSchema, { use crate::schemars::generate::SchemaSettings; let mut settings = SchemaSettings::draft07(); settings.transforms = vec![Box::new(schemars::transform::AddNullable::default())]; let generator = settings.into_generator(); let schema = generator.into_root_schema_for::(); let object = serde_json::to_value(schema).expect("failed to serialize schema"); match object { serde_json::Value::Object(object) => Self::from_json_schema(object), _ => panic!( "Schema serialization produced non-object value: expected JSON object but got {:?}", object ), } } /// Set the required fields pub fn with_required(mut self, required: Vec) -> Self { self.required = Some(required); self } /// Set the title pub fn with_title(mut self, title: impl Into>) -> Self { self.title = Some(title.into()); self } /// Set the description pub fn with_description(mut self, description: impl Into>) -> Self { self.description = Some(description.into()); self } /// Create a builder for constructing elicitation schemas fluently pub fn builder() -> ElicitationSchemaBuilder { ElicitationSchemaBuilder::new() } } // ============================================================================= // BUILDER // ============================================================================= /// Fluent builder for constructing elicitation schemas. /// /// # Example /// /// ```rust /// use rmcp::model::*; /// /// let schema = ElicitationSchema::builder() /// .required_email("email") /// .required_integer("age", 0, 150) /// .optional_bool("newsletter", false) /// .description("User registration") /// .build(); /// ``` #[derive(Debug, Default)] #[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct ElicitationSchemaBuilder { pub properties: BTreeMap, pub required: Vec, pub title: Option>, pub description: Option>, } impl ElicitationSchemaBuilder { /// Create a new builder pub fn new() -> Self { Self::default() } /// Add a property to the schema pub fn property(mut self, name: impl Into, schema: PrimitiveSchemaDefinition) -> Self { self.properties.insert(name.into(), schema); self } /// Add a required property to the schema pub fn required_property( mut self, name: impl Into, schema: PrimitiveSchemaDefinition, ) -> Self { let name_str = name.into(); self.required.push(name_str.clone()); self.properties.insert(name_str, schema); self } // =========================================================================== // TYPED PROPERTY METHODS - Cleaner API without PrimitiveSchemaDefinition wrapper // =========================================================================== /// Add a string property with custom builder (required) pub fn string_property( mut self, name: impl Into, f: impl FnOnce(StringSchema) -> StringSchema, ) -> Self { self.properties.insert( name.into(), PrimitiveSchemaDefinition::String(f(StringSchema::new())), ); self } /// Add a required string property with custom builder pub fn required_string_property( mut self, name: impl Into, f: impl FnOnce(StringSchema) -> StringSchema, ) -> Self { let name_str = name.into(); self.required.push(name_str.clone()); self.properties.insert( name_str, PrimitiveSchemaDefinition::String(f(StringSchema::new())), ); self } /// Add a number property with custom builder pub fn number_property( mut self, name: impl Into, f: impl FnOnce(NumberSchema) -> NumberSchema, ) -> Self { self.properties.insert( name.into(), PrimitiveSchemaDefinition::Number(f(NumberSchema::new())), ); self } /// Add a required number property with custom builder pub fn required_number_property( mut self, name: impl Into, f: impl FnOnce(NumberSchema) -> NumberSchema, ) -> Self { let name_str = name.into(); self.required.push(name_str.clone()); self.properties.insert( name_str, PrimitiveSchemaDefinition::Number(f(NumberSchema::new())), ); self } /// Add an integer property with custom builder pub fn integer_property( mut self, name: impl Into, f: impl FnOnce(IntegerSchema) -> IntegerSchema, ) -> Self { self.properties.insert( name.into(), PrimitiveSchemaDefinition::Integer(f(IntegerSchema::new())), ); self } /// Add a required integer property with custom builder pub fn required_integer_property( mut self, name: impl Into, f: impl FnOnce(IntegerSchema) -> IntegerSchema, ) -> Self { let name_str = name.into(); self.required.push(name_str.clone()); self.properties.insert( name_str, PrimitiveSchemaDefinition::Integer(f(IntegerSchema::new())), ); self } /// Add a boolean property with custom builder pub fn bool_property( mut self, name: impl Into, f: impl FnOnce(BooleanSchema) -> BooleanSchema, ) -> Self { self.properties.insert( name.into(), PrimitiveSchemaDefinition::Boolean(f(BooleanSchema::new())), ); self } /// Add a required boolean property with custom builder pub fn required_bool_property( mut self, name: impl Into, f: impl FnOnce(BooleanSchema) -> BooleanSchema, ) -> Self { let name_str = name.into(); self.required.push(name_str.clone()); self.properties.insert( name_str, PrimitiveSchemaDefinition::Boolean(f(BooleanSchema::new())), ); self } // =========================================================================== // CONVENIENCE METHODS - Simple common cases // =========================================================================== /// Add a required string property pub fn required_string(self, name: impl Into) -> Self { self.required_property(name, PrimitiveSchemaDefinition::String(StringSchema::new())) } /// Add an optional string property pub fn optional_string(self, name: impl Into) -> Self { self.property(name, PrimitiveSchemaDefinition::String(StringSchema::new())) } /// Add a required email property pub fn required_email(self, name: impl Into) -> Self { self.required_property( name, PrimitiveSchemaDefinition::String(StringSchema::email()), ) } /// Add an optional email property pub fn optional_email(self, name: impl Into) -> Self { self.property( name, PrimitiveSchemaDefinition::String(StringSchema::email()), ) } /// Add a required string property with custom builder pub fn required_string_with( self, name: impl Into, f: impl FnOnce(StringSchema) -> StringSchema, ) -> Self { self.required_property( name, PrimitiveSchemaDefinition::String(f(StringSchema::new())), ) } /// Add an optional string property with custom builder pub fn optional_string_with( self, name: impl Into, f: impl FnOnce(StringSchema) -> StringSchema, ) -> Self { self.property( name, PrimitiveSchemaDefinition::String(f(StringSchema::new())), ) } // Convenience methods for numbers /// Add a required number property with range pub fn required_number(self, name: impl Into, min: f64, max: f64) -> Self { self.required_property( name, PrimitiveSchemaDefinition::Number(NumberSchema::new().range(min, max)), ) } /// Add an optional number property with range pub fn optional_number(self, name: impl Into, min: f64, max: f64) -> Self { self.property( name, PrimitiveSchemaDefinition::Number(NumberSchema::new().range(min, max)), ) } /// Add a required number property with custom builder pub fn required_number_with( self, name: impl Into, f: impl FnOnce(NumberSchema) -> NumberSchema, ) -> Self { self.required_property( name, PrimitiveSchemaDefinition::Number(f(NumberSchema::new())), ) } /// Add an optional number property with custom builder pub fn optional_number_with( self, name: impl Into, f: impl FnOnce(NumberSchema) -> NumberSchema, ) -> Self { self.property( name, PrimitiveSchemaDefinition::Number(f(NumberSchema::new())), ) } // Convenience methods for integers /// Add a required integer property with range pub fn required_integer(self, name: impl Into, min: i64, max: i64) -> Self { self.required_property( name, PrimitiveSchemaDefinition::Integer(IntegerSchema::new().range(min, max)), ) } /// Add an optional integer property with range pub fn optional_integer(self, name: impl Into, min: i64, max: i64) -> Self { self.property( name, PrimitiveSchemaDefinition::Integer(IntegerSchema::new().range(min, max)), ) } /// Add a required integer property with custom builder pub fn required_integer_with( self, name: impl Into, f: impl FnOnce(IntegerSchema) -> IntegerSchema, ) -> Self { self.required_property( name, PrimitiveSchemaDefinition::Integer(f(IntegerSchema::new())), ) } /// Add an optional integer property with custom builder pub fn optional_integer_with( self, name: impl Into, f: impl FnOnce(IntegerSchema) -> IntegerSchema, ) -> Self { self.property( name, PrimitiveSchemaDefinition::Integer(f(IntegerSchema::new())), ) } // Convenience methods for booleans /// Add a required boolean property pub fn required_bool(self, name: impl Into) -> Self { self.required_property( name, PrimitiveSchemaDefinition::Boolean(BooleanSchema::new()), ) } /// Add an optional boolean property with default value pub fn optional_bool(self, name: impl Into, default: bool) -> Self { self.property( name, PrimitiveSchemaDefinition::Boolean(BooleanSchema::new().with_default(default)), ) } /// Add a required boolean property with custom builder pub fn required_bool_with( self, name: impl Into, f: impl FnOnce(BooleanSchema) -> BooleanSchema, ) -> Self { self.required_property( name, PrimitiveSchemaDefinition::Boolean(f(BooleanSchema::new())), ) } /// Add an optional boolean property with custom builder pub fn optional_bool_with( self, name: impl Into, f: impl FnOnce(BooleanSchema) -> BooleanSchema, ) -> Self { self.property( name, PrimitiveSchemaDefinition::Boolean(f(BooleanSchema::new())), ) } // Enum convenience methods /// Add a required enum property using EnumSchema pub fn required_enum_schema(self, name: impl Into, enum_schema: EnumSchema) -> Self { self.required_property(name, PrimitiveSchemaDefinition::Enum(enum_schema)) } /// Add an optional enum property using EnumSchema pub fn optional_enum_schema(self, name: impl Into, enum_schema: EnumSchema) -> Self { self.property(name, PrimitiveSchemaDefinition::Enum(enum_schema)) } /// Add a required enum property using values. Creates an untitled single-select enum. #[deprecated( since = "0.13.0", note = "Use ElicitationSchemaBuilder::required_enum_schema with EnumSchema::builder instead" )] pub fn required_enum(self, name: impl Into, values: Vec) -> Self { self.required_property( name, PrimitiveSchemaDefinition::Enum(EnumSchema::Legacy(LegacyEnumSchema { type_: StringTypeConst, title: None, description: None, enum_: values, enum_names: None, default: None, })), ) } /// Add an optional enum property using values. Creates an untitled single-select enum. #[deprecated( since = "0.13.0", note = "Use ElicitationSchemaBuilder::optional_enum_schema with EnumSchema::builder instead" )] pub fn optional_enum(self, name: impl Into, values: Vec) -> Self { self.property( name, PrimitiveSchemaDefinition::Enum(EnumSchema::Legacy(LegacyEnumSchema { type_: StringTypeConst, title: None, description: None, enum_: values, enum_names: None, default: None, })), ) } /// Mark an existing property as required pub fn mark_required(mut self, name: impl Into) -> Self { self.required.push(name.into()); self } /// Set the schema title pub fn title(mut self, title: impl Into>) -> Self { self.title = Some(title.into()); self } /// Set the schema description pub fn description(mut self, description: impl Into>) -> Self { self.description = Some(description.into()); self } /// Build the elicitation schema with validation /// /// # Errors /// /// Returns an error if: /// - Required fields reference non-existent properties /// - No properties are defined (empty schema) pub fn build(self) -> Result { // Validate that all required fields exist in properties if !self.required.is_empty() { for field_name in &self.required { if !self.properties.contains_key(field_name) { return Err("Required field does not exist in properties"); } } } Ok(ElicitationSchema { type_: ObjectTypeConst, title: self.title, properties: self.properties, required: if self.required.is_empty() { None } else { Some(self.required) }, description: self.description, }) } /// Build the elicitation schema without validation (panics on invalid schema) /// /// # Panics /// /// Panics if required fields reference non-existent properties pub fn build_unchecked(self) -> ElicitationSchema { self.build().expect("Invalid elicitation schema") } } #[cfg(test)] mod tests { use anyhow::anyhow; use rstest::rstest; use serde_json::json; use super::*; fn string_schema_json() -> serde_json::Value { serde_json::to_value(StringSchema::email().description("Email address")).unwrap() } fn number_schema_json() -> serde_json::Value { serde_json::to_value( NumberSchema::new() .range(0.0, 100.0) .description("Percentage"), ) .unwrap() } fn integer_schema_json() -> serde_json::Value { serde_json::to_value(IntegerSchema::new().range(0, 150)).unwrap() } fn boolean_schema_json() -> serde_json::Value { serde_json::to_value(BooleanSchema::new().with_default(true)).unwrap() } #[rstest] #[case::string_schema( string_schema_json, json!({ "type": "string", "format": "email", "description": "Email address", }) )] #[case::number_schema( number_schema_json, json!({ "type": "number", "description": "Percentage", "minimum": 0.0, "maximum": 100.0, }) )] #[case::integer_schema( integer_schema_json, json!({ "type": "integer", "minimum": 0, "maximum": 150, }) )] #[case::boolean_schema( boolean_schema_json, json!({ "type": "boolean", "default": true, }) )] fn primitive_schema_serializes_to_expected_json( #[case] schema_json: fn() -> serde_json::Value, #[case] expected: serde_json::Value, ) { assert_eq!(schema_json(), expected); } #[test] fn test_enum_schema_untitled_single_select_serialization() { let schema = EnumSchema::builder(vec!["US".to_string(), "UK".to_string()]) .description("Country code") .build(); let json = serde_json::to_value(&schema).unwrap(); assert_eq!(json["type"], "string"); assert_eq!(json["enum"], json!(["US", "UK"])); assert_eq!(json["description"], "Country code"); } #[test] fn test_enum_schema_untitled_multi_select_serialization() -> anyhow::Result<()> { let schema = EnumSchema::builder(vec!["US".to_string(), "UK".to_string()]) .multiselect() .min_items(1u64) .map_err(|e| anyhow!("{e}"))? .max_items(4u64) .map_err(|e| anyhow!("{e}"))? .description("Country code") .build(); let json = serde_json::to_value(&schema)?; assert_eq!(json["type"], "array"); assert_eq!(json["minItems"], 1u64); assert_eq!(json["maxItems"], 4u64); assert_eq!(json["items"], json!({"type":"string", "enum":["US", "UK"]})); assert_eq!(json["description"], "Country code"); Ok(()) } #[test] fn test_enum_schema_titled_single_select_serialization() -> anyhow::Result<()> { let schema = EnumSchema::builder(vec!["US".to_string(), "UK".to_string()]) .enum_titles(vec![ "United States".to_string(), "United Kingdom".to_string(), ]) .map_err(|e| anyhow!("{e}"))? .description("Country code") .build(); let json = serde_json::to_value(&schema)?; assert_eq!(json["type"], "string"); assert_eq!( json["oneOf"], json!([ {"const": "US", "title":"United States"}, {"const": "UK", "title":"United Kingdom"} ]) ); assert_eq!(json["description"], "Country code"); Ok(()) } #[test] fn test_enum_schema_legacy_serialization() -> anyhow::Result<()> { let schema = EnumSchema::Legacy(LegacyEnumSchema { type_: StringTypeConst, title: Some("Legacy Enum".into()), description: Some("A legacy enum schema".into()), enum_: vec!["A".to_string(), "B".to_string()], enum_names: Some(vec!["Option A".to_string(), "Option B".to_string()]), default: None, }); let json = serde_json::to_value(&schema)?; assert_eq!(json["type"], "string"); assert_eq!(json["title"], "Legacy Enum"); assert_eq!(json["description"], "A legacy enum schema"); assert_eq!(json["enum"], json!(["A", "B"])); assert_eq!(json["enumNames"], json!(["Option A", "Option B"])); Ok(()) } #[test] fn test_legacy_enum_schema_roundtrip_preserves_enum_names() -> anyhow::Result<()> { // Regression test for: legacy enum payload with `enumNames` was silently // deserialized as `UntitledSingleSelectEnumSchema` (which has no `enumNames` // field), causing the array to be dropped on re-serialization. let input = serde_json::json!({ "type": "object", "properties": { "choice": { "type": "string", "enum": ["opt1", "opt2", "opt3"], "enumNames": ["Option One", "Option Two", "Option Three"] } } }); let schema: ElicitationSchema = serde_json::from_value(input.clone())?; let output = serde_json::to_value(&schema)?; assert_eq!( output["properties"]["choice"]["enumNames"], serde_json::json!(["Option One", "Option Two", "Option Three"]), ); Ok(()) } #[test] fn test_legacy_enum_schema_no_enum_names_omits_field() -> anyhow::Result<()> { // `LegacyEnumSchema` with `enum_names: None` must not serialize `"enumNames": null`. let schema = EnumSchema::Legacy(LegacyEnumSchema { type_: StringTypeConst, title: None, description: None, enum_: vec!["a".to_string(), "b".to_string()], enum_names: None, default: None, }); let json = serde_json::to_value(&schema)?; assert!(!json.as_object().unwrap().contains_key("enumNames")); Ok(()) } #[test] fn test_enum_schema_titled_multi_select_serialization() -> anyhow::Result<()> { let schema = EnumSchema::builder(vec!["US".to_string(), "UK".to_string()]) .enum_titles(vec![ "United States".to_string(), "United Kingdom".to_string(), ]) .map_err(|e| anyhow!("{e}"))? .multiselect() .min_items(1u64) .map_err(|e| anyhow!("{e}"))? .max_items(4u64) .map_err(|e| anyhow!("{e}"))? .description("Country code") .build(); let json = serde_json::to_value(&schema)?; assert_eq!(json["type"], "array"); assert_eq!(json["minItems"], 1u64); assert_eq!(json["maxItems"], 4u64); assert_eq!( json["items"], json!({"anyOf":[ {"const":"US","title":"United States"}, {"const":"UK","title":"United Kingdom"} ]}) ); assert_eq!(json["description"], "Country code"); Ok(()) } #[test] fn test_enum_schema_single_select_with_default() -> anyhow::Result<()> { let schema = EnumSchema::builder(vec![ "red".to_string(), "green".to_string(), "blue".to_string(), ]) .with_default("green") .map_err(|e| anyhow!("{e}"))? .description("Favorite color") .build(); let json = serde_json::to_value(&schema)?; assert_eq!(json["type"], "string"); assert_eq!(json["enum"], json!(["red", "green", "blue"])); assert_eq!(json["default"], "green"); assert_eq!(json["description"], "Favorite color"); Ok(()) } #[test] fn test_enum_schema_multi_select_with_default() -> anyhow::Result<()> { let schema = EnumSchema::builder(vec![ "red".to_string(), "green".to_string(), "blue".to_string(), ]) .multiselect() .with_default(vec!["red".to_string(), "blue".to_string()]) .map_err(|e| anyhow!("{e}"))? .min_items(1) .map_err(|e| anyhow!("{e}"))? .max_items(3) .map_err(|e| anyhow!("{e}"))? .build(); let json = serde_json::to_value(&schema)?; assert_eq!(json["type"], "array"); assert_eq!(json["items"]["enum"], json!(["red", "green", "blue"])); assert_eq!(json["default"], json!(["red", "blue"])); assert_eq!(json["minItems"], 1); assert_eq!(json["maxItems"], 3); Ok(()) } #[test] fn test_enum_schema_transition_clears_defaults() -> anyhow::Result<()> { // Start with single-select with default let builder = EnumSchema::builder(vec!["A".to_string(), "B".to_string()]) .with_default("A") .map_err(|e| anyhow!("{e}"))?; // Transition to multi-select should clear the default let schema = builder.multiselect().build(); let json = serde_json::to_value(&schema)?; assert_eq!(json["type"], "array"); assert!(json["default"].is_null()); Ok(()) } #[test] fn test_enum_schema_multi_to_single_transition() -> anyhow::Result<()> { // Start with multi-select with defaults let builder = EnumSchema::builder(vec!["A".to_string(), "B".to_string(), "C".to_string()]) .multiselect() .with_default(vec!["A".to_string(), "B".to_string()]) .map_err(|e| anyhow!("{e}"))? .min_items(1) .map_err(|e| anyhow!("{e}"))?; // Transition back to single-select should clear defaults and min/max items let schema = builder.single_select().build(); let json = serde_json::to_value(&schema)?; assert_eq!(json["type"], "string"); assert!(json["default"].is_null()); assert!(json["minItems"].is_null()); assert!(json["maxItems"].is_null()); Ok(()) } #[test] fn test_enum_schema_invalid_single_default() { let result = EnumSchema::builder(vec!["A".to_string(), "B".to_string()]).with_default("C"); assert!(result.is_err()); assert_eq!( result.unwrap_err(), "Provided default value is not in enum values" ); } #[test] fn test_enum_schema_invalid_multi_default() { let result = EnumSchema::builder(vec!["A".to_string(), "B".to_string()]) .multiselect() .with_default(vec!["A".to_string(), "C".to_string()]); assert!(result.is_err()); assert_eq!( result.unwrap_err(), "One of the provided default values is not in enum values" ); } #[test] fn test_enum_schema_titled_with_default() -> anyhow::Result<()> { let schema = EnumSchema::builder(vec!["US".to_string(), "UK".to_string()]) .enum_titles(vec![ "United States".to_string(), "United Kingdom".to_string(), ]) .map_err(|e| anyhow!("{e}"))? .with_default("UK") .map_err(|e| anyhow!("{e}"))? .build(); let json = serde_json::to_value(&schema)?; assert_eq!(json["type"], "string"); assert_eq!(json["default"], "UK"); assert_eq!( json["oneOf"], json!([ {"const": "US", "title": "United States"}, {"const": "UK", "title": "United Kingdom"} ]) ); Ok(()) } #[test] fn test_enum_schema_untitled_after_titled() -> anyhow::Result<()> { let schema = EnumSchema::builder(vec!["A".to_string(), "B".to_string()]) .enum_titles(vec!["Option A".to_string(), "Option B".to_string()]) .map_err(|e| anyhow!("{e}"))? .untitled() .build(); let json = serde_json::to_value(&schema)?; assert_eq!(json["type"], "string"); assert_eq!(json["enum"], json!(["A", "B"])); assert!(json["oneOf"].is_null()); Ok(()) } #[test] fn test_primitive_schema_enum_deserialization() { // Test that enum schemas deserialize as Enum variant, not String let json = json!({ "type": "string", "enum": ["a", "b"] }); let schema: PrimitiveSchemaDefinition = serde_json::from_value(json).unwrap(); assert!(matches!(schema, PrimitiveSchemaDefinition::Enum(_))); // Test that string schemas deserialize as String variant let json = json!({ "type": "string" }); let schema: PrimitiveSchemaDefinition = serde_json::from_value(json).unwrap(); assert!(matches!(schema, PrimitiveSchemaDefinition::String(_))); } #[test] fn test_elicitation_schema_builder_simple() { let schema = ElicitationSchema::builder() .required_email("email") .optional_bool("newsletter", false) .build() .unwrap(); assert_eq!(schema.properties.len(), 2); assert!(schema.properties.contains_key("email")); assert!(schema.properties.contains_key("newsletter")); assert_eq!(schema.required, Some(vec!["email".to_string()])); } #[test] fn test_elicitation_schema_builder_complex() { let enum_schema = EnumSchema::builder(vec!["US".to_string(), "UK".to_string(), "CA".to_string()]).build(); let schema = ElicitationSchema::builder() .required_string_with("name", |s| s.length(1, 100)) .required_integer("age", 0, 150) .optional_bool("newsletter", false) .required_enum_schema("country", enum_schema) .description("User registration") .build() .unwrap(); assert_eq!(schema.properties.len(), 4); assert_eq!( schema.required, Some(vec![ "name".to_string(), "age".to_string(), "country".to_string() ]) ); assert_eq!(schema.description.as_deref(), Some("User registration")); } #[test] fn test_elicitation_schema_serialization() { let schema = ElicitationSchema::builder() .required_string_with("name", |s| s.length(1, 100)) .build() .unwrap(); let json = serde_json::to_value(&schema).unwrap(); assert_eq!(json["type"], "object"); assert!(json["properties"]["name"].is_object()); assert_eq!(json["required"], json!(["name"])); } #[test] #[should_panic(expected = "minimum must be <= maximum")] fn test_integer_range_validation() { IntegerSchema::new().range(10, 5); // Should panic } #[test] #[should_panic(expected = "min_length must be <= max_length")] fn test_string_length_validation() { StringSchema::new().length(10, 5); // Should panic } #[test] fn test_integer_range_validation_with_result() { let result = IntegerSchema::new().with_range(10, 5); assert!(result.is_err()); assert_eq!(result.unwrap_err(), "minimum must be <= maximum"); } #[cfg(feature = "schemars")] mod schemars_tests { use anyhow::Result; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_json::json; use crate::model::ElicitationSchema; #[derive(Debug, Serialize, Deserialize, JsonSchema, Default)] #[schemars(inline)] #[schemars(extend("type" = "string"))] enum TitledEnum { #[schemars(title = "Title for the first value")] #[default] FirstValue, #[schemars(title = "Title for the second value")] SecondValue, } #[derive(Debug, Serialize, Deserialize, JsonSchema)] #[schemars(inline)] enum UntitledEnum { First, Second, Third, } fn default_untitled_multi_select() -> Vec { vec![UntitledEnum::Second, UntitledEnum::Third] } #[derive(Debug, Serialize, Deserialize, JsonSchema)] #[schemars(description = "User information")] struct UserInfo { #[schemars(description = "User's name")] pub name: String, pub single_select_untitled: UntitledEnum, #[schemars( title = "Single Select Titled", description = "Description for single select enum", default )] pub single_select_titled: TitledEnum, #[serde(default = "default_untitled_multi_select")] pub multi_select_untitled: Vec, #[schemars( title = "Multi Select Titled", description = "Multi Select Description" )] pub multi_select_titled: Vec, } #[test] fn test_schema_inference_for_enum_fields() -> Result<()> { let schema = ElicitationSchema::from_type::()?; let json = serde_json::to_value(&schema)?; assert_eq!(json["type"], "object"); assert_eq!(json["description"], "User information"); assert_eq!( json["required"], json!(["name", "single_select_untitled", "multi_select_titled"]) ); let properties = match json.get("properties") { Some(serde_json::Value::Object(map)) => map, _ => panic!("Schema does not have 'properties' field or it is not object type"), }; assert_eq!(properties.len(), 5); assert_eq!( properties["name"], json!({"description":"User's name", "type":"string"}) ); assert_eq!( properties["single_select_untitled"], serde_json::json!({ "type":"string", "enum":["First", "Second", "Third"] }) ); assert_eq!( properties["single_select_titled"], json!({ "type":"string", "title":"Single Select Titled", "description":"Description for single select enum", "oneOf":[ {"const":"FirstValue", "title":"Title for the first value"}, {"const":"SecondValue", "title":"Title for the second value"} ], "default":"FirstValue" }) ); assert_eq!( properties["multi_select_untitled"], serde_json::json!({ "type":"array", "items" : { "type":"string", "enum":["First", "Second", "Third"] }, "default":["Second", "Third"] }) ); assert_eq!( properties["multi_select_titled"], serde_json::json!({ "type":"array", "title":"Multi Select Titled", "description":"Multi Select Description", "items":{ "anyOf":[ {"const":"FirstValue", "title":"Title for the first value"}, {"const":"SecondValue", "title":"Title for the second value"} ] } }) ); Ok(()) } } }