turboclaude_protocol/
content.rs

1//! Content block types
2//!
3//! Represents the different types of content that can appear in messages.
4//! Matches the Anthropic API's content block structure.
5
6use serde::{Deserialize, Serialize};
7
8/// A content block in a message
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
10#[serde(tag = "type", rename_all = "snake_case")]
11pub enum ContentBlock {
12    /// Plain text content.
13    #[serde(rename = "text")]
14    Text {
15        /// The text content.
16        text: String,
17    },
18
19    /// Image content.
20    #[serde(rename = "image")]
21    Image {
22        /// The source of the image data.
23        #[serde(skip_serializing_if = "Option::is_none")]
24        source: Option<ImageSource>,
25    },
26
27    /// A request from the model to use a tool.
28    #[serde(rename = "tool_use")]
29    ToolUse {
30        /// The unique identifier for this tool use request.
31        id: String,
32        /// The name of the tool to be used.
33        name: String,
34        /// The input to the tool, as a JSON object.
35        #[serde(default)]
36        input: serde_json::Value,
37    },
38
39    /// The result of a tool execution.
40    #[serde(rename = "tool_result")]
41    ToolResult {
42        /// The `id` of the `tool_use` block this result is for.
43        tool_use_id: String,
44        /// The content of the tool's output.
45        #[serde(skip_serializing_if = "Option::is_none")]
46        content: Option<String>,
47        /// Whether the tool execution resulted in an error.
48        #[serde(skip_serializing_if = "Option::is_none")]
49        is_error: Option<bool>,
50    },
51
52    /// A block indicating that the model is performing an extended computation.
53    #[serde(rename = "thinking")]
54    Thinking {
55        /// A description of the thinking process.
56        thinking: String,
57    },
58
59    /// Content from a document.
60    #[serde(rename = "document")]
61    Document {
62        /// The source of the document.
63        source: DocumentSource,
64        /// The title of the document.
65        #[serde(skip_serializing_if = "Option::is_none")]
66        title: Option<String>,
67    },
68}
69
70/// Image source specification
71#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
72#[serde(tag = "type", rename_all = "snake_case")]
73pub enum ImageSource {
74    /// A base64-encoded image.
75    #[serde(rename = "base64")]
76    Base64 {
77        /// The media type of the image (e.g., "image/jpeg").
78        media_type: String,
79        /// The base64-encoded image data.
80        data: String,
81    },
82
83    /// An image referenced by a URL.
84    #[serde(rename = "url")]
85    Url {
86        /// The URL of the image.
87        url: String,
88    },
89}
90
91/// Document source specification
92#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
93#[serde(tag = "type", rename_all = "snake_case")]
94pub enum DocumentSource {
95    /// A base64-encoded PDF document.
96    #[serde(rename = "pdf")]
97    Pdf {
98        /// The base64-encoded PDF data.
99        data: String,
100    },
101
102    /// A plain text document.
103    #[serde(rename = "text")]
104    Text {
105        /// The text content of the document.
106        text: String,
107    },
108
109    /// A document referenced by a URL.
110    #[serde(rename = "url")]
111    Url {
112        /// The URL of the document.
113        url: String,
114    },
115}
116
117impl ContentBlock {
118    /// Create a text content block
119    pub fn text(text: impl Into<String>) -> Self {
120        Self::Text { text: text.into() }
121    }
122
123    /// Create a tool use content block
124    pub fn tool_use(
125        id: impl Into<String>,
126        name: impl Into<String>,
127        input: serde_json::Value,
128    ) -> Self {
129        Self::ToolUse {
130            id: id.into(),
131            name: name.into(),
132            input,
133        }
134    }
135
136    /// Create a tool result content block
137    pub fn tool_result(tool_use_id: impl Into<String>, content: impl Into<String>) -> Self {
138        Self::ToolResult {
139            tool_use_id: tool_use_id.into(),
140            content: Some(content.into()),
141            is_error: None,
142        }
143    }
144
145    /// Create an error tool result
146    pub fn tool_error(tool_use_id: impl Into<String>, error: impl Into<String>) -> Self {
147        Self::ToolResult {
148            tool_use_id: tool_use_id.into(),
149            content: Some(error.into()),
150            is_error: Some(true),
151        }
152    }
153
154    /// Create a thinking content block
155    pub fn thinking(thinking: impl Into<String>) -> Self {
156        Self::Thinking {
157            thinking: thinking.into(),
158        }
159    }
160
161    /// Get the type name of this content block
162    pub fn type_name(&self) -> &'static str {
163        match self {
164            Self::Text { .. } => "text",
165            Self::Image { .. } => "image",
166            Self::ToolUse { .. } => "tool_use",
167            Self::ToolResult { .. } => "tool_result",
168            Self::Thinking { .. } => "thinking",
169            Self::Document { .. } => "document",
170        }
171    }
172
173    /// Check if this is a text block
174    pub fn is_text(&self) -> bool {
175        matches!(self, Self::Text { .. })
176    }
177
178    /// Check if this is a tool use block
179    pub fn is_tool_use(&self) -> bool {
180        matches!(self, Self::ToolUse { .. })
181    }
182
183    /// Check if this is a tool result block
184    pub fn is_tool_result(&self) -> bool {
185        matches!(self, Self::ToolResult { .. })
186    }
187
188    /// Extract text if this is a text block
189    pub fn as_text(&self) -> Option<&str> {
190        match self {
191            Self::Text { text } => Some(text),
192            _ => None,
193        }
194    }
195
196    /// Extract tool use if this is a tool use block
197    pub fn as_tool_use(&self) -> Option<(&str, &str, &serde_json::Value)> {
198        match self {
199            Self::ToolUse { id, name, input } => Some((id, name, input)),
200            _ => None,
201        }
202    }
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208
209    #[test]
210    fn test_text_content_serialization() {
211        let content = ContentBlock::text("Hello, world!");
212        let json = serde_json::to_string(&content).unwrap();
213        let deserialized: ContentBlock = serde_json::from_str(&json).unwrap();
214        assert_eq!(content, deserialized);
215    }
216
217    #[test]
218    fn test_tool_use_content() {
219        let content =
220            ContentBlock::tool_use("id_123", "bash", serde_json::json!({ "command": "ls" }));
221        assert!(content.is_tool_use());
222        assert_eq!(content.type_name(), "tool_use");
223    }
224
225    #[test]
226    fn test_content_type_checks() {
227        let text = ContentBlock::text("test");
228        assert!(text.is_text());
229        assert!(!text.is_tool_use());
230    }
231}