use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "camelCase")]
pub enum ElicitationAction {
Accept,
Decline,
Cancel,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
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 {
pub fn new() -> Self {
Self {
schema_type: "object".to_string(),
properties: HashMap::new(),
required: Some(Vec::new()),
additional_properties: Some(false),
}
}
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(ref mut required_fields) = self.required {
required_fields.push(name);
}
self
}
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(ref mut required_fields) = self.required {
required_fields.push(name);
}
self
}
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(ref mut required_fields) = self.required {
required_fields.push(name);
}
self
}
}
impl Default for ElicitationSchema {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[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(skip_serializing_if = "Option::is_none")]
#[serde(rename = "minLength")]
min_length: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "maxLength")]
max_length: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "enum")]
enum_values: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "enumNames")]
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)]
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)]
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)]
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)]
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)]
pub struct MultiSelectItems {
#[serde(rename = "anyOf")]
pub any_of: Vec<EnumOption>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UntitledMultiSelectItems {
#[serde(rename = "type")]
pub schema_type: String,
#[serde(rename = "enum")]
pub enum_values: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum EnumSchema {
TitledSingleSelect(TitledSingleSelectEnumSchema),
UntitledSingleSelect(UntitledSingleSelectEnumSchema),
TitledMultiSelect(TitledMultiSelectEnumSchema),
UntitledMultiSelect(UntitledMultiSelectEnumSchema),
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "lowercase")]
pub enum ElicitMode {
Form,
Url,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct URLElicitRequestParams {
pub mode: ElicitMode,
#[serde(rename = "elicitationId")]
pub elicitation_id: String,
pub message: String,
#[serde(with = "url_serde")]
pub url: url::Url,
}
mod url_serde {
use serde::{Deserialize, Deserializer, Serializer};
use url::Url;
pub(super) fn serialize<S>(url: &Url, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(url.as_str())
}
pub(super) fn deserialize<'de, D>(deserializer: D) -> Result<Url, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Url::parse(&s).map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FormElicitRequestParams {
pub message: String,
#[serde(rename = "requestedSchema")]
pub schema: ElicitationSchema,
#[serde(rename = "timeoutMs", skip_serializing_if = "Option::is_none")]
pub timeout_ms: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cancellable: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ElicitRequestParams {
Form(FormElicitRequestParams),
Url(URLElicitRequestParams),
}
impl ElicitRequestParams {
pub fn form(
message: String,
schema: ElicitationSchema,
timeout_ms: Option<u32>,
cancellable: Option<bool>,
) -> Self {
ElicitRequestParams::Form(FormElicitRequestParams {
message,
schema,
timeout_ms,
cancellable,
})
}
pub fn url(elicitation_id: String, message: String, url: url::Url) -> Self {
ElicitRequestParams::Url(URLElicitRequestParams {
mode: ElicitMode::Url,
elicitation_id,
message,
url,
})
}
pub fn message(&self) -> &str {
match self {
ElicitRequestParams::Form(form) => &form.message,
ElicitRequestParams::Url(url_params) => &url_params.message,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ElicitRequest {
#[serde(flatten)]
pub params: ElicitRequestParams,
#[serde(skip_serializing_if = "Option::is_none")]
pub task: Option<crate::types::tasks::TaskMetadata>,
#[serde(skip_serializing_if = "Option::is_none")]
pub _meta: Option<serde_json::Value>,
}
impl Default for ElicitRequest {
fn default() -> Self {
Self {
params: ElicitRequestParams::form(String::new(), ElicitationSchema::new(), None, None),
task: None,
_meta: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ElicitResult {
pub action: ElicitationAction,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<std::collections::HashMap<String, serde_json::Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub _meta: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ElicitationCompleteParams {
#[serde(rename = "elicitationId")]
pub elicitation_id: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_elicitation_action_serialization() {
assert_eq!(
serde_json::to_string(&ElicitationAction::Accept).unwrap(),
"\"accept\""
);
assert_eq!(
serde_json::to_string(&ElicitationAction::Decline).unwrap(),
"\"decline\""
);
assert_eq!(
serde_json::to_string(&ElicitationAction::Cancel).unwrap(),
"\"cancel\""
);
}
#[test]
fn test_form_elicit_params() {
let schema = ElicitationSchema::new().add_string_property(
"name".to_string(),
true,
Some("User name".to_string()),
);
let params = ElicitRequestParams::form(
"Please provide your name".to_string(),
schema,
Some(30000),
Some(true),
);
assert_eq!(params.message(), "Please provide your name");
let json = serde_json::to_string(¶ms).unwrap();
assert!(json.contains("Please provide your name"));
assert!(json.contains("requestedSchema"));
}
#[test]
fn test_url_elicit_params() {
use url::Url;
let url = Url::parse("https://example.com/oauth/authorize").unwrap();
let params = ElicitRequestParams::url(
"test-id-123".to_string(),
"Please authorize the connection".to_string(),
url,
);
assert_eq!(params.message(), "Please authorize the connection");
let json = serde_json::to_string(¶ms).unwrap();
assert!(json.contains("test-id-123"));
assert!(json.contains("https://example.com/oauth/authorize"));
assert!(json.contains("\"mode\":\"url\""));
}
#[test]
fn test_elicit_mode_serialization() {
assert_eq!(
serde_json::to_string(&ElicitMode::Form).unwrap(),
"\"form\""
);
assert_eq!(serde_json::to_string(&ElicitMode::Url).unwrap(), "\"url\"");
}
#[test]
fn test_completion_notification() {
let params = ElicitationCompleteParams {
elicitation_id: "550e8400-e29b-41d4-a716-446655440000".to_string(),
};
let json = serde_json::to_string(¶ms).unwrap();
assert!(json.contains("550e8400-e29b-41d4-a716-446655440000"));
assert!(json.contains("elicitationId"));
}
#[test]
fn test_elicit_result_form_mode() {
let mut content = std::collections::HashMap::new();
content.insert("name".to_string(), serde_json::json!("Alice"));
let result = ElicitResult {
action: ElicitationAction::Accept,
content: Some(content),
_meta: None,
};
let json = serde_json::to_string(&result).unwrap();
assert!(json.contains("\"action\":\"accept\""));
assert!(json.contains("\"name\":\"Alice\""));
}
#[test]
fn test_elicit_result_url_mode() {
let result = ElicitResult {
action: ElicitationAction::Accept,
content: None,
_meta: None,
};
let json = serde_json::to_string(&result).unwrap();
assert!(json.contains("\"action\":\"accept\""));
assert!(!json.contains("content"));
}
#[test]
fn test_titled_single_select_enum_schema() {
use super::{EnumOption, TitledSingleSelectEnumSchema};
let schema = TitledSingleSelectEnumSchema {
schema_type: "string".to_string(),
one_of: vec![
EnumOption {
const_value: "#FF0000".to_string(),
title: "Red".to_string(),
},
EnumOption {
const_value: "#00FF00".to_string(),
title: "Green".to_string(),
},
EnumOption {
const_value: "#0000FF".to_string(),
title: "Blue".to_string(),
},
],
title: Some("Color Selection".to_string()),
description: Some("Choose your favorite color".to_string()),
default: Some("#FF0000".to_string()),
};
let json = serde_json::to_string(&schema).unwrap();
assert!(json.contains("\"type\":\"string\""));
assert!(json.contains("\"oneOf\""));
assert!(json.contains("\"const\":\"#FF0000\""));
assert!(json.contains("\"title\":\"Red\""));
assert!(json.contains("\"default\":\"#FF0000\""));
let deserialized: TitledSingleSelectEnumSchema = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.one_of.len(), 3);
assert_eq!(deserialized.one_of[0].const_value, "#FF0000");
assert_eq!(deserialized.one_of[0].title, "Red");
}
#[test]
fn test_untitled_single_select_enum_schema() {
use super::UntitledSingleSelectEnumSchema;
let schema = UntitledSingleSelectEnumSchema {
schema_type: "string".to_string(),
enum_values: vec!["red".to_string(), "green".to_string(), "blue".to_string()],
title: Some("Color Selection".to_string()),
description: Some("Choose a color".to_string()),
default: Some("red".to_string()),
};
let json = serde_json::to_string(&schema).unwrap();
assert!(json.contains("\"type\":\"string\""));
assert!(json.contains("\"enum\""));
assert!(json.contains("\"red\""));
assert!(json.contains("\"green\""));
assert!(json.contains("\"blue\""));
assert!(!json.contains("oneOf"));
let deserialized: UntitledSingleSelectEnumSchema = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.enum_values.len(), 3);
assert_eq!(deserialized.enum_values[0], "red");
}
#[test]
fn test_titled_multi_select_enum_schema() {
use super::{EnumOption, MultiSelectItems, TitledMultiSelectEnumSchema};
let schema = TitledMultiSelectEnumSchema {
schema_type: "array".to_string(),
min_items: Some(1),
max_items: Some(2),
items: MultiSelectItems {
any_of: vec![
EnumOption {
const_value: "#FF0000".to_string(),
title: "Red".to_string(),
},
EnumOption {
const_value: "#00FF00".to_string(),
title: "Green".to_string(),
},
],
},
title: Some("Color Selection".to_string()),
description: Some("Choose up to 2 colors".to_string()),
default: Some(vec!["#FF0000".to_string()]),
};
let json = serde_json::to_string(&schema).unwrap();
assert!(json.contains("\"type\":\"array\""));
assert!(json.contains("\"minItems\":1"));
assert!(json.contains("\"maxItems\":2"));
assert!(json.contains("\"anyOf\""));
assert!(json.contains("\"const\":\"#FF0000\""));
let deserialized: TitledMultiSelectEnumSchema = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.items.any_of.len(), 2);
assert_eq!(deserialized.min_items, Some(1));
assert_eq!(deserialized.max_items, Some(2));
}
#[test]
fn test_untitled_multi_select_enum_schema() {
use super::{UntitledMultiSelectEnumSchema, UntitledMultiSelectItems};
let schema = UntitledMultiSelectEnumSchema {
schema_type: "array".to_string(),
min_items: Some(1),
max_items: None,
items: UntitledMultiSelectItems {
schema_type: "string".to_string(),
enum_values: vec!["red".to_string(), "green".to_string(), "blue".to_string()],
},
title: Some("Color Selection".to_string()),
description: Some("Choose colors".to_string()),
default: Some(vec!["red".to_string(), "green".to_string()]),
};
let json = serde_json::to_string(&schema).unwrap();
assert!(json.contains("\"type\":\"array\""));
assert!(json.contains("\"minItems\":1"));
assert!(json.contains("\"items\""));
assert!(json.contains("\"enum\""));
assert!(!json.contains("anyOf"));
let deserialized: UntitledMultiSelectEnumSchema = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.items.enum_values.len(), 3);
assert_eq!(deserialized.default.as_ref().unwrap().len(), 2);
}
#[test]
fn test_enum_schema_union_type() {
use super::EnumSchema;
let titled_json = r#"{
"type": "string",
"oneOf": [
{"const": "red", "title": "Red"},
{"const": "green", "title": "Green"}
]
}"#;
let schema: EnumSchema = serde_json::from_str(titled_json).unwrap();
match schema {
EnumSchema::TitledSingleSelect(s) => {
assert_eq!(s.one_of.len(), 2);
assert_eq!(s.one_of[0].const_value, "red");
}
_ => panic!("Expected TitledSingleSelect variant"),
}
let untitled_json = r#"{
"type": "string",
"enum": ["red", "green", "blue"]
}"#;
let schema: EnumSchema = serde_json::from_str(untitled_json).unwrap();
match schema {
EnumSchema::UntitledSingleSelect(s) => {
assert_eq!(s.enum_values.len(), 3);
}
_ => panic!("Expected UntitledSingleSelect variant"),
}
}
#[test]
fn test_enum_option_serialization() {
use super::EnumOption;
let option = EnumOption {
const_value: "#FF0000".to_string(),
title: "Red".to_string(),
};
let json = serde_json::to_string(&option).unwrap();
assert!(json.contains("\"const\":\"#FF0000\""));
assert!(json.contains("\"title\":\"Red\""));
assert!(!json.contains("const_value"));
let deserialized: EnumOption = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.const_value, "#FF0000");
assert_eq!(deserialized.title, "Red");
}
}