use serde::{Deserialize, Serialize};
#[cfg(not(feature = "std"))]
use alloc::{
collections::BTreeMap as HashMap,
string::{String, ToString},
vec::Vec,
};
#[cfg(feature = "std")]
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ElicitationSchema {
#[serde(rename = "type")]
pub schema_type: String,
pub properties: HashMap<String, PrimitiveSchemaDefinition>,
#[serde(skip_serializing_if = "Option::is_none")]
pub required: Option<Vec<String>>,
#[serde(
rename = "additionalProperties",
skip_serializing_if = "Option::is_none"
)]
pub additional_properties: Option<bool>,
}
impl ElicitationSchema {
#[must_use]
pub fn new() -> Self {
Self {
schema_type: "object".to_string(),
properties: HashMap::new(),
required: Some(Vec::new()),
additional_properties: Some(false),
}
}
#[must_use]
pub fn add_string_property(
mut self,
name: String,
required: bool,
description: Option<String>,
) -> Self {
let property = PrimitiveSchemaDefinition::String {
title: None,
description,
format: None,
min_length: None,
max_length: None,
enum_values: None,
enum_names: None,
};
self.properties.insert(name.clone(), property);
if required && let Some(required_fields) = self.required.as_mut() {
required_fields.push(name);
}
self
}
#[must_use]
pub fn add_number_property(
mut self,
name: String,
required: bool,
description: Option<String>,
minimum: Option<f64>,
maximum: Option<f64>,
) -> Self {
let property = PrimitiveSchemaDefinition::Number {
title: None,
description,
minimum,
maximum,
};
self.properties.insert(name.clone(), property);
if required && let Some(required_fields) = self.required.as_mut() {
required_fields.push(name);
}
self
}
#[must_use]
pub fn add_boolean_property(
mut self,
name: String,
required: bool,
description: Option<String>,
default: Option<bool>,
) -> Self {
let property = PrimitiveSchemaDefinition::Boolean {
title: None,
description,
default,
};
self.properties.insert(name.clone(), property);
if required && let Some(required_fields) = self.required.as_mut() {
required_fields.push(name);
}
self
}
}
impl Default for ElicitationSchema {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type")]
pub enum PrimitiveSchemaDefinition {
#[serde(rename = "string")]
String {
#[serde(skip_serializing_if = "Option::is_none")]
title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
format: Option<String>,
#[serde(rename = "minLength", skip_serializing_if = "Option::is_none")]
min_length: Option<u32>,
#[serde(rename = "maxLength", skip_serializing_if = "Option::is_none")]
max_length: Option<u32>,
#[serde(rename = "enum", skip_serializing_if = "Option::is_none")]
enum_values: Option<Vec<String>>,
#[serde(rename = "enumNames", skip_serializing_if = "Option::is_none")]
enum_names: Option<Vec<String>>,
},
#[serde(rename = "number")]
Number {
#[serde(skip_serializing_if = "Option::is_none")]
title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
minimum: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
maximum: Option<f64>,
},
#[serde(rename = "integer")]
Integer {
#[serde(skip_serializing_if = "Option::is_none")]
title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
minimum: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
maximum: Option<i64>,
},
#[serde(rename = "boolean")]
Boolean {
#[serde(skip_serializing_if = "Option::is_none")]
title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
default: Option<bool>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct EnumOption {
#[serde(rename = "const")]
pub const_value: String,
pub title: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TitledSingleSelectEnumSchema {
#[serde(rename = "type")]
pub schema_type: String,
#[serde(rename = "oneOf")]
pub one_of: Vec<EnumOption>,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct UntitledSingleSelectEnumSchema {
#[serde(rename = "type")]
pub schema_type: String,
#[serde(rename = "enum")]
pub enum_values: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TitledMultiSelectEnumSchema {
#[serde(rename = "type")]
pub schema_type: String,
#[serde(rename = "minItems", skip_serializing_if = "Option::is_none")]
pub min_items: Option<u32>,
#[serde(rename = "maxItems", skip_serializing_if = "Option::is_none")]
pub max_items: Option<u32>,
pub items: MultiSelectItems,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct UntitledMultiSelectEnumSchema {
#[serde(rename = "type")]
pub schema_type: String,
#[serde(rename = "minItems", skip_serializing_if = "Option::is_none")]
pub min_items: Option<u32>,
#[serde(rename = "maxItems", skip_serializing_if = "Option::is_none")]
pub max_items: Option<u32>,
pub items: UntitledMultiSelectItems,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct MultiSelectItems {
#[serde(rename = "anyOf")]
pub any_of: Vec<EnumOption>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct UntitledMultiSelectItems {
#[serde(rename = "type")]
pub schema_type: String,
#[serde(rename = "enum")]
pub enum_values: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum EnumSchema {
TitledSingleSelect(TitledSingleSelectEnumSchema),
UntitledSingleSelect(UntitledSingleSelectEnumSchema),
TitledMultiSelect(TitledMultiSelectEnumSchema),
UntitledMultiSelect(UntitledMultiSelectEnumSchema),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct URLElicitationRequiredError {
pub url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(rename = "elicitationId", skip_serializing_if = "Option::is_none")]
pub elicitation_id: Option<String>,
}
impl URLElicitationRequiredError {
pub fn new(url: impl Into<String>) -> Self {
Self {
url: url.into(),
description: None,
elicitation_id: None,
}
}
#[must_use]
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
#[must_use]
pub fn with_elicitation_id(mut self, id: impl Into<String>) -> Self {
self.elicitation_id = Some(id.into());
self
}
pub const ERROR_CODE: i32 = -32001;
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::Value;
#[test]
fn elicitation_schema_builder_round_trip() {
let schema = ElicitationSchema::new()
.add_string_property("name".into(), true, Some("User name".into()))
.add_number_property("age".into(), false, None, Some(0.0), Some(120.0));
let json = serde_json::to_string(&schema).unwrap();
let v: Value = serde_json::from_str(&json).unwrap();
assert_eq!(v["type"], "object");
assert!(v["properties"]["name"].is_object());
assert_eq!(v["properties"]["name"]["type"], "string");
assert_eq!(v["properties"]["age"]["type"], "number");
assert_eq!(
v["required"].as_array().unwrap(),
&vec![Value::from("name")]
);
assert_eq!(v["additionalProperties"], false);
}
#[test]
fn primitive_schema_string_serde() {
let s = PrimitiveSchemaDefinition::String {
title: Some("Name".into()),
description: None,
format: Some("email".into()),
min_length: Some(1),
max_length: Some(80),
enum_values: None,
enum_names: None,
};
let json = serde_json::to_string(&s).unwrap();
assert!(json.contains("\"type\":\"string\""));
assert!(json.contains("\"format\":\"email\""));
let back: PrimitiveSchemaDefinition = serde_json::from_str(&json).unwrap();
assert_eq!(s, back);
}
#[test]
fn titled_single_select_enum_schema_round_trip() {
let schema = TitledSingleSelectEnumSchema {
schema_type: "string".into(),
one_of: vec![
EnumOption {
const_value: "#FF0000".into(),
title: "Red".into(),
},
EnumOption {
const_value: "#00FF00".into(),
title: "Green".into(),
},
],
title: Some("Color".into()),
description: None,
default: Some("#FF0000".into()),
};
let json = serde_json::to_string(&schema).unwrap();
let v: Value = serde_json::from_str(&json).unwrap();
assert_eq!(v["type"], "string");
assert!(v["oneOf"].is_array());
assert_eq!(v["default"], "#FF0000");
let back: TitledSingleSelectEnumSchema = serde_json::from_str(&json).unwrap();
assert_eq!(schema, back);
}
#[test]
fn enum_schema_union_discriminates_correctly() {
let titled = r#"{"type":"string","oneOf":[{"const":"a","title":"A"}]}"#;
match serde_json::from_str::<EnumSchema>(titled).unwrap() {
EnumSchema::TitledSingleSelect(_) => {}
_ => panic!("expected TitledSingleSelect"),
}
let untitled = r#"{"type":"string","enum":["a","b"]}"#;
match serde_json::from_str::<EnumSchema>(untitled).unwrap() {
EnumSchema::UntitledSingleSelect(_) => {}
_ => panic!("expected UntitledSingleSelect"),
}
let multi_titled = r#"{"type":"array","items":{"anyOf":[{"const":"a","title":"A"}]}}"#;
match serde_json::from_str::<EnumSchema>(multi_titled).unwrap() {
EnumSchema::TitledMultiSelect(_) => {}
_ => panic!("expected TitledMultiSelect"),
}
let multi_untitled = r#"{"type":"array","items":{"type":"string","enum":["a","b"]}}"#;
match serde_json::from_str::<EnumSchema>(multi_untitled).unwrap() {
EnumSchema::UntitledMultiSelect(_) => {}
_ => panic!("expected UntitledMultiSelect"),
}
}
#[test]
fn url_elicitation_required_error_round_trip() {
let err = URLElicitationRequiredError::new("https://example.com/oauth")
.with_description("Please sign in")
.with_elicitation_id("e-123");
let json = serde_json::to_string(&err).unwrap();
assert!(json.contains("\"elicitationId\":\"e-123\""));
let back: URLElicitationRequiredError = serde_json::from_str(&json).unwrap();
assert_eq!(err, back);
assert_eq!(URLElicitationRequiredError::ERROR_CODE, -32001);
}
}