use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use super::enums::ResourceType;
use super::message::Message;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCall {
pub tool_call_id: String,
pub name: String,
#[serde(default)]
pub arguments: HashMap<String, serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub namespace: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolResult {
pub tool_call_id: String,
pub tool_name: String,
#[serde(default)]
pub content: serde_json::Value,
#[serde(default)]
pub is_error: bool,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Resource {
pub resource_request_id: String,
pub uri: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub resource_type: ResourceType,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub blob: Option<Vec<u8>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mime_type: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub size_bytes: Option<u64>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub annotations: HashMap<String, serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
}
impl Resource {
pub fn is_embedded(&self) -> bool {
self.content.is_some() || self.blob.is_some()
}
pub fn get_text_content(&self) -> Option<&str> {
self.content.as_deref()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResourceReference {
pub resource_request_id: String,
pub uri: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
pub resource_type: ResourceType,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub range_start: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub range_end: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub selector: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PromptRequest {
pub prompt_request_id: String,
pub name: String,
#[serde(default)]
pub arguments: HashMap<String, serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub server_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PromptResult {
pub prompt_request_id: String,
pub prompt_name: String,
#[serde(default)]
pub messages: Vec<Message>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
#[serde(default)]
pub is_error: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error_message: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImageSource {
#[serde(rename = "type")]
pub source_type: String,
pub data: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub media_type: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VideoSource {
#[serde(rename = "type")]
pub source_type: String,
pub data: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub media_type: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub duration_ms: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AudioSource {
#[serde(rename = "type")]
pub source_type: String,
pub data: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub media_type: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub duration_ms: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DocumentSource {
#[serde(rename = "type")]
pub source_type: String,
pub data: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub media_type: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "content_type")]
pub enum ContentPart {
#[serde(rename = "text")]
Text { text: String },
#[serde(rename = "thinking")]
Thinking { text: String },
#[serde(rename = "tool_call")]
ToolCall { content: ToolCall },
#[serde(rename = "tool_result")]
ToolResult { content: ToolResult },
#[serde(rename = "resource")]
Resource { content: Resource },
#[serde(rename = "resource_ref")]
ResourceRef { content: ResourceReference },
#[serde(rename = "prompt_request")]
PromptRequest { content: PromptRequest },
#[serde(rename = "prompt_result")]
PromptResult { content: PromptResult },
#[serde(rename = "image")]
Image { content: ImageSource },
#[serde(rename = "video")]
Video { content: VideoSource },
#[serde(rename = "audio")]
Audio { content: AudioSource },
#[serde(rename = "document")]
Document { content: DocumentSource },
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_text_content_part_serde() {
let json = r#"{"content_type":"text","text":"Hello, world!"}"#;
let part: ContentPart = serde_json::from_str(json).unwrap();
match &part {
ContentPart::Text { text } => assert_eq!(text, "Hello, world!"),
_ => panic!("expected Text variant"),
}
let roundtrip = serde_json::to_string(&part).unwrap();
let part2: ContentPart = serde_json::from_str(&roundtrip).unwrap();
match part2 {
ContentPart::Text { text } => assert_eq!(text, "Hello, world!"),
_ => panic!("expected Text variant"),
}
}
#[test]
fn test_tool_call_content_part_serde() {
let json = r#"{
"content_type": "tool_call",
"content": {
"tool_call_id": "tc_001",
"name": "get_weather",
"arguments": {"city": "London"}
}
}"#;
let part: ContentPart = serde_json::from_str(json).unwrap();
match &part {
ContentPart::ToolCall { content } => {
assert_eq!(content.name, "get_weather");
assert_eq!(content.tool_call_id, "tc_001");
assert_eq!(content.arguments["city"], "London");
},
_ => panic!("expected ToolCall variant"),
}
}
#[test]
fn test_tool_result_content_part_serde() {
let json = r#"{
"content_type": "tool_result",
"content": {
"tool_call_id": "tc_001",
"tool_name": "get_weather",
"content": {"temp": 20, "unit": "C"},
"is_error": false
}
}"#;
let part: ContentPart = serde_json::from_str(json).unwrap();
match &part {
ContentPart::ToolResult { content } => {
assert_eq!(content.tool_name, "get_weather");
assert!(!content.is_error);
},
_ => panic!("expected ToolResult variant"),
}
}
#[test]
fn test_resource_content_part_serde() {
let json = r#"{
"content_type": "resource",
"content": {
"resource_request_id": "rr_001",
"uri": "file:///data.txt",
"resource_type": "file",
"content": "Hello from file"
}
}"#;
let part: ContentPart = serde_json::from_str(json).unwrap();
match &part {
ContentPart::Resource { content } => {
assert_eq!(content.uri, "file:///data.txt");
assert!(content.is_embedded());
assert_eq!(content.get_text_content(), Some("Hello from file"));
},
_ => panic!("expected Resource variant"),
}
}
#[test]
fn test_resource_ref_content_part_serde() {
let json = r#"{
"content_type": "resource_ref",
"content": {
"resource_request_id": "rr_002",
"uri": "db://users/42",
"resource_type": "database"
}
}"#;
let part: ContentPart = serde_json::from_str(json).unwrap();
match &part {
ContentPart::ResourceRef { content } => {
assert_eq!(content.uri, "db://users/42");
assert_eq!(content.resource_type, ResourceType::Database);
},
_ => panic!("expected ResourceRef variant"),
}
}
#[test]
fn test_image_content_part_serde() {
let json = r#"{
"content_type": "image",
"content": {
"type": "url",
"data": "https://example.com/photo.jpg",
"media_type": "image/jpeg"
}
}"#;
let part: ContentPart = serde_json::from_str(json).unwrap();
match &part {
ContentPart::Image { content } => {
assert_eq!(content.source_type, "url");
assert_eq!(content.data, "https://example.com/photo.jpg");
},
_ => panic!("expected Image variant"),
}
}
#[test]
fn test_prompt_request_content_part_serde() {
let json = r#"{
"content_type": "prompt_request",
"content": {
"prompt_request_id": "pr_001",
"name": "summarize",
"arguments": {"text": "Long document..."}
}
}"#;
let part: ContentPart = serde_json::from_str(json).unwrap();
match &part {
ContentPart::PromptRequest { content } => {
assert_eq!(content.name, "summarize");
},
_ => panic!("expected PromptRequest variant"),
}
}
#[test]
fn test_thinking_content_part_serde() {
let json = r#"{"content_type":"thinking","text":"Let me analyze..."}"#;
let part: ContentPart = serde_json::from_str(json).unwrap();
match &part {
ContentPart::Thinking { text } => assert_eq!(text, "Let me analyze..."),
_ => panic!("expected Thinking variant"),
}
}
#[test]
fn test_tool_call_construction() {
let tc = ToolCall {
tool_call_id: "tc_001".into(),
name: "search".into(),
arguments: [("query".to_string(), serde_json::json!("rust"))].into(),
namespace: None,
};
assert_eq!(tc.name, "search");
assert_eq!(tc.arguments["query"], "rust");
}
#[test]
fn test_resource_is_embedded() {
let embedded = Resource {
resource_request_id: "rr_001".into(),
uri: "file:///data.txt".into(),
name: None,
description: None,
resource_type: ResourceType::File,
content: Some("data".into()),
blob: None,
mime_type: None,
size_bytes: None,
annotations: HashMap::new(),
version: None,
};
assert!(embedded.is_embedded());
let not_embedded = Resource {
resource_request_id: "rr_002".into(),
uri: "file:///other.txt".into(),
name: None,
description: None,
resource_type: ResourceType::File,
content: None,
blob: None,
mime_type: None,
size_bytes: None,
annotations: HashMap::new(),
version: None,
};
assert!(!not_embedded.is_embedded());
}
}