use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ContentBlock {
Text(TextBlock),
ToolUse(ToolUseBlock),
ToolResult(ToolResultBlock),
Thinking(ThinkingBlock),
Image(ImageBlock),
}
impl ContentBlock {
pub fn as_text(&self) -> Option<&str> {
match self {
ContentBlock::Text(t) => Some(&t.text),
_ => None,
}
}
pub fn is_thinking(&self) -> bool {
matches!(self, ContentBlock::Thinking(_))
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TextBlock {
pub text: String,
#[serde(flatten)]
pub extra: Value,
}
impl TextBlock {
pub fn new(text: impl Into<String>) -> Self {
Self {
text: text.into(),
extra: Value::Object(Default::default()),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ToolUseBlock {
pub id: String,
pub name: String,
#[serde(default)]
pub input: Value,
#[serde(flatten)]
pub extra: Value,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ToolResultBlock {
pub tool_use_id: String,
#[serde(default)]
pub content: Vec<ToolResultContent>,
#[serde(default)]
pub is_error: bool,
#[serde(flatten)]
pub extra: Value,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ToolResultContent {
Text { text: String },
Image { source: ImageSource },
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ThinkingBlock {
#[serde(default)]
pub thinking: String,
#[serde(flatten)]
pub extra: Value,
}
impl ThinkingBlock {
pub fn new(thinking: impl Into<String>) -> Self {
Self {
thinking: thinking.into(),
extra: Value::Object(Default::default()),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ImageBlock {
pub source: ImageSource,
#[serde(flatten)]
pub extra: Value,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ImageSource {
Base64(Base64ImageSource),
Url(UrlImageSource),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Base64ImageSource {
pub media_type: String,
pub data: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct UrlImageSource {
pub url: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum UserContent {
Text { text: String },
Image { source: ImageSource },
}
impl UserContent {
pub fn text(text: impl Into<String>) -> Self {
UserContent::Text { text: text.into() }
}
pub fn image_base64(media_type: impl Into<String>, data: impl Into<String>) -> Self {
UserContent::Image {
source: ImageSource::Base64(Base64ImageSource {
media_type: media_type.into(),
data: data.into(),
}),
}
}
pub fn image_url(url: impl Into<String>) -> Self {
UserContent::Image {
source: ImageSource::Url(UrlImageSource { url: url.into() }),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_content_block_text_serde_roundtrip() {
let block = ContentBlock::Text(TextBlock::new("hello world"));
let json = serde_json::to_value(&block).expect("serialize");
assert_eq!(json["type"], "text");
assert_eq!(json["text"], "hello world");
let restored: ContentBlock = serde_json::from_value(json).expect("deserialize");
assert_eq!(restored, block);
}
#[test]
fn test_content_block_tool_use_serde_roundtrip() {
let block = ContentBlock::ToolUse(ToolUseBlock {
id: "call_123".into(),
name: "read_file".into(),
input: json!({"path": "/tmp/foo.txt"}),
extra: Value::Object(Default::default()),
});
let json = serde_json::to_value(&block).expect("serialize");
assert_eq!(json["type"], "tool_use");
assert_eq!(json["id"], "call_123");
assert_eq!(json["name"], "read_file");
let restored: ContentBlock = serde_json::from_value(json).expect("deserialize");
assert_eq!(restored, block);
}
#[test]
fn test_content_block_as_text_some() {
let block = ContentBlock::Text(TextBlock::new("greetings"));
assert_eq!(block.as_text(), Some("greetings"));
}
#[test]
fn test_content_block_as_text_none() {
let block = ContentBlock::ToolUse(ToolUseBlock {
id: "id".into(),
name: "tool".into(),
input: Value::Null,
extra: Value::Object(Default::default()),
});
assert_eq!(block.as_text(), None);
}
#[test]
fn test_content_block_is_thinking() {
let thinking = ContentBlock::Thinking(ThinkingBlock::new("deep thought"));
assert!(thinking.is_thinking());
let text = ContentBlock::Text(TextBlock::new("plain text"));
assert!(!text.is_thinking());
}
#[test]
fn test_text_block_new() {
let tb = TextBlock::new("sample");
assert_eq!(tb.text, "sample");
assert_eq!(tb.extra, Value::Object(Default::default()));
}
#[test]
fn test_thinking_block_new() {
let tb = ThinkingBlock::new("pondering");
assert_eq!(tb.thinking, "pondering");
assert_eq!(tb.extra, Value::Object(Default::default()));
}
#[test]
fn test_user_content_text() {
let uc = UserContent::text("hi there");
assert_eq!(uc, UserContent::Text { text: "hi there".into() });
let json = serde_json::to_value(&uc).expect("serialize");
assert_eq!(json["type"], "text");
assert_eq!(json["text"], "hi there");
let restored: UserContent = serde_json::from_value(json).expect("deserialize");
assert_eq!(restored, uc);
}
#[test]
fn test_user_content_image_base64() {
let uc = UserContent::image_base64("image/png", "abc123==");
let expected = UserContent::Image {
source: ImageSource::Base64(Base64ImageSource {
media_type: "image/png".into(),
data: "abc123==".into(),
}),
};
assert_eq!(uc, expected);
let json = serde_json::to_value(&uc).expect("serialize");
assert_eq!(json["type"], "image");
assert_eq!(json["source"]["type"], "base64");
assert_eq!(json["source"]["media_type"], "image/png");
assert_eq!(json["source"]["data"], "abc123==");
}
#[test]
fn test_user_content_image_url() {
let uc = UserContent::image_url("https://example.com/photo.jpg");
let expected = UserContent::Image {
source: ImageSource::Url(UrlImageSource {
url: "https://example.com/photo.jpg".into(),
}),
};
assert_eq!(uc, expected);
let json = serde_json::to_value(&uc).expect("serialize");
assert_eq!(json["type"], "image");
assert_eq!(json["source"]["type"], "url");
assert_eq!(json["source"]["url"], "https://example.com/photo.jpg");
}
}