use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use super::content::{Content, Role};
use super::protocol::Cursor;
use super::protocol::RequestMeta;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ListPromptsRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub cursor: Cursor,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[non_exhaustive]
#[serde(rename_all = "camelCase")]
pub struct PromptInfo {
pub name: 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 arguments: Option<Vec<PromptArgument>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub icons: Option<Vec<super::protocol::IconInfo>>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<serde_json::Map<String, serde_json::Value>>,
}
impl PromptInfo {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
title: None,
description: None,
arguments: None,
icons: None,
meta: None,
}
}
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn with_arguments(mut self, arguments: Vec<PromptArgument>) -> Self {
self.arguments = Some(arguments);
self
}
pub fn with_icons(mut self, icons: Vec<super::protocol::IconInfo>) -> Self {
self.icons = Some(icons);
self
}
pub fn with_meta(mut self, meta: serde_json::Map<String, serde_json::Value>) -> Self {
self.meta = Some(meta);
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum PromptArgumentType {
#[default]
String,
Number,
Integer,
Boolean,
}
impl PromptArgumentType {
pub fn parse_value(&self, s: &str) -> Result<serde_json::Value, String> {
match self {
Self::String => Ok(serde_json::Value::String(s.to_string())),
Self::Number => s
.parse::<f64>()
.map(|n| serde_json::json!(n))
.map_err(|_| format!("'{}' is not a valid number", s)),
Self::Integer => s
.parse::<i64>()
.map(|n| serde_json::json!(n))
.map_err(|_| format!("'{}' is not a valid integer", s)),
Self::Boolean => match s.to_lowercase().as_str() {
"true" | "1" | "yes" => Ok(serde_json::json!(true)),
"false" | "0" | "no" => Ok(serde_json::json!(false)),
_ => Err(format!("'{}' is not a valid boolean (use true/false)", s)),
},
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[non_exhaustive]
#[serde(rename_all = "camelCase")]
pub struct PromptArgument {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default)]
pub required: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub completion: Option<crate::types::completable::CompletionConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub arg_type: Option<PromptArgumentType>,
}
impl PromptArgument {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
description: None,
required: false,
completion: None,
arg_type: None,
}
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn required(mut self) -> Self {
self.required = true;
self
}
pub fn with_completion(
mut self,
completion: crate::types::completable::CompletionConfig,
) -> Self {
self.completion = Some(completion);
self
}
pub fn with_arg_type(mut self, arg_type: PromptArgumentType) -> Self {
self.arg_type = Some(arg_type);
self
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ListPromptsResult {
pub prompts: Vec<PromptInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_cursor: Cursor,
}
impl ListPromptsResult {
pub fn new(prompts: Vec<PromptInfo>) -> Self {
Self {
prompts,
next_cursor: None,
}
}
pub fn with_next_cursor(mut self, cursor: impl Into<String>) -> Self {
self.next_cursor = Some(cursor.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GetPromptRequest {
pub name: String,
#[serde(default)]
pub arguments: HashMap<String, String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[allow(clippy::pub_underscore_fields)] pub _meta: Option<RequestMeta>,
}
#[non_exhaustive]
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GetPromptResult {
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub messages: Vec<PromptMessage>,
#[serde(rename = "_meta")]
#[serde(skip_serializing_if = "Option::is_none")]
#[allow(clippy::pub_underscore_fields)]
pub _meta: Option<serde_json::Map<String, serde_json::Value>>,
}
impl GetPromptResult {
pub fn new(messages: Vec<PromptMessage>, description: Option<String>) -> Self {
Self {
description,
messages,
_meta: None,
}
}
#[allow(clippy::used_underscore_binding)] pub fn with_meta(mut self, meta: serde_json::Map<String, serde_json::Value>) -> Self {
self._meta = Some(meta);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
#[serde(rename_all = "camelCase")]
pub struct PromptMessage {
pub role: Role,
pub content: Content,
}
impl PromptMessage {
pub fn new(role: Role, content: Content) -> Self {
Self { role, content }
}
pub fn user(content: Content) -> Self {
Self {
role: Role::User,
content,
}
}
pub fn assistant(content: Content) -> Self {
Self {
role: Role::Assistant,
content,
}
}
pub fn system(content: Content) -> Self {
Self {
role: Role::System,
content,
}
}
}
#[cfg(test)]
#[allow(clippy::used_underscore_binding)]
mod tests {
use super::*;
use crate::types::content::Content;
#[test]
fn test_prompt_types() {
let prompt = PromptInfo::new("test_prompt")
.with_description("A test prompt")
.with_arguments(vec![PromptArgument::new("arg1")
.with_description("First argument")
.required()]);
let json = serde_json::to_value(&prompt).unwrap();
assert_eq!(json["name"], "test_prompt");
assert_eq!(json["arguments"][0]["name"], "arg1");
assert_eq!(json["arguments"][0]["required"], true);
}
#[test]
fn get_prompt_result_without_meta_omits_field() {
let result = GetPromptResult::new(vec![], Some("Test".to_string()));
let json = serde_json::to_value(&result).unwrap();
assert!(
json.get("_meta").is_none(),
"_meta should be omitted when None"
);
assert_eq!(json["description"], "Test");
}
#[test]
fn get_prompt_result_with_meta_includes_field() {
let mut meta = serde_json::Map::new();
meta.insert(
"taskId".to_string(),
serde_json::Value::String("task-123".to_string()),
);
let result = GetPromptResult::new(vec![], None).with_meta(meta);
let json = serde_json::to_value(&result).unwrap();
assert!(json.get("_meta").is_some(), "_meta should be present");
assert_eq!(json["_meta"]["taskId"], "task-123");
}
#[test]
fn get_prompt_result_deserialize_without_meta_backward_compat() {
let json_str = r#"{"messages": [], "description": "Test"}"#;
let result: GetPromptResult = serde_json::from_str(json_str).unwrap();
assert!(
result._meta.is_none(),
"Missing _meta should deserialize as None"
);
assert_eq!(result.description.as_deref(), Some("Test"));
}
#[test]
fn get_prompt_result_serde_round_trip_with_meta() {
let mut meta = serde_json::Map::new();
meta.insert(
"taskId".to_string(),
serde_json::Value::String("task-456".to_string()),
);
meta.insert(
"status".to_string(),
serde_json::Value::String("working".to_string()),
);
let result = GetPromptResult::new(
vec![PromptMessage::user(Content::text("Hello"))],
Some("Workflow result".to_string()),
)
.with_meta(meta);
let json = serde_json::to_value(&result).unwrap();
let round_trip: GetPromptResult = serde_json::from_value(json).unwrap();
assert_eq!(round_trip.description.as_deref(), Some("Workflow result"));
assert_eq!(round_trip.messages.len(), 1);
assert!(round_trip._meta.is_some());
let rt_meta = round_trip._meta.unwrap();
assert_eq!(
rt_meta.get("taskId").unwrap(),
&serde_json::Value::String("task-456".to_string())
);
}
#[test]
fn test_prompt_message_convenience() {
let user_msg = PromptMessage::user(Content::text("Hello"));
assert_eq!(user_msg.role, Role::User);
let assistant_msg = PromptMessage::assistant(Content::text("Hi"));
assert_eq!(assistant_msg.role, Role::Assistant);
let system_msg = PromptMessage::system(Content::text("Be helpful"));
assert_eq!(system_msg.role, Role::System);
}
#[test]
fn test_prompt_argument_default() {
let arg = PromptArgument::new("test");
assert_eq!(arg.name, "test");
assert!(!arg.required);
assert!(arg.description.is_none());
}
}