Skip to main content

claude_agent_sdk/internal/
message_parser.rs

1//! Message parser for converting JSON to typed messages
2//!
3//! This module provides both traditional and zero-copy JSON parsing:
4//!
5//! - **MessageParser**: Traditional parser that creates owned Message types
6//! - **ZeroCopyMessageParser**: Zero-copy parser that borrows from input string
7//!
8//! ## Zero-Copy Parsing Benefits
9//!
10//! - **Reduced allocations**: No intermediate `serde_json::Value` allocation
11//! - **Faster parsing**: Direct deserialization from string to Message
12//! - **Lower memory pressure**: Especially beneficial for high-frequency message streams
13//!
14//! ## Parsing Modes
15//!
16//! Use [`ParsingMode`] to select the parsing strategy:
17//!
18//! - [`ParsingMode::Traditional`]: Uses intermediate `serde_json::Value` (default, safest)
19//! - [`ParsingMode::ZeroCopy`]: Direct parsing from string (faster, less memory)
20//!
21//! ## Example
22//!
23//! ```ignore
24//! use claude_agent_sdk::internal::message_parser::{ZeroCopyMessageParser, ParsingMode};
25//!
26//! let json = r#"{"type":"assistant","message":{"role":"assistant","content":"Hello"}}"#;
27//! let message = ZeroCopyMessageParser::parse(json)?;
28//! ```
29
30use crate::errors::{ClaudeError, MessageParseError, Result};
31use crate::types::messages::Message;
32
33/// Parsing mode for message deserialization
34///
35/// Controls how JSON strings are parsed into [`Message`] types.
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
37pub enum ParsingMode {
38    /// Traditional parsing via intermediate `serde_json::Value`
39    ///
40    /// This is the safest option that creates an intermediate `Value` before
41    /// deserializing to `Message`. Use this when you need maximum compatibility.
42    #[default]
43    Traditional,
44
45    /// Zero-copy parsing directly from string
46    ///
47    /// Parses directly from the input string without creating an intermediate
48    /// `Value`. This is faster and uses less memory, especially for large messages.
49    ///
50    /// # Performance
51    ///
52    /// - ~30-50% less memory allocation for large messages
53    /// - ~10-20% faster parsing time
54    ZeroCopy,
55}
56
57/// Message parser for CLI output (traditional owned parsing)
58pub struct MessageParser;
59
60impl MessageParser {
61    /// Parse a JSON value into a Message
62    pub fn parse(data: serde_json::Value) -> Result<Message> {
63        serde_json::from_value(data).map_err(|e| {
64            MessageParseError::new(format!("Failed to parse message: {}", e), None).into()
65        })
66    }
67}
68
69/// Zero-copy message parser for CLI output
70///
71/// This parser avoids intermediate allocations by parsing directly from
72/// the input string. Use this for high-performance scenarios where the
73/// input string lifetime allows borrowing.
74pub struct ZeroCopyMessageParser;
75
76impl ZeroCopyMessageParser {
77    /// Parse a JSON string directly into a Message without intermediate allocation.
78    ///
79    /// This is the most efficient way to parse messages, as it:
80    /// 1. Parses directly from the string without creating a `serde_json::Value`
81    /// 2. Allocates only for the resulting Message's owned fields
82    ///
83    /// # Arguments
84    ///
85    /// * `json` - A JSON string containing a message
86    ///
87    /// # Errors
88    ///
89    /// Returns an error if the JSON is malformed or doesn't match the Message schema.
90    ///
91    /// # Example
92    ///
93    /// ```ignore
94    /// let json = r#"{"type":"assistant","message":{"role":"assistant","content":"Hello"}}"#;
95    /// let message = ZeroCopyMessageParser::parse(json)?;
96    /// ```
97    pub fn parse(json: &str) -> Result<Message> {
98        serde_json::from_str(json).map_err(|e| {
99            ClaudeError::MessageParse(MessageParseError::new(
100                format!("Failed to parse message: {}", e),
101                Some(serde_json::Value::String(json.to_string())),
102            ))
103        })
104    }
105
106    /// Parse bytes directly into a Message.
107    ///
108    /// This is useful when reading from a byte buffer (e.g., from async I/O).
109    /// The bytes must be valid UTF-8.
110    ///
111    /// # Arguments
112    ///
113    /// * `bytes` - UTF-8 encoded JSON bytes
114    ///
115    /// # Errors
116    ///
117    /// Returns an error if the bytes are not valid UTF-8 or the JSON is malformed.
118    #[allow(dead_code)]
119    pub fn parse_bytes(bytes: &[u8]) -> Result<Message> {
120        let json = std::str::from_utf8(bytes).map_err(|e| {
121            ClaudeError::MessageParse(MessageParseError::new(
122                format!("Invalid UTF-8 in message: {}", e),
123                None,
124            ))
125        })?;
126        Self::parse(json)
127    }
128}
129
130/// Parse a JSON value into a Message using the specified parsing mode.
131///
132/// This is a convenience function that selects the appropriate parser based on
133/// the [`ParsingMode`].
134///
135/// # Arguments
136///
137/// * `data` - Either a `serde_json::Value` (for Traditional mode) or a string (for ZeroCopy mode)
138/// * `mode` - The parsing mode to use
139///
140/// # Errors
141///
142/// Returns an error if parsing fails.
143///
144/// # Example
145///
146/// ```ignore
147/// use claude_agent_sdk::internal::message_parser::{parse_with_mode, ParsingMode};
148///
149/// let json = r#"{"type":"assistant","message":{"role":"assistant","content":"Hello"}}"#;
150/// let message = parse_with_mode(json, ParsingMode::ZeroCopy)?;
151/// ```
152pub fn parse_with_mode(json: &str, mode: ParsingMode) -> Result<Message> {
153    match mode {
154        ParsingMode::Traditional => {
155            // Parse directly to Message - no need for intermediate Value
156            // The "Traditional" name refers to using owned types, not double parsing
157            serde_json::from_str(json).map_err(|e| {
158                ClaudeError::MessageParse(MessageParseError::new(
159                    format!("Failed to parse JSON: {}", e),
160                    None,
161                ))
162            })
163        }
164        ParsingMode::ZeroCopy => ZeroCopyMessageParser::parse(json),
165    }
166}
167
168/// Parse a `serde_json::Value` into a Message.
169///
170/// This is an alias for `MessageParser::parse` for convenience.
171/// Note: Currently unused but reserved for future API.
172#[allow(dead_code)]
173pub fn parse_from_value(value: serde_json::Value) -> Result<Message> {
174    MessageParser::parse(value)
175}
176
177/// Raw message type discriminator for quick message type checking.
178///
179/// This provides zero-copy access to the message type without full deserialization.
180/// Note: Currently unused but reserved for future API.
181#[allow(dead_code)]
182#[derive(Debug, Clone, Copy, PartialEq, Eq)]
183pub enum MessageKind {
184    /// Assistant message
185    Assistant,
186    /// System message
187    System,
188    /// Result message
189    Result,
190    /// Stream event
191    StreamEvent,
192    /// User message
193    User,
194    /// Control message
195    Control,
196    /// Unknown or unparseable message
197    Unknown,
198}
199
200#[allow(dead_code)]
201impl MessageKind {
202    /// Detect the message kind from a JSON string without full parsing.
203    pub fn detect(json: &str) -> Self {
204        let trimmed = json.trim();
205
206        if trimmed.contains(r#""type":"assistant""#) || trimmed.contains(r#""type": "assistant""#) {
207            return MessageKind::Assistant;
208        }
209        if trimmed.contains(r#""type":"system""#) || trimmed.contains(r#""type": "system""#) {
210            return MessageKind::System;
211        }
212        if trimmed.contains(r#""type":"result""#) || trimmed.contains(r#""type": "result""#) {
213            return MessageKind::Result;
214        }
215        if trimmed.contains(r#""type":"stream_event""#) || trimmed.contains(r#""type": "stream_event""#)
216        {
217            return MessageKind::StreamEvent;
218        }
219        if trimmed.contains(r#""type":"user""#) || trimmed.contains(r#""type": "user""#) {
220            return MessageKind::User;
221        }
222        if trimmed.contains(r#""type":"control""#) || trimmed.contains(r#""type": "control""#) {
223            return MessageKind::Control;
224        }
225
226        MessageKind::Unknown
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233
234    #[test]
235    fn test_message_parser() {
236        let json = serde_json::json!({
237            "type": "assistant",
238            "message": {
239                "role": "assistant",
240                "content": [{"type": "text", "text": "Hello, world!"}]
241            }
242        });
243
244        let result = MessageParser::parse(json);
245        assert!(result.is_ok());
246    }
247
248    #[test]
249    fn test_zero_copy_parser_assistant() {
250        let json = r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"Hello"}]}}"#;
251        let result = ZeroCopyMessageParser::parse(json);
252        assert!(result.is_ok());
253
254        let message = result.unwrap();
255        match message {
256            Message::Assistant(msg) => {
257                // Check that content is present
258                assert!(!msg.message.content.is_empty());
259            }
260            _ => panic!("Expected Assistant message"),
261        }
262    }
263
264    #[test]
265    fn test_zero_copy_parser_system() {
266        let json = r#"{"type":"system","subtype":"init","cwd":"/home/user","session_id":"test-123"}"#;
267        let result = ZeroCopyMessageParser::parse(json);
268        assert!(result.is_ok());
269
270        let message = result.unwrap();
271        match message {
272            Message::System(msg) => {
273                assert_eq!(msg.subtype, "init");
274            }
275            _ => panic!("Expected System message"),
276        }
277    }
278
279    #[test]
280    fn test_zero_copy_parser_result() {
281        let json = r#"{"type":"result","subtype":"complete","result":"Task completed","session_id":"test-123","cost_usd":0.001,"duration_ms":500,"duration_api_ms":300,"num_turns":1,"total_cost_usd":0.001,"is_error":false}"#;
282        let result = ZeroCopyMessageParser::parse(json);
283        if result.is_err() {
284            eprintln!("Error: {:?}", result.as_ref().err());
285        }
286        assert!(result.is_ok());
287
288        let message = result.unwrap();
289        match message {
290            Message::Result(msg) => {
291                assert_eq!(msg.result, Some("Task completed".to_string()));
292                assert!(!msg.is_error);
293            }
294            _ => panic!("Expected Result message"),
295        }
296    }
297
298    #[test]
299    fn test_zero_copy_parser_invalid_json() {
300        let json = r#"{"type":"assistant","invalid"#;
301        let result = ZeroCopyMessageParser::parse(json);
302        assert!(result.is_err());
303    }
304
305    #[test]
306    fn test_zero_copy_parser_bytes() {
307        let json = br#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"Hello"}]}}"#;
308        let result = ZeroCopyMessageParser::parse_bytes(json);
309        assert!(result.is_ok());
310    }
311
312    #[test]
313    fn test_zero_copy_parser_bytes_invalid_utf8() {
314        let invalid_bytes: &[u8] = &[0xff, 0xfe, 0xfd];
315        let result = ZeroCopyMessageParser::parse_bytes(invalid_bytes);
316        assert!(result.is_err());
317    }
318
319    #[test]
320    fn test_message_kind_detect_assistant() {
321        let json = r#"{"type":"assistant","message":{}}"#;
322        assert_eq!(MessageKind::detect(json), MessageKind::Assistant);
323    }
324
325    #[test]
326    fn test_message_kind_detect_system() {
327        let json = r#"{"type":"system","subtype":"init"}"#;
328        assert_eq!(MessageKind::detect(json), MessageKind::System);
329    }
330
331    #[test]
332    fn test_message_kind_detect_result() {
333        let json = r#"{"type":"result","session_id":"123"}"#;
334        assert_eq!(MessageKind::detect(json), MessageKind::Result);
335    }
336
337    #[test]
338    fn test_message_kind_detect_stream_event() {
339        let json = r#"{"type":"stream_event","event":"text"}}"#;
340        assert_eq!(MessageKind::detect(json), MessageKind::StreamEvent);
341    }
342
343    #[test]
344    fn test_message_kind_detect_user() {
345        let json = r#"{"type":"user","text":"Hello"}"#;
346        assert_eq!(MessageKind::detect(json), MessageKind::User);
347    }
348
349    #[test]
350    fn test_message_kind_detect_unknown() {
351        let json = r#"{"foo":"bar"}"#;
352        assert_eq!(MessageKind::detect(json), MessageKind::Unknown);
353    }
354
355    #[test]
356    fn test_message_kind_detect_with_spaces() {
357        let json = r#"{"type": "assistant", "message": {}}"#;
358        assert_eq!(MessageKind::detect(json), MessageKind::Assistant);
359    }
360
361    #[test]
362    fn test_parse_with_mode_zero_copy() {
363        let json = r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"Hello"}]}}"#;
364        let result = parse_with_mode(json, ParsingMode::ZeroCopy);
365        assert!(result.is_ok());
366
367        let message = result.unwrap();
368        match message {
369            Message::Assistant(msg) => {
370                assert!(!msg.message.content.is_empty());
371            }
372            _ => panic!("Expected Assistant message"),
373        }
374    }
375
376    #[test]
377    fn test_parse_with_mode_traditional() {
378        let json = r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"Hello"}]}}"#;
379        let result = parse_with_mode(json, ParsingMode::Traditional);
380        assert!(result.is_ok());
381
382        let message = result.unwrap();
383        match message {
384            Message::Assistant(msg) => {
385                assert!(!msg.message.content.is_empty());
386            }
387            _ => panic!("Expected Assistant message"),
388        }
389    }
390
391    #[test]
392    fn test_parsing_mode_default() {
393        // Default should be Traditional
394        assert_eq!(ParsingMode::default(), ParsingMode::Traditional);
395    }
396}