Skip to main content

claude_code_rs/types/
content.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4/// A block of content within a message.
5#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
6#[serde(tag = "type", rename_all = "snake_case")]
7pub enum ContentBlock {
8    Text {
9        text: String,
10    },
11    Thinking {
12        thinking: String,
13        #[serde(default)]
14        signature: Option<String>,
15    },
16    ToolUse {
17        id: String,
18        name: String,
19        input: Value,
20    },
21    ToolResult {
22        tool_use_id: String,
23        content: ToolResultContent,
24        #[serde(default)]
25        is_error: bool,
26    },
27}
28
29/// Content of a tool result - can be a simple string or structured blocks.
30#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
31#[serde(untagged)]
32pub enum ToolResultContent {
33    Text(String),
34    Blocks(Vec<ToolResultBlock>),
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
38#[serde(tag = "type", rename_all = "snake_case")]
39pub enum ToolResultBlock {
40    Text { text: String },
41    Image { source: ImageSource },
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
45pub struct ImageSource {
46    #[serde(rename = "type")]
47    pub source_type: String,
48    pub media_type: String,
49    pub data: String,
50}
51
52impl ContentBlock {
53    /// Extract text content if this is a Text block.
54    pub fn as_text(&self) -> Option<&str> {
55        match self {
56            ContentBlock::Text { text } => Some(text),
57            _ => None,
58        }
59    }
60
61    /// Extract thinking content if this is a Thinking block.
62    pub fn as_thinking(&self) -> Option<&str> {
63        match self {
64            ContentBlock::Thinking { thinking, .. } => Some(thinking),
65            _ => None,
66        }
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73
74    #[test]
75    fn deserialize_text_block() {
76        let json = r#"{"type": "text", "text": "hello"}"#;
77        let block: ContentBlock = serde_json::from_str(json).unwrap();
78        assert_eq!(block.as_text(), Some("hello"));
79    }
80
81    #[test]
82    fn deserialize_tool_use_block() {
83        let json = r#"{"type": "tool_use", "id": "tu_1", "name": "Bash", "input": {"command": "ls"}}"#;
84        let block: ContentBlock = serde_json::from_str(json).unwrap();
85        match block {
86            ContentBlock::ToolUse { id, name, input } => {
87                assert_eq!(id, "tu_1");
88                assert_eq!(name, "Bash");
89                assert_eq!(input["command"], "ls");
90            }
91            _ => panic!("expected ToolUse"),
92        }
93    }
94
95    #[test]
96    fn deserialize_tool_result_block() {
97        let json = r#"{"type": "tool_result", "tool_use_id": "tu_1", "content": "ok", "is_error": false}"#;
98        let block: ContentBlock = serde_json::from_str(json).unwrap();
99        match block {
100            ContentBlock::ToolResult {
101                tool_use_id,
102                content,
103                is_error,
104            } => {
105                assert_eq!(tool_use_id, "tu_1");
106                assert_eq!(content, ToolResultContent::Text("ok".into()));
107                assert!(!is_error);
108            }
109            _ => panic!("expected ToolResult"),
110        }
111    }
112
113    #[test]
114    fn roundtrip_content_block() {
115        let block = ContentBlock::Thinking {
116            thinking: "hmm".into(),
117            signature: Some("sig".into()),
118        };
119        let json = serde_json::to_string(&block).unwrap();
120        let back: ContentBlock = serde_json::from_str(&json).unwrap();
121        assert_eq!(block, back);
122    }
123}