Skip to main content

claude_codes/io/
content_blocks.rs

1use serde::{Deserialize, Deserializer, Serialize};
2use serde_json::Value;
3
4/// Deserialize content blocks that can be either a string or array
5pub(crate) fn deserialize_content_blocks<'de, D>(
6    deserializer: D,
7) -> Result<Vec<ContentBlock>, D::Error>
8where
9    D: Deserializer<'de>,
10{
11    let value: Value = Value::deserialize(deserializer)?;
12    match value {
13        Value::String(s) => Ok(vec![ContentBlock::Text(TextBlock { text: s })]),
14        Value::Array(_) => serde_json::from_value(value).map_err(serde::de::Error::custom),
15        _ => Err(serde::de::Error::custom(
16            "content must be a string or array",
17        )),
18    }
19}
20
21/// Content blocks for messages
22#[derive(Debug, Clone, Serialize, Deserialize)]
23#[serde(tag = "type", rename_all = "snake_case")]
24pub enum ContentBlock {
25    Text(TextBlock),
26    Image(ImageBlock),
27    Thinking(ThinkingBlock),
28    ToolUse(ToolUseBlock),
29    ToolResult(ToolResultBlock),
30}
31
32/// Text content block
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct TextBlock {
35    pub text: String,
36}
37
38/// Image content block (follows Anthropic API structure)
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct ImageBlock {
41    pub source: ImageSource,
42}
43
44/// Image source information
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct ImageSource {
47    #[serde(rename = "type")]
48    pub source_type: String, // "base64"
49    pub media_type: String, // e.g., "image/jpeg", "image/png"
50    pub data: String,       // Base64-encoded image data
51}
52
53/// Thinking content block
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct ThinkingBlock {
56    pub thinking: String,
57    pub signature: String,
58}
59
60/// Tool use content block
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct ToolUseBlock {
63    pub id: String,
64    pub name: String,
65    pub input: Value,
66}
67
68impl ToolUseBlock {
69    /// Try to parse the input as a typed ToolInput.
70    ///
71    /// This attempts to deserialize the raw JSON input into a strongly-typed
72    /// `ToolInput` enum variant. Returns `None` if parsing fails.
73    ///
74    /// # Example
75    ///
76    /// ```
77    /// use claude_codes::{ToolUseBlock, ToolInput};
78    /// use serde_json::json;
79    ///
80    /// let block = ToolUseBlock {
81    ///     id: "toolu_123".to_string(),
82    ///     name: "Bash".to_string(),
83    ///     input: json!({"command": "ls -la"}),
84    /// };
85    ///
86    /// if let Some(ToolInput::Bash(bash)) = block.typed_input() {
87    ///     assert_eq!(bash.command, "ls -la");
88    /// }
89    /// ```
90    pub fn typed_input(&self) -> Option<crate::tool_inputs::ToolInput> {
91        serde_json::from_value(self.input.clone()).ok()
92    }
93
94    /// Parse the input as a typed ToolInput, returning an error on failure.
95    ///
96    /// Unlike `typed_input()`, this method returns the parsing error for debugging.
97    pub fn try_typed_input(&self) -> Result<crate::tool_inputs::ToolInput, serde_json::Error> {
98        serde_json::from_value(self.input.clone())
99    }
100}
101
102/// Tool result content block
103#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct ToolResultBlock {
105    pub tool_use_id: String,
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub content: Option<ToolResultContent>,
108    #[serde(skip_serializing_if = "Option::is_none")]
109    pub is_error: Option<bool>,
110}
111
112/// Tool result content type
113#[derive(Debug, Clone, Serialize, Deserialize)]
114#[serde(untagged)]
115pub enum ToolResultContent {
116    Text(String),
117    Structured(Vec<Value>),
118}