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