Skip to main content

adk_managed/types/
content.rs

1//! Content block types for managed agent messages.
2//!
3//! Defines [`ContentBlock`], the union type for message content within
4//! the managed agent runtime. Conforms to CANON §3.5 wire shapes.
5
6use serde::{Deserialize, Serialize};
7
8/// Content within a message. Forward-compatible: unknown types are opaque.
9///
10/// Each variant serializes with a `"type"` discriminator tag using snake_case
11/// naming. The enum is `#[non_exhaustive]` to allow additive evolution.
12///
13/// # Wire Shapes (CANON §3.5)
14///
15/// ```json
16/// {"type": "text", "text": "Hello, world!"}
17/// {"type": "image", "source": {"url": "https://example.com/img.png"}}
18/// {"type": "file", "file_id": "file_abc123"}
19/// ```
20///
21/// # Example
22///
23/// ```rust
24/// use adk_managed::types::ContentBlock;
25///
26/// let block = ContentBlock::Text { text: "Hello".to_string() };
27/// let json = serde_json::to_string(&block).unwrap();
28/// assert!(json.contains(r#""type":"text""#));
29/// ```
30#[derive(Debug, Clone, Serialize, Deserialize)]
31#[serde(tag = "type", rename_all = "snake_case")]
32#[non_exhaustive]
33pub enum ContentBlock {
34    /// Plain text content.
35    Text {
36        /// The text content.
37        text: String,
38    },
39    /// Image content.
40    Image {
41        /// Image source descriptor (opaque JSON value for forward-compatibility).
42        source: serde_json::Value,
43    },
44    /// File reference.
45    File {
46        /// The file identifier.
47        file_id: String,
48    },
49}
50
51#[cfg(test)]
52mod tests {
53    use super::*;
54    use serde_json::json;
55
56    #[test]
57    fn test_text_serialization_round_trip() {
58        let block = ContentBlock::Text { text: "Hello, world!".to_string() };
59
60        let serialized = serde_json::to_value(&block).unwrap();
61        assert_eq!(
62            serialized,
63            json!({
64                "type": "text",
65                "text": "Hello, world!"
66            })
67        );
68
69        let deserialized: ContentBlock = serde_json::from_value(serialized).unwrap();
70        match deserialized {
71            ContentBlock::Text { text } => assert_eq!(text, "Hello, world!"),
72            _ => panic!("Expected Text variant"),
73        }
74    }
75
76    #[test]
77    fn test_image_serialization_round_trip() {
78        let source = json!({"url": "https://example.com/img.png", "media_type": "image/png"});
79        let block = ContentBlock::Image { source: source.clone() };
80
81        let serialized = serde_json::to_value(&block).unwrap();
82        assert_eq!(
83            serialized,
84            json!({
85                "type": "image",
86                "source": {"url": "https://example.com/img.png", "media_type": "image/png"}
87            })
88        );
89
90        let deserialized: ContentBlock = serde_json::from_value(serialized).unwrap();
91        match deserialized {
92            ContentBlock::Image { source: deserialized_source } => {
93                assert_eq!(deserialized_source, source);
94            }
95            _ => panic!("Expected Image variant"),
96        }
97    }
98
99    #[test]
100    fn test_file_serialization_round_trip() {
101        let block = ContentBlock::File { file_id: "file_abc123".to_string() };
102
103        let serialized = serde_json::to_value(&block).unwrap();
104        assert_eq!(
105            serialized,
106            json!({
107                "type": "file",
108                "file_id": "file_abc123"
109            })
110        );
111
112        let deserialized: ContentBlock = serde_json::from_value(serialized).unwrap();
113        match deserialized {
114            ContentBlock::File { file_id } => assert_eq!(file_id, "file_abc123"),
115            _ => panic!("Expected File variant"),
116        }
117    }
118
119    #[test]
120    fn test_text_from_json_string() {
121        let json_str = r#"{"type": "text", "text": "sample text"}"#;
122        let block: ContentBlock = serde_json::from_str(json_str).unwrap();
123        match block {
124            ContentBlock::Text { text } => assert_eq!(text, "sample text"),
125            _ => panic!("Expected Text variant"),
126        }
127    }
128
129    #[test]
130    fn test_image_from_json_string() {
131        let json_str =
132            r#"{"type": "image", "source": {"url": "https://cdn.example.com/photo.jpg"}}"#;
133        let block: ContentBlock = serde_json::from_str(json_str).unwrap();
134        match block {
135            ContentBlock::Image { source } => {
136                assert_eq!(source["url"], "https://cdn.example.com/photo.jpg");
137            }
138            _ => panic!("Expected Image variant"),
139        }
140    }
141
142    #[test]
143    fn test_file_from_json_string() {
144        let json_str = r#"{"type": "file", "file_id": "file_xyz789"}"#;
145        let block: ContentBlock = serde_json::from_str(json_str).unwrap();
146        match block {
147            ContentBlock::File { file_id } => assert_eq!(file_id, "file_xyz789"),
148            _ => panic!("Expected File variant"),
149        }
150    }
151
152    #[test]
153    fn test_unknown_type_rejected() {
154        let json_str = r#"{"type": "video", "url": "https://example.com/vid.mp4"}"#;
155        let result: Result<ContentBlock, _> = serde_json::from_str(json_str);
156        assert!(result.is_err(), "Unknown type should be rejected");
157    }
158
159    #[test]
160    fn test_vec_content_blocks_round_trip() {
161        let blocks = vec![
162            ContentBlock::Text { text: "Here is an image:".to_string() },
163            ContentBlock::Image { source: json!({"url": "https://example.com/img.png"}) },
164            ContentBlock::File { file_id: "attachment_001".to_string() },
165        ];
166
167        let serialized = serde_json::to_value(&blocks).unwrap();
168        let deserialized: Vec<ContentBlock> = serde_json::from_value(serialized).unwrap();
169
170        assert_eq!(deserialized.len(), 3);
171        match &deserialized[0] {
172            ContentBlock::Text { text } => assert_eq!(text, "Here is an image:"),
173            _ => panic!("Expected Text"),
174        }
175        match &deserialized[1] {
176            ContentBlock::Image { source } => {
177                assert_eq!(source["url"], "https://example.com/img.png");
178            }
179            _ => panic!("Expected Image"),
180        }
181        match &deserialized[2] {
182            ContentBlock::File { file_id } => assert_eq!(file_id, "attachment_001"),
183            _ => panic!("Expected File"),
184        }
185    }
186
187    #[test]
188    fn test_debug_impl() {
189        let block = ContentBlock::Text { text: "test".to_string() };
190        let debug_str = format!("{block:?}");
191        assert!(debug_str.contains("Text"));
192        assert!(debug_str.contains("test"));
193    }
194
195    #[test]
196    fn test_clone_impl() {
197        let block = ContentBlock::Image { source: json!({"url": "https://example.com/img.png"}) };
198        let cloned = block.clone();
199        let original_json = serde_json::to_value(&block).unwrap();
200        let cloned_json = serde_json::to_value(&cloned).unwrap();
201        assert_eq!(original_json, cloned_json);
202    }
203}