1use serde::{Deserialize, Serialize};
4
5const SUPPORTED_IMAGE_MIME_TYPES: &[&str] = &["image/jpeg", "image/png", "image/gif", "image/webp"];
7
8const MAX_BASE64_SIZE: usize = 15_728_640;
10
11#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
13#[serde(rename_all = "snake_case")]
14pub enum AssistantMessageError {
15 AuthenticationFailed,
17 BillingError,
19 RateLimit,
21 InvalidRequest,
23 ServerError,
25 Unknown,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
31#[serde(tag = "type", rename_all = "lowercase")]
32pub enum Message {
33 #[serde(rename = "assistant")]
35 Assistant(AssistantMessage),
36 #[serde(rename = "system")]
38 System(SystemMessage),
39 #[serde(rename = "result")]
41 Result(ResultMessage),
42 #[serde(rename = "stream_event")]
44 StreamEvent(StreamEvent),
45 #[serde(rename = "user")]
47 User(UserMessage),
48 #[serde(rename = "control_cancel_request")]
50 ControlCancelRequest(serde_json::Value),
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct UserMessage {
56 #[serde(skip_serializing_if = "Option::is_none")]
58 pub text: Option<String>,
59 #[serde(skip_serializing_if = "Option::is_none")]
61 pub content: Option<Vec<ContentBlock>>,
62 #[serde(skip_serializing_if = "Option::is_none")]
64 pub uuid: Option<String>,
65 #[serde(skip_serializing_if = "Option::is_none")]
67 pub parent_tool_use_id: Option<String>,
68 #[serde(flatten)]
70 pub extra: serde_json::Value,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
75#[serde(untagged)]
76pub enum MessageContent {
77 Text { text: String },
79 Blocks { content: Vec<ContentBlock> },
81}
82
83impl From<String> for MessageContent {
84 fn from(text: String) -> Self {
85 MessageContent::Text { text }
86 }
87}
88
89impl From<&str> for MessageContent {
90 fn from(text: &str) -> Self {
91 MessageContent::Text {
92 text: text.to_string(),
93 }
94 }
95}
96
97impl From<Vec<ContentBlock>> for MessageContent {
98 fn from(blocks: Vec<ContentBlock>) -> Self {
99 MessageContent::Blocks { content: blocks }
100 }
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct AssistantMessage {
106 pub message: AssistantMessageInner,
108 #[serde(skip_serializing_if = "Option::is_none")]
110 pub parent_tool_use_id: Option<String>,
111 #[serde(skip_serializing_if = "Option::is_none")]
113 pub session_id: Option<String>,
114 #[serde(skip_serializing_if = "Option::is_none")]
116 pub uuid: Option<String>,
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct AssistantMessageInner {
122 #[serde(default)]
124 pub content: Vec<ContentBlock>,
125 #[serde(skip_serializing_if = "Option::is_none")]
127 pub model: Option<String>,
128 #[serde(skip_serializing_if = "Option::is_none")]
130 pub id: Option<String>,
131 #[serde(skip_serializing_if = "Option::is_none")]
133 pub stop_reason: Option<String>,
134 #[serde(skip_serializing_if = "Option::is_none")]
136 pub usage: Option<serde_json::Value>,
137 #[serde(skip_serializing_if = "Option::is_none")]
139 pub error: Option<AssistantMessageError>,
140}
141
142#[derive(Debug, Clone, Serialize, Deserialize)]
144pub struct SystemMessage {
145 pub subtype: String,
147 #[serde(skip_serializing_if = "Option::is_none")]
149 pub cwd: Option<String>,
150 #[serde(skip_serializing_if = "Option::is_none")]
152 pub session_id: Option<String>,
153 #[serde(skip_serializing_if = "Option::is_none")]
155 pub tools: Option<Vec<String>>,
156 #[serde(skip_serializing_if = "Option::is_none")]
158 pub mcp_servers: Option<Vec<serde_json::Value>>,
159 #[serde(skip_serializing_if = "Option::is_none")]
161 pub model: Option<String>,
162 #[serde(skip_serializing_if = "Option::is_none", rename = "permissionMode")]
164 pub permission_mode: Option<String>,
165 #[serde(skip_serializing_if = "Option::is_none")]
167 pub uuid: Option<String>,
168 #[serde(flatten)]
170 pub data: serde_json::Value,
171}
172
173#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct ResultMessage {
176 pub subtype: String,
178 pub duration_ms: u64,
180 pub duration_api_ms: u64,
182 pub is_error: bool,
184 pub num_turns: u32,
186 pub session_id: String,
188 #[serde(skip_serializing_if = "Option::is_none")]
190 pub total_cost_usd: Option<f64>,
191 #[serde(skip_serializing_if = "Option::is_none")]
193 pub usage: Option<serde_json::Value>,
194 #[serde(skip_serializing_if = "Option::is_none")]
196 pub result: Option<String>,
197 #[serde(skip_serializing_if = "Option::is_none")]
199 pub structured_output: Option<serde_json::Value>,
200}
201
202#[derive(Debug, Clone, Serialize, Deserialize)]
204pub struct StreamEvent {
205 pub uuid: String,
207 pub session_id: String,
209 pub event: serde_json::Value,
211 #[serde(skip_serializing_if = "Option::is_none")]
213 pub parent_tool_use_id: Option<String>,
214}
215
216#[derive(Debug, Clone, Serialize, Deserialize)]
218#[serde(tag = "type", rename_all = "snake_case")]
219pub enum ContentBlock {
220 Text(TextBlock),
222 Thinking(ThinkingBlock),
224 ToolUse(ToolUseBlock),
226 ToolResult(ToolResultBlock),
228 Image(ImageBlock),
230}
231
232#[derive(Debug, Clone, Serialize, Deserialize)]
234pub struct TextBlock {
235 pub text: String,
237}
238
239#[derive(Debug, Clone, Serialize, Deserialize)]
241pub struct ThinkingBlock {
242 pub thinking: String,
244 pub signature: String,
246}
247
248#[derive(Debug, Clone, Serialize, Deserialize)]
250pub struct ToolUseBlock {
251 pub id: String,
253 pub name: String,
255 pub input: serde_json::Value,
257}
258
259#[derive(Debug, Clone, Serialize, Deserialize)]
261pub struct ToolResultBlock {
262 pub tool_use_id: String,
264 #[serde(skip_serializing_if = "Option::is_none")]
266 pub content: Option<ToolResultContent>,
267 #[serde(skip_serializing_if = "Option::is_none")]
269 pub is_error: Option<bool>,
270}
271
272#[derive(Debug, Clone, Serialize, Deserialize)]
274#[serde(untagged)]
275pub enum ToolResultContent {
276 Text(String),
278 Blocks(Vec<serde_json::Value>),
280}
281
282#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
294#[serde(tag = "type", rename_all = "snake_case")]
295pub enum ImageSource {
296 Base64 {
298 media_type: String,
300 data: String,
302 },
303 Url {
305 url: String,
307 },
308}
309
310#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
312pub struct ImageBlock {
313 pub source: ImageSource,
315}
316
317#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
321#[serde(tag = "type", rename_all = "snake_case")]
322pub enum UserContentBlock {
323 Text {
325 text: String,
327 },
328 Image {
330 source: ImageSource,
332 },
333}
334
335impl UserContentBlock {
336 pub fn text(text: impl Into<String>) -> Self {
338 UserContentBlock::Text { text: text.into() }
339 }
340
341 pub fn image_base64(
362 media_type: impl Into<String>,
363 data: impl Into<String>,
364 ) -> crate::errors::Result<Self> {
365 let media_type_str = media_type.into();
366 let data_str = data.into();
367
368 if !SUPPORTED_IMAGE_MIME_TYPES.contains(&media_type_str.as_str()) {
370 return Err(crate::errors::ImageValidationError::new(format!(
371 "Unsupported media type '{}'. Supported types: {:?}",
372 media_type_str, SUPPORTED_IMAGE_MIME_TYPES
373 ))
374 .into());
375 }
376
377 if data_str.len() > MAX_BASE64_SIZE {
379 return Err(crate::errors::ImageValidationError::new(format!(
380 "Base64 data exceeds maximum size of {} bytes (got {} bytes)",
381 MAX_BASE64_SIZE,
382 data_str.len()
383 ))
384 .into());
385 }
386
387 Ok(UserContentBlock::Image {
388 source: ImageSource::Base64 {
389 media_type: media_type_str,
390 data: data_str,
391 },
392 })
393 }
394
395 pub fn image_url(url: impl Into<String>) -> Self {
397 UserContentBlock::Image {
398 source: ImageSource::Url { url: url.into() },
399 }
400 }
401
402 pub fn validate_content(blocks: &[UserContentBlock]) -> crate::Result<()> {
411 if blocks.is_empty() {
412 return Err(crate::errors::ClaudeError::InvalidConfig(
413 "Content must include at least one block (text or image)".to_string(),
414 ));
415 }
416 Ok(())
417 }
418}
419
420impl From<String> for UserContentBlock {
421 fn from(text: String) -> Self {
422 UserContentBlock::Text { text }
423 }
424}
425
426impl From<&str> for UserContentBlock {
427 fn from(text: &str) -> Self {
428 UserContentBlock::Text {
429 text: text.to_string(),
430 }
431 }
432}
433
434#[cfg(test)]
435mod tests {
436 use super::*;
437 use serde_json::json;
438
439 #[test]
440 fn test_content_block_text_serialization() {
441 let block = ContentBlock::Text(TextBlock {
442 text: "Hello".to_string(),
443 });
444
445 let json = serde_json::to_value(&block).unwrap();
446 assert_eq!(json["type"], "text");
447 assert_eq!(json["text"], "Hello");
448 }
449
450 #[test]
451 fn test_content_block_tool_use_serialization() {
452 let block = ContentBlock::ToolUse(ToolUseBlock {
453 id: "tool_123".to_string(),
454 name: "Bash".to_string(),
455 input: json!({"command": "echo hello"}),
456 });
457
458 let json = serde_json::to_value(&block).unwrap();
459 assert_eq!(json["type"], "tool_use");
460 assert_eq!(json["id"], "tool_123");
461 assert_eq!(json["name"], "Bash");
462 assert_eq!(json["input"]["command"], "echo hello");
463 }
464
465 #[test]
466 fn test_message_assistant_deserialization() {
467 let json_str = r#"{
468 "type": "assistant",
469 "message": {
470 "content": [{"type": "text", "text": "Hello"}],
471 "model": "claude-sonnet-4"
472 },
473 "session_id": "test-session"
474 }"#;
475
476 let msg: Message = serde_json::from_str(json_str).unwrap();
477 match msg {
478 Message::Assistant(assistant) => {
479 assert_eq!(assistant.session_id, Some("test-session".to_string()));
480 assert_eq!(assistant.message.model, Some("claude-sonnet-4".to_string()));
481 },
482 _ => panic!("Expected Assistant variant"),
483 }
484 }
485
486 #[test]
487 fn test_message_result_deserialization() {
488 let json_str = r#"{
489 "type": "result",
490 "subtype": "query_complete",
491 "duration_ms": 1500,
492 "duration_api_ms": 1200,
493 "is_error": false,
494 "num_turns": 3,
495 "session_id": "test-session",
496 "total_cost_usd": 0.0042
497 }"#;
498
499 let msg: Message = serde_json::from_str(json_str).unwrap();
500 match msg {
501 Message::Result(result) => {
502 assert_eq!(result.subtype, "query_complete");
503 assert_eq!(result.duration_ms, 1500);
504 assert_eq!(result.num_turns, 3);
505 assert_eq!(result.total_cost_usd, Some(0.0042));
506 },
507 _ => panic!("Expected Result variant"),
508 }
509 }
510
511 #[test]
512 fn test_message_system_deserialization() {
513 let json_str = r#"{
514 "type": "system",
515 "subtype": "session_start",
516 "cwd": "/home/user",
517 "session_id": "test-session",
518 "tools": ["Bash", "Read", "Write"]
519 }"#;
520
521 let msg: Message = serde_json::from_str(json_str).unwrap();
522 match msg {
523 Message::System(system) => {
524 assert_eq!(system.subtype, "session_start");
525 assert_eq!(system.cwd, Some("/home/user".to_string()));
526 assert_eq!(system.tools.as_ref().unwrap().len(), 3);
527 },
528 _ => panic!("Expected System variant"),
529 }
530 }
531
532 #[test]
533 fn test_tool_result_content_text() {
534 let content = ToolResultContent::Text("Command output".to_string());
535 let json = serde_json::to_value(&content).unwrap();
536 assert_eq!(json, "Command output");
537 }
538
539 #[test]
540 fn test_tool_result_content_blocks() {
541 let content = ToolResultContent::Blocks(vec![json!({"type": "text", "text": "Result"})]);
542 let json = serde_json::to_value(&content).unwrap();
543 assert!(json.is_array());
544 assert_eq!(json[0]["type"], "text");
545 }
546
547 #[test]
548 fn test_image_source_base64_serialization() {
549 let source = ImageSource::Base64 {
550 media_type: "image/png".to_string(),
551 data: "iVBORw0KGgo=".to_string(),
552 };
553
554 let json = serde_json::to_value(&source).unwrap();
555 assert_eq!(json["type"], "base64");
556 assert_eq!(json["media_type"], "image/png");
557 assert_eq!(json["data"], "iVBORw0KGgo=");
558 }
559
560 #[test]
561 fn test_image_source_url_serialization() {
562 let source = ImageSource::Url {
563 url: "https://example.com/image.png".to_string(),
564 };
565
566 let json = serde_json::to_value(&source).unwrap();
567 assert_eq!(json["type"], "url");
568 assert_eq!(json["url"], "https://example.com/image.png");
569 }
570
571 #[test]
572 fn test_image_source_base64_deserialization() {
573 let json_str = r#"{
574 "type": "base64",
575 "media_type": "image/jpeg",
576 "data": "base64data=="
577 }"#;
578
579 let source: ImageSource = serde_json::from_str(json_str).unwrap();
580 match source {
581 ImageSource::Base64 { media_type, data } => {
582 assert_eq!(media_type, "image/jpeg");
583 assert_eq!(data, "base64data==");
584 },
585 _ => panic!("Expected Base64 variant"),
586 }
587 }
588
589 #[test]
590 fn test_image_source_url_deserialization() {
591 let json_str = r#"{
592 "type": "url",
593 "url": "https://example.com/test.gif"
594 }"#;
595
596 let source: ImageSource = serde_json::from_str(json_str).unwrap();
597 match source {
598 ImageSource::Url { url } => {
599 assert_eq!(url, "https://example.com/test.gif");
600 },
601 _ => panic!("Expected Url variant"),
602 }
603 }
604
605 #[test]
606 fn test_user_content_block_text_serialization() {
607 let block = UserContentBlock::text("Hello world");
608
609 let json = serde_json::to_value(&block).unwrap();
610 assert_eq!(json["type"], "text");
611 assert_eq!(json["text"], "Hello world");
612 }
613
614 #[test]
615 fn test_user_content_block_image_base64_serialization() {
616 let block = UserContentBlock::image_base64("image/png", "iVBORw0KGgo=").unwrap();
617
618 let json = serde_json::to_value(&block).unwrap();
619 assert_eq!(json["type"], "image");
620 assert_eq!(json["source"]["type"], "base64");
621 assert_eq!(json["source"]["media_type"], "image/png");
622 assert_eq!(json["source"]["data"], "iVBORw0KGgo=");
623 }
624
625 #[test]
626 fn test_user_content_block_image_url_serialization() {
627 let block = UserContentBlock::image_url("https://example.com/image.webp");
628
629 let json = serde_json::to_value(&block).unwrap();
630 assert_eq!(json["type"], "image");
631 assert_eq!(json["source"]["type"], "url");
632 assert_eq!(json["source"]["url"], "https://example.com/image.webp");
633 }
634
635 #[test]
636 fn test_user_content_block_from_string() {
637 let block: UserContentBlock = "Test message".into();
638
639 match block {
640 UserContentBlock::Text { text } => {
641 assert_eq!(text, "Test message");
642 },
643 _ => panic!("Expected Text variant"),
644 }
645 }
646
647 #[test]
648 fn test_user_content_block_from_owned_string() {
649 let block: UserContentBlock = String::from("Owned message").into();
650
651 match block {
652 UserContentBlock::Text { text } => {
653 assert_eq!(text, "Owned message");
654 },
655 _ => panic!("Expected Text variant"),
656 }
657 }
658
659 #[test]
660 fn test_image_block_serialization() {
661 let block = ImageBlock {
662 source: ImageSource::Base64 {
663 media_type: "image/gif".to_string(),
664 data: "R0lGODlh".to_string(),
665 },
666 };
667
668 let json = serde_json::to_value(&block).unwrap();
669 assert_eq!(json["source"]["type"], "base64");
670 assert_eq!(json["source"]["media_type"], "image/gif");
671 assert_eq!(json["source"]["data"], "R0lGODlh");
672 }
673
674 #[test]
675 fn test_content_block_image_serialization() {
676 let block = ContentBlock::Image(ImageBlock {
677 source: ImageSource::Url {
678 url: "https://example.com/photo.jpg".to_string(),
679 },
680 });
681
682 let json = serde_json::to_value(&block).unwrap();
683 assert_eq!(json["type"], "image");
684 assert_eq!(json["source"]["type"], "url");
685 assert_eq!(json["source"]["url"], "https://example.com/photo.jpg");
686 }
687
688 #[test]
689 fn test_content_block_image_deserialization() {
690 let json_str = r#"{
691 "type": "image",
692 "source": {
693 "type": "base64",
694 "media_type": "image/webp",
695 "data": "UklGR"
696 }
697 }"#;
698
699 let block: ContentBlock = serde_json::from_str(json_str).unwrap();
700 match block {
701 ContentBlock::Image(image) => match image.source {
702 ImageSource::Base64 { media_type, data } => {
703 assert_eq!(media_type, "image/webp");
704 assert_eq!(data, "UklGR");
705 },
706 _ => panic!("Expected Base64 source"),
707 },
708 _ => panic!("Expected Image variant"),
709 }
710 }
711
712 #[test]
713 fn test_user_content_block_deserialization() {
714 let json_str = r#"{
715 "type": "text",
716 "text": "Describe this image"
717 }"#;
718
719 let block: UserContentBlock = serde_json::from_str(json_str).unwrap();
720 match block {
721 UserContentBlock::Text { text } => {
722 assert_eq!(text, "Describe this image");
723 },
724 _ => panic!("Expected Text variant"),
725 }
726 }
727
728 #[test]
729 fn test_user_content_block_image_deserialization() {
730 let json_str = r#"{
731 "type": "image",
732 "source": {
733 "type": "url",
734 "url": "https://example.com/diagram.png"
735 }
736 }"#;
737
738 let block: UserContentBlock = serde_json::from_str(json_str).unwrap();
739 match block {
740 UserContentBlock::Image { source } => match source {
741 ImageSource::Url { url } => {
742 assert_eq!(url, "https://example.com/diagram.png");
743 },
744 _ => panic!("Expected Url source"),
745 },
746 _ => panic!("Expected Image variant"),
747 }
748 }
749
750 #[test]
751 fn test_image_base64_valid() {
752 let block = UserContentBlock::image_base64("image/png", "iVBORw0KGgo=");
753 assert!(block.is_ok());
754 }
755
756 #[test]
757 fn test_image_base64_invalid_mime_type() {
758 let block = UserContentBlock::image_base64("image/bmp", "data");
759 assert!(block.is_err());
760 let err = block.unwrap_err().to_string();
761 assert!(err.contains("Unsupported media type"));
762 assert!(err.contains("image/bmp"));
763 }
764
765 #[test]
766 fn test_image_base64_exceeds_size_limit() {
767 let large_data = "a".repeat(MAX_BASE64_SIZE + 1);
768 let block = UserContentBlock::image_base64("image/png", large_data);
769 assert!(block.is_err());
770 let err = block.unwrap_err().to_string();
771 assert!(err.contains("exceeds maximum size"));
772 }
773}