use serde::{Deserialize, Serialize};
const SUPPORTED_IMAGE_MIME_TYPES: &[&str] = &["image/jpeg", "image/png", "image/gif", "image/webp"];
const MAX_BASE64_SIZE: usize = 15_728_640;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum AssistantMessageError {
AuthenticationFailed,
BillingError,
RateLimit,
InvalidRequest,
ServerError,
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum Message {
#[serde(rename = "assistant")]
Assistant(AssistantMessage),
#[serde(rename = "system")]
System(SystemMessage),
#[serde(rename = "result")]
Result(ResultMessage),
#[serde(rename = "stream_event")]
StreamEvent(StreamEvent),
#[serde(rename = "user")]
User(UserMessage),
#[serde(rename = "control_cancel_request")]
ControlCancelRequest(serde_json::Value),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserMessage {
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<Vec<ContentBlock>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub uuid: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parent_tool_use_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_use_result: Option<serde_json::Value>,
#[serde(flatten)]
pub extra: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum MessageContent {
Text { text: String },
Blocks { content: Vec<ContentBlock> },
}
impl From<String> for MessageContent {
fn from(text: String) -> Self {
MessageContent::Text { text }
}
}
impl From<&str> for MessageContent {
fn from(text: &str) -> Self {
MessageContent::Text {
text: text.to_string(),
}
}
}
impl From<Vec<ContentBlock>> for MessageContent {
fn from(blocks: Vec<ContentBlock>) -> Self {
MessageContent::Blocks { content: blocks }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AssistantMessage {
pub message: AssistantMessageInner,
#[serde(skip_serializing_if = "Option::is_none")]
pub parent_tool_use_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub session_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub uuid: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AssistantMessageInner {
#[serde(default)]
pub content: Vec<ContentBlock>,
#[serde(skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stop_reason: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub usage: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<AssistantMessageError>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SystemMessage {
pub subtype: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub cwd: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub session_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tools: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mcp_servers: Option<Vec<serde_json::Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", rename = "permissionMode")]
pub permission_mode: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub uuid: Option<String>,
#[serde(flatten)]
pub data: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResultMessage {
pub subtype: String,
pub duration_ms: u64,
pub duration_api_ms: u64,
pub is_error: bool,
pub num_turns: u32,
pub session_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub total_cost_usd: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub usage: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub structured_output: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StreamEvent {
pub uuid: String,
pub session_id: String,
pub event: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub parent_tool_use_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ContentBlock {
Text(TextBlock),
Thinking(ThinkingBlock),
ToolUse(ToolUseBlock),
ToolResult(ToolResultBlock),
Image(ImageBlock),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TextBlock {
pub text: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ThinkingBlock {
pub thinking: String,
pub signature: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolUseBlock {
pub id: String,
pub name: String,
pub input: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolResultBlock {
pub tool_use_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<ToolResultContent>,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_error: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ToolResultContent {
Text(String),
Blocks(Vec<serde_json::Value>),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ImageSource {
Base64 {
media_type: String,
data: String,
},
Url {
url: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ImageBlock {
pub source: ImageSource,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum UserContentBlock {
Text {
text: String,
},
Image {
source: ImageSource,
},
}
impl UserContentBlock {
pub fn text(text: impl Into<String>) -> Self {
UserContentBlock::Text { text: text.into() }
}
pub fn image_base64(
media_type: impl Into<String>,
data: impl Into<String>,
) -> crate::errors::Result<Self> {
let media_type_str = media_type.into();
let data_str = data.into();
if !SUPPORTED_IMAGE_MIME_TYPES.contains(&media_type_str.as_str()) {
return Err(crate::errors::ImageValidationError::new(format!(
"Unsupported media type '{}'. Supported types: {:?}",
media_type_str, SUPPORTED_IMAGE_MIME_TYPES
))
.into());
}
if data_str.len() > MAX_BASE64_SIZE {
return Err(crate::errors::ImageValidationError::new(format!(
"Base64 data exceeds maximum size of {} bytes (got {} bytes)",
MAX_BASE64_SIZE,
data_str.len()
))
.into());
}
Ok(UserContentBlock::Image {
source: ImageSource::Base64 {
media_type: media_type_str,
data: data_str,
},
})
}
pub fn image_url(url: impl Into<String>) -> Self {
UserContentBlock::Image {
source: ImageSource::Url { url: url.into() },
}
}
pub fn validate_content(blocks: &[UserContentBlock]) -> crate::Result<()> {
if blocks.is_empty() {
return Err(crate::errors::ClaudeError::InvalidConfig(
"Content must include at least one block (text or image)".to_string(),
));
}
Ok(())
}
}
impl From<String> for UserContentBlock {
fn from(text: String) -> Self {
UserContentBlock::Text { text }
}
}
impl From<&str> for UserContentBlock {
fn from(text: &str) -> Self {
UserContentBlock::Text {
text: text.to_string(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_content_block_text_serialization() {
let block = ContentBlock::Text(TextBlock {
text: "Hello".to_string(),
});
let json = serde_json::to_value(&block).unwrap();
assert_eq!(json["type"], "text");
assert_eq!(json["text"], "Hello");
}
#[test]
fn test_content_block_tool_use_serialization() {
let block = ContentBlock::ToolUse(ToolUseBlock {
id: "tool_123".to_string(),
name: "Bash".to_string(),
input: json!({"command": "echo hello"}),
});
let json = serde_json::to_value(&block).unwrap();
assert_eq!(json["type"], "tool_use");
assert_eq!(json["id"], "tool_123");
assert_eq!(json["name"], "Bash");
assert_eq!(json["input"]["command"], "echo hello");
}
#[test]
fn test_message_assistant_deserialization() {
let json_str = r#"{
"type": "assistant",
"message": {
"content": [{"type": "text", "text": "Hello"}],
"model": "claude-sonnet-4"
},
"session_id": "test-session"
}"#;
let msg: Message = serde_json::from_str(json_str).unwrap();
match msg {
Message::Assistant(assistant) => {
assert_eq!(assistant.session_id, Some("test-session".to_string()));
assert_eq!(assistant.message.model, Some("claude-sonnet-4".to_string()));
}
_ => panic!("Expected Assistant variant"),
}
}
#[test]
fn test_message_result_deserialization() {
let json_str = r#"{
"type": "result",
"subtype": "query_complete",
"duration_ms": 1500,
"duration_api_ms": 1200,
"is_error": false,
"num_turns": 3,
"session_id": "test-session",
"total_cost_usd": 0.0042
}"#;
let msg: Message = serde_json::from_str(json_str).unwrap();
match msg {
Message::Result(result) => {
assert_eq!(result.subtype, "query_complete");
assert_eq!(result.duration_ms, 1500);
assert_eq!(result.num_turns, 3);
assert_eq!(result.total_cost_usd, Some(0.0042));
}
_ => panic!("Expected Result variant"),
}
}
#[test]
fn test_message_system_deserialization() {
let json_str = r#"{
"type": "system",
"subtype": "session_start",
"cwd": "/home/user",
"session_id": "test-session",
"tools": ["Bash", "Read", "Write"]
}"#;
let msg: Message = serde_json::from_str(json_str).unwrap();
match msg {
Message::System(system) => {
assert_eq!(system.subtype, "session_start");
assert_eq!(system.cwd, Some("/home/user".to_string()));
assert_eq!(system.tools.as_ref().unwrap().len(), 3);
}
_ => panic!("Expected System variant"),
}
}
#[test]
fn test_tool_result_content_text() {
let content = ToolResultContent::Text("Command output".to_string());
let json = serde_json::to_value(&content).unwrap();
assert_eq!(json, "Command output");
}
#[test]
fn test_tool_result_content_blocks() {
let content = ToolResultContent::Blocks(vec![json!({"type": "text", "text": "Result"})]);
let json = serde_json::to_value(&content).unwrap();
assert!(json.is_array());
assert_eq!(json[0]["type"], "text");
}
#[test]
fn test_image_source_base64_serialization() {
let source = ImageSource::Base64 {
media_type: "image/png".to_string(),
data: "iVBORw0KGgo=".to_string(),
};
let json = serde_json::to_value(&source).unwrap();
assert_eq!(json["type"], "base64");
assert_eq!(json["media_type"], "image/png");
assert_eq!(json["data"], "iVBORw0KGgo=");
}
#[test]
fn test_image_source_url_serialization() {
let source = ImageSource::Url {
url: "https://example.com/image.png".to_string(),
};
let json = serde_json::to_value(&source).unwrap();
assert_eq!(json["type"], "url");
assert_eq!(json["url"], "https://example.com/image.png");
}
#[test]
fn test_image_source_base64_deserialization() {
let json_str = r#"{
"type": "base64",
"media_type": "image/jpeg",
"data": "base64data=="
}"#;
let source: ImageSource = serde_json::from_str(json_str).unwrap();
match source {
ImageSource::Base64 { media_type, data } => {
assert_eq!(media_type, "image/jpeg");
assert_eq!(data, "base64data==");
}
_ => panic!("Expected Base64 variant"),
}
}
#[test]
fn test_image_source_url_deserialization() {
let json_str = r#"{
"type": "url",
"url": "https://example.com/test.gif"
}"#;
let source: ImageSource = serde_json::from_str(json_str).unwrap();
match source {
ImageSource::Url { url } => {
assert_eq!(url, "https://example.com/test.gif");
}
_ => panic!("Expected Url variant"),
}
}
#[test]
fn test_user_content_block_text_serialization() {
let block = UserContentBlock::text("Hello world");
let json = serde_json::to_value(&block).unwrap();
assert_eq!(json["type"], "text");
assert_eq!(json["text"], "Hello world");
}
#[test]
fn test_user_content_block_image_base64_serialization() {
let block = UserContentBlock::image_base64("image/png", "iVBORw0KGgo=").unwrap();
let json = serde_json::to_value(&block).unwrap();
assert_eq!(json["type"], "image");
assert_eq!(json["source"]["type"], "base64");
assert_eq!(json["source"]["media_type"], "image/png");
assert_eq!(json["source"]["data"], "iVBORw0KGgo=");
}
#[test]
fn test_user_content_block_image_url_serialization() {
let block = UserContentBlock::image_url("https://example.com/image.webp");
let json = serde_json::to_value(&block).unwrap();
assert_eq!(json["type"], "image");
assert_eq!(json["source"]["type"], "url");
assert_eq!(json["source"]["url"], "https://example.com/image.webp");
}
#[test]
fn test_user_content_block_from_string() {
let block: UserContentBlock = "Test message".into();
match block {
UserContentBlock::Text { text } => {
assert_eq!(text, "Test message");
}
_ => panic!("Expected Text variant"),
}
}
#[test]
fn test_user_content_block_from_owned_string() {
let block: UserContentBlock = String::from("Owned message").into();
match block {
UserContentBlock::Text { text } => {
assert_eq!(text, "Owned message");
}
_ => panic!("Expected Text variant"),
}
}
#[test]
fn test_image_block_serialization() {
let block = ImageBlock {
source: ImageSource::Base64 {
media_type: "image/gif".to_string(),
data: "R0lGODlh".to_string(),
},
};
let json = serde_json::to_value(&block).unwrap();
assert_eq!(json["source"]["type"], "base64");
assert_eq!(json["source"]["media_type"], "image/gif");
assert_eq!(json["source"]["data"], "R0lGODlh");
}
#[test]
fn test_content_block_image_serialization() {
let block = ContentBlock::Image(ImageBlock {
source: ImageSource::Url {
url: "https://example.com/photo.jpg".to_string(),
},
});
let json = serde_json::to_value(&block).unwrap();
assert_eq!(json["type"], "image");
assert_eq!(json["source"]["type"], "url");
assert_eq!(json["source"]["url"], "https://example.com/photo.jpg");
}
#[test]
fn test_content_block_image_deserialization() {
let json_str = r#"{
"type": "image",
"source": {
"type": "base64",
"media_type": "image/webp",
"data": "UklGR"
}
}"#;
let block: ContentBlock = serde_json::from_str(json_str).unwrap();
match block {
ContentBlock::Image(image) => match image.source {
ImageSource::Base64 { media_type, data } => {
assert_eq!(media_type, "image/webp");
assert_eq!(data, "UklGR");
}
_ => panic!("Expected Base64 source"),
},
_ => panic!("Expected Image variant"),
}
}
#[test]
fn test_user_content_block_deserialization() {
let json_str = r#"{
"type": "text",
"text": "Describe this image"
}"#;
let block: UserContentBlock = serde_json::from_str(json_str).unwrap();
match block {
UserContentBlock::Text { text } => {
assert_eq!(text, "Describe this image");
}
_ => panic!("Expected Text variant"),
}
}
#[test]
fn test_user_content_block_image_deserialization() {
let json_str = r#"{
"type": "image",
"source": {
"type": "url",
"url": "https://example.com/diagram.png"
}
}"#;
let block: UserContentBlock = serde_json::from_str(json_str).unwrap();
match block {
UserContentBlock::Image { source } => match source {
ImageSource::Url { url } => {
assert_eq!(url, "https://example.com/diagram.png");
}
_ => panic!("Expected Url source"),
},
_ => panic!("Expected Image variant"),
}
}
#[test]
fn test_image_base64_valid() {
let block = UserContentBlock::image_base64("image/png", "iVBORw0KGgo=");
assert!(block.is_ok());
}
#[test]
fn test_image_base64_invalid_mime_type() {
let block = UserContentBlock::image_base64("image/bmp", "data");
assert!(block.is_err());
let err = block.unwrap_err().to_string();
assert!(err.contains("Unsupported media type"));
assert!(err.contains("image/bmp"));
}
#[test]
fn test_image_base64_exceeds_size_limit() {
let large_data = "a".repeat(MAX_BASE64_SIZE + 1);
let block = UserContentBlock::image_base64("image/png", large_data);
assert!(block.is_err());
let err = block.unwrap_err().to_string();
assert!(err.contains("exceeds maximum size"));
}
#[test]
fn test_user_message_with_tool_use_result() {
let json_str = r#"{
"type": "user",
"text": "result",
"tool_use_result": {"output": "success", "exit_code": 0}
}"#;
let msg: UserMessage = serde_json::from_str(json_str).unwrap();
assert!(msg.tool_use_result.is_some());
let result = msg.tool_use_result.unwrap();
assert_eq!(result["output"], "success");
assert_eq!(result["exit_code"], 0);
}
#[test]
fn test_user_message_without_tool_use_result() {
let json_str = r#"{
"type": "user",
"text": "Hello"
}"#;
let msg: UserMessage = serde_json::from_str(json_str).unwrap();
assert!(msg.tool_use_result.is_none());
}
#[test]
fn test_user_message_tool_use_result_serialization() {
let msg = UserMessage {
text: Some("test".to_string()),
content: None,
uuid: None,
parent_tool_use_id: None,
tool_use_result: Some(json!({"status": "ok"})),
extra: json!({}),
};
let json = serde_json::to_value(&msg).unwrap();
assert_eq!(json["tool_use_result"]["status"], "ok");
}
}