feat(elicitation): implement SEP-1330 Elicitation Enum Schema Improvements (#539)

This commit is contained in:
Pavel Bezglasny 2025-12-24 19:54:20 +01:00 • committed by GitHub
commit e9029ccc99
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -32,6 +32,7 @@ const_string!(NumberTypeConst = "number");
const_string!(IntegerTypeConst = "integer");
const_string!(BooleanTypeConst = "boolean");
const_string!(EnumTypeConst = "string");
const_string!(ArrayTypeConst = "array");
// =============================================================================
// PRIMITIVE SCHEMA DEFINITIONS
@ -41,10 +42,16 @@ const_string!(EnumTypeConst = "string");
///
/// 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)]
pub enum PrimitiveSchema {
/// Enum property (explicit enum schema)
Enum(EnumSchema),
/// String property (with optional enum constraint)
String(StringSchema),
/// Number property (with optional enum constraint)
@ -53,8 +60,6 @@ pub enum PrimitiveSchema {
Integer(IntegerSchema),
/// Boolean property
Boolean(BooleanSchema),
/// Enum property (explicit enum schema)
Enum(EnumSchema),
}
// =============================================================================
@ -466,62 +471,449 @@ impl BooleanSchema {
/// Schema definition for enum properties.
///
/// Compliant with MCP 2025-06-18 specification for elicitation schemas.
/// Enums must have string type and can optionally include human-readable names.
/// Represent single entry for titled item
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct ConstTitle {
#[serde(rename = "const")]
pub const_: String,
pub title: String,
}
/// Legacy enum schema, keep for backward compatibility
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct LegacyEnumSchema {
#[serde(rename = "type")]
pub type_: StringTypeConst,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<Cow<'static, str>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<Cow<'static, str>>,
#[serde(rename = "enum")]
pub enum_: Vec<String>,
pub enum_names: Option<Vec<String>>,
}
/// Untitled single-select
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct UntitledSingleSelectEnumSchema {
#[serde(rename = "type")]
pub type_: StringTypeConst,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<Cow<'static, str>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<Cow<'static, str>>,
#[serde(rename = "enum")]
pub enum_: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<String>,
}
/// Titled single-select
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct TitledSingleSelectEnumSchema {
#[serde(rename = "type")]
pub type_: StringTypeConst,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<Cow<'static, str>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<Cow<'static, str>>,
#[serde(rename = "oneOf")]
pub one_of: Vec<ConstTitle>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<String>,
}
/// Combined single-select
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(untagged)]
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))]
pub struct UntitledItems {
#[serde(rename = "type")]
pub type_: StringTypeConst,
#[serde(rename = "enum")]
pub enum_: Vec<String>,
}
/// Items for titled multi-select options
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
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<ConstTitle>,
}
/// Multi-select untitled options
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct EnumSchema {
/// Type discriminator (always "string" for enums)
pub struct UntitledMultiSelectEnumSchema {
#[serde(rename = "type")]
pub type_: StringTypeConst,
/// Allowed enum values (string values only per MCP spec)
#[serde(rename = "enum")]
pub enum_values: Vec<String>,
/// Optional human-readable names for each enum value
#[serde(skip_serializing_if = "Option::is_none")]
pub enum_names: Option<Vec<String>>,
/// Optional title for the schema
pub type_: ArrayTypeConst,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<Cow<'static, str>>,
/// Human-readable description
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<Cow<'static, str>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub min_items: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_items: Option<u64>,
pub items: UntitledItems,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<Vec<String>>,
}
impl EnumSchema {
/// Create a new enum schema with string values
pub fn new(values: Vec<String>) -> Self {
/// Multi-select titled options
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct TitledMultiSelectEnumSchema {
#[serde(rename = "type")]
pub type_: ArrayTypeConst,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<Cow<'static, str>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<Cow<'static, str>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub min_items: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_items: Option<u64>,
pub items: TitledItems,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<Vec<String>>,
}
/// Multi-select enum options
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(untagged)]
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)]
pub enum EnumSchema {
Single(SingleSelectEnumSchema),
Multi(MultiSelectEnumSchema),
Legacy(LegacyEnumSchema),
}
/// 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<String>,
/// If true generate SingleSelect EnumSchema, MultiSelect otherwise
single_select: bool,
/// If true generate Titled EnumSchema, UnTitled otherwise
titled: bool,
/// Title of EnumSchema
title: Option<Cow<'static, str>>,
/// Description of EnumSchema
description: Option<Cow<'static, str>>,
/// Titles of given enum values
enum_titles: Vec<String>,
/// Minimum number of items to choose for MultiSelect
min_items: Option<u64>,
/// Maximum number of items to choose for MultiSelect
max_items: Option<u64>,
/// Default values for enum
default: Vec<String>,
}
impl Default for EnumSchemaBuilder {
fn default() -> Self {
Self {
type_: StringTypeConst,
enum_values: values,
enum_names: None,
title: None,
description: None,
single_select: true,
titled: false,
enum_titles: Vec::new(),
enum_values: Vec::new(),
min_items: None,
max_items: None,
default: Vec::new(),
}
}
}
/// Enum selection builder
impl EnumSchemaBuilder {
pub fn new(values: Vec<String>) -> EnumSchemaBuilder {
EnumSchemaBuilder {
enum_values: values,
single_select: true,
titled: false,
..Default::default()
}
}
/// Set enum names (human-readable names for each enum value)
pub fn enum_names(mut self, names: Vec<String>) -> Self {
self.enum_names = Some(names);
/// Set titles to enum values. Also, implicitly set this enum schema as titled
pub fn enum_titles(mut self, titles: Vec<String>) -> Result<EnumSchemaBuilder, 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)
}
/// Set enum as single-select
/// If it was multi-select, clear default values
pub fn single_select(mut self) -> EnumSchemaBuilder {
if !self.single_select {
self.default = Vec::new();
}
self.single_select = true;
self
}
/// Set title
pub fn title(mut self, title: impl Into<Cow<'static, str>>) -> Self {
self.title = Some(title.into());
/// Set enum as multi-select
/// If it was single-select, clear default value
pub fn multiselect(mut self) -> EnumSchemaBuilder {
if self.single_select {
self.default = Vec::new();
}
self.single_select = false;
self
}
/// Set description
pub fn description(mut self, description: impl Into<Cow<'static, str>>) -> Self {
self.description = Some(description.into());
/// Set enum as untitled
/// Clears any previously set titles
pub fn untitled(mut self) -> EnumSchemaBuilder {
self.enum_titles = Vec::new();
self.titled = false;
self
}
/// Set default value for single-select enum
pub fn single_select_default(
mut self,
default_value: String,
) -> Result<EnumSchemaBuilder, String> {
if !self.enum_values.contains(&default_value) {
return Err("Provided default value is not in enum values".to_string());
}
if !self.single_select {
return Err(
"Set single default value available only when the builder is set to single-select. \
Use multi_select_default method for multi-select options".to_string(),
);
}
self.default = vec![default_value];
Ok(self)
}
/// Set default value for multi-select enum
pub fn multi_select_default(
mut self,
default_values: Vec<String>,
) -> Result<EnumSchemaBuilder, String> {
if self.single_select {
return Err(
"Set multiple default values available only when the builder is set to multi-select. \
Use single_select_default method for single-select options".to_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<EnumSchemaBuilder, 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<EnumSchemaBuilder, 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)
}
/// Set title of enum schema
pub fn title(mut self, value: impl Into<Cow<'static, str>>) -> Self {
self.title = Some(value.into());
self
}
/// Set description of enum schema
pub fn description(mut self, value: impl Into<Cow<'static, str>>) -> Self {
self.description = Some(value.into());
self
}
/// Build enum schema
pub fn build(mut self) -> EnumSchema {
match (self.single_select, self.titled) {
(true, false) => EnumSchema::Single(SingleSelectEnumSchema::Untitled(
UntitledSingleSelectEnumSchema {
type_: StringTypeConst,
title: self.title,
description: self.description,
enum_: self.enum_values,
default: self.default.pop(),
},
)),
(true, 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(),
},
)),
(false, 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)
},
},
)),
(false, 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 builder = EnumSchema::builder(vec!["A".to_string(), "B".to_string()]);
/// ```
pub fn builder(values: Vec<String>) -> EnumSchemaBuilder {
EnumSchemaBuilder::new(values)
}
}
// =============================================================================
@ -971,14 +1363,38 @@ impl ElicitationSchemaBuilder {
// Enum convenience methods
/// Add a required enum property
pub fn required_enum(self, name: impl Into<String>, values: Vec<String>) -> Self {
self.required_property(name, PrimitiveSchema::Enum(EnumSchema::new(values)))
/// Add a required enum property using EnumSchema
pub fn required_enum_schema(self, name: impl Into<String>, enum_schema: EnumSchema) -> Self {
self.required_property(name, PrimitiveSchema::Enum(enum_schema))
}
/// Add an optional enum property
/// Add an optional enum property using EnumSchema
pub fn optional_enum_schema(self, name: impl Into<String>, enum_schema: EnumSchema) -> Self {
self.property(name, PrimitiveSchema::Enum(enum_schema))
}
/// Add a required enum property using values. Creates an untitled single-select enum.
#[deprecated(
since = "0.12.0",
note = "Use ElicitationSchemaBuilder::required_enum_schema with EnumSchema::builder instead"
)]
pub fn required_enum(self, name: impl Into<String>, values: Vec<String>) -> Self {
self.required_property(
name,
PrimitiveSchema::Enum(EnumSchema::builder(values).build()),
)
}
/// Add an optional enum property using values. Creates an untitled single-select enum.
#[deprecated(
since = "0.12.0",
note = "Use ElicitationSchemaBuilder::optional_enum_schema with EnumSchema::builder instead"
)]
pub fn optional_enum(self, name: impl Into<String>, values: Vec<String>) -> Self {
self.property(name, PrimitiveSchema::Enum(EnumSchema::new(values)))
self.property(
name,
PrimitiveSchema::Enum(EnumSchema::builder(values).build()),
)
}
/// Mark an existing property as required
@ -1041,6 +1457,7 @@ impl ElicitationSchemaBuilder {
#[cfg(test)]
mod tests {
use anyhow::anyhow;
use serde_json::json;
use super::*;
@ -1087,22 +1504,107 @@ mod tests {
}
#[test]
fn test_enum_schema_serialization() {
let schema = EnumSchema::new(vec!["US".to_string(), "UK".to_string()])
.enum_names(vec![
"United States".to_string(),
"United Kingdom".to_string(),
])
.description("Country code");
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["enumNames"],
json!(["United States", "United Kingdom"])
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_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_primitive_schema_enum_deserialization() {
// Test that enum schemas deserialize as Enum variant, not String
let json = json!({
"type": "string",
"enum": ["a", "b"]
});
let schema: PrimitiveSchema = serde_json::from_value(json).unwrap();
assert!(matches!(schema, PrimitiveSchema::Enum(_)));
// Test that string schemas deserialize as String variant
let json = json!({
"type": "string"
});
let schema: PrimitiveSchema = serde_json::from_value(json).unwrap();
assert!(matches!(schema, PrimitiveSchema::String(_)));
}
#[test]
@ -1121,14 +1623,13 @@ mod tests {
#[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(
"country",
vec!["US".to_string(), "UK".to_string(), "CA".to_string()],
)
.required_enum_schema("country", enum_schema)
.description("User registration")
.build()
.unwrap();
@ -1177,4 +1678,129 @@ mod tests {
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<UntitledEnum> {
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<UntitledEnum>,
#[schemars(
title = "Multi Select Titled",
description = "Multi Select Description"
)]
pub multi_select_titled: Vec<TitledEnum>,
}
#[test]
fn test_schema_inference_for_enum_fields() -> Result<()> {
let schema = ElicitationSchema::from_type::<UserInfo>()?;
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(())
}
}
}