1use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
10#[serde(tag = "type", rename_all = "snake_case")]
11pub enum ContentBlock {
12 Text(TextBlock),
13 ToolUse(ToolUseBlock),
14 ToolResult(ToolResultBlock),
15 Thinking(ThinkingBlock),
16 Image(ImageBlock),
17}
18
19impl ContentBlock {
20 pub fn as_text(&self) -> Option<&str> {
31 match self {
32 ContentBlock::Text(t) => Some(&t.text),
33 _ => None,
34 }
35 }
36
37 pub fn is_thinking(&self) -> bool {
48 matches!(self, ContentBlock::Thinking(_))
49 }
50}
51
52#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
56pub struct TextBlock {
57 pub text: String,
58 #[serde(flatten)]
59 pub extra: Value,
60}
61
62impl TextBlock {
63 pub fn new(text: impl Into<String>) -> Self {
65 Self {
66 text: text.into(),
67 extra: Value::Object(Default::default()),
68 }
69 }
70}
71
72#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
76pub struct ToolUseBlock {
77 pub id: String,
79 pub name: String,
81 #[serde(default)]
83 pub input: Value,
84 #[serde(flatten)]
85 pub extra: Value,
86}
87
88#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
92pub struct ToolResultBlock {
93 pub tool_use_id: String,
95 #[serde(default)]
97 pub content: Vec<ToolResultContent>,
98 #[serde(default)]
100 pub is_error: bool,
101 #[serde(flatten)]
102 pub extra: Value,
103}
104
105#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
107#[serde(tag = "type", rename_all = "snake_case")]
108pub enum ToolResultContent {
109 Text { text: String },
110 Image { source: ImageSource },
111}
112
113#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
119pub struct ThinkingBlock {
120 #[serde(default)]
122 pub thinking: String,
123 #[serde(flatten)]
124 pub extra: Value,
125}
126
127impl ThinkingBlock {
128 pub fn new(thinking: impl Into<String>) -> Self {
130 Self {
131 thinking: thinking.into(),
132 extra: Value::Object(Default::default()),
133 }
134 }
135}
136
137#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
141pub struct ImageBlock {
142 pub source: ImageSource,
143 #[serde(flatten)]
144 pub extra: Value,
145}
146
147#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
149#[serde(tag = "type", rename_all = "snake_case")]
150pub enum ImageSource {
151 Base64(Base64ImageSource),
152 Url(UrlImageSource),
153}
154
155#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
157pub struct Base64ImageSource {
158 pub media_type: String,
160 pub data: String,
162}
163
164#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
166pub struct UrlImageSource {
167 pub url: String,
168}
169
170#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
177#[serde(tag = "type", rename_all = "snake_case")]
178pub enum UserContent {
179 Text { text: String },
180 Image { source: ImageSource },
181}
182
183impl UserContent {
184 pub fn text(text: impl Into<String>) -> Self {
194 UserContent::Text { text: text.into() }
195 }
196
197 pub fn image_base64(media_type: impl Into<String>, data: impl Into<String>) -> Self {
207 UserContent::Image {
208 source: ImageSource::Base64(Base64ImageSource {
209 media_type: media_type.into(),
210 data: data.into(),
211 }),
212 }
213 }
214
215 pub fn image_url(url: impl Into<String>) -> Self {
225 UserContent::Image {
226 source: ImageSource::Url(UrlImageSource { url: url.into() }),
227 }
228 }
229}
230
231#[cfg(test)]
234mod tests {
235 use super::*;
236 use serde_json::json;
237
238 #[test]
241 fn test_content_block_text_serde_roundtrip() {
242 let block = ContentBlock::Text(TextBlock::new("hello world"));
243 let json = serde_json::to_value(&block).expect("serialize");
244 assert_eq!(json["type"], "text");
245 assert_eq!(json["text"], "hello world");
246
247 let restored: ContentBlock = serde_json::from_value(json).expect("deserialize");
248 assert_eq!(restored, block);
249 }
250
251 #[test]
252 fn test_content_block_tool_use_serde_roundtrip() {
253 let block = ContentBlock::ToolUse(ToolUseBlock {
254 id: "call_123".into(),
255 name: "read_file".into(),
256 input: json!({"path": "/tmp/foo.txt"}),
257 extra: Value::Object(Default::default()),
258 });
259 let json = serde_json::to_value(&block).expect("serialize");
260 assert_eq!(json["type"], "tool_use");
261 assert_eq!(json["id"], "call_123");
262 assert_eq!(json["name"], "read_file");
263
264 let restored: ContentBlock = serde_json::from_value(json).expect("deserialize");
265 assert_eq!(restored, block);
266 }
267
268 #[test]
271 fn test_content_block_as_text_some() {
272 let block = ContentBlock::Text(TextBlock::new("greetings"));
273 assert_eq!(block.as_text(), Some("greetings"));
274 }
275
276 #[test]
277 fn test_content_block_as_text_none() {
278 let block = ContentBlock::ToolUse(ToolUseBlock {
279 id: "id".into(),
280 name: "tool".into(),
281 input: Value::Null,
282 extra: Value::Object(Default::default()),
283 });
284 assert_eq!(block.as_text(), None);
285 }
286
287 #[test]
288 fn test_content_block_is_thinking() {
289 let thinking = ContentBlock::Thinking(ThinkingBlock::new("deep thought"));
290 assert!(thinking.is_thinking());
291
292 let text = ContentBlock::Text(TextBlock::new("plain text"));
293 assert!(!text.is_thinking());
294 }
295
296 #[test]
299 fn test_text_block_new() {
300 let tb = TextBlock::new("sample");
301 assert_eq!(tb.text, "sample");
302 assert_eq!(tb.extra, Value::Object(Default::default()));
303 }
304
305 #[test]
306 fn test_thinking_block_new() {
307 let tb = ThinkingBlock::new("pondering");
308 assert_eq!(tb.thinking, "pondering");
309 assert_eq!(tb.extra, Value::Object(Default::default()));
310 }
311
312 #[test]
315 fn test_user_content_text() {
316 let uc = UserContent::text("hi there");
317 assert_eq!(uc, UserContent::Text { text: "hi there".into() });
318
319 let json = serde_json::to_value(&uc).expect("serialize");
320 assert_eq!(json["type"], "text");
321 assert_eq!(json["text"], "hi there");
322
323 let restored: UserContent = serde_json::from_value(json).expect("deserialize");
324 assert_eq!(restored, uc);
325 }
326
327 #[test]
328 fn test_user_content_image_base64() {
329 let uc = UserContent::image_base64("image/png", "abc123==");
330 let expected = UserContent::Image {
331 source: ImageSource::Base64(Base64ImageSource {
332 media_type: "image/png".into(),
333 data: "abc123==".into(),
334 }),
335 };
336 assert_eq!(uc, expected);
337
338 let json = serde_json::to_value(&uc).expect("serialize");
339 assert_eq!(json["type"], "image");
340 assert_eq!(json["source"]["type"], "base64");
341 assert_eq!(json["source"]["media_type"], "image/png");
342 assert_eq!(json["source"]["data"], "abc123==");
343 }
344
345 #[test]
346 fn test_user_content_image_url() {
347 let uc = UserContent::image_url("https://example.com/photo.jpg");
348 let expected = UserContent::Image {
349 source: ImageSource::Url(UrlImageSource {
350 url: "https://example.com/photo.jpg".into(),
351 }),
352 };
353 assert_eq!(uc, expected);
354
355 let json = serde_json::to_value(&uc).expect("serialize");
356 assert_eq!(json["type"], "image");
357 assert_eq!(json["source"]["type"], "url");
358 assert_eq!(json["source"]["url"], "https://example.com/photo.jpg");
359 }
360}