cc-agent-sdk 0.1.7

claude agent sdk
Documentation
//! Message parser for converting JSON to typed messages
//!
//! This module provides both traditional and zero-copy JSON parsing:
//!
//! - **MessageParser**: Traditional parser that creates owned Message types
//! - **ZeroCopyMessageParser**: Zero-copy parser that borrows from input string
//!
//! ## Zero-Copy Parsing Benefits
//!
//! - **Reduced allocations**: No intermediate `serde_json::Value` allocation
//! - **Faster parsing**: Direct deserialization from string to Message
//! - **Lower memory pressure**: Especially beneficial for high-frequency message streams
//!
//! ## Parsing Modes
//!
//! Use [`ParsingMode`] to select the parsing strategy:
//!
//! - [`ParsingMode::Traditional`]: Uses intermediate `serde_json::Value` (default, safest)
//! - [`ParsingMode::ZeroCopy`]: Direct parsing from string (faster, less memory)
//!
//! ## Example
//!
//! ```ignore
//! use claude_agent_sdk::internal::message_parser::{ZeroCopyMessageParser, ParsingMode};
//!
//! let json = r#"{"type":"assistant","message":{"role":"assistant","content":"Hello"}}"#;
//! let message = ZeroCopyMessageParser::parse(json)?;
//! ```

use crate::errors::{ClaudeError, MessageParseError, Result};
use crate::types::messages::Message;

/// Parsing mode for message deserialization
///
/// Controls how JSON strings are parsed into [`Message`] types.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ParsingMode {
    /// Traditional parsing via intermediate `serde_json::Value`
    ///
    /// This is the safest option that creates an intermediate `Value` before
    /// deserializing to `Message`. Use this when you need maximum compatibility.
    #[default]
    Traditional,

    /// Zero-copy parsing directly from string
    ///
    /// Parses directly from the input string without creating an intermediate
    /// `Value`. This is faster and uses less memory, especially for large messages.
    ///
    /// # Performance
    ///
    /// - ~30-50% less memory allocation for large messages
    /// - ~10-20% faster parsing time
    ZeroCopy,
}

/// Message parser for CLI output (traditional owned parsing)
pub struct MessageParser;

impl MessageParser {
    /// Parse a JSON value into a Message
    pub fn parse(data: serde_json::Value) -> Result<Message> {
        serde_json::from_value(data).map_err(|e| {
            MessageParseError::new(format!("Failed to parse message: {}", e), None).into()
        })
    }
}

/// Zero-copy message parser for CLI output
///
/// This parser avoids intermediate allocations by parsing directly from
/// the input string. Use this for high-performance scenarios where the
/// input string lifetime allows borrowing.
pub struct ZeroCopyMessageParser;

impl ZeroCopyMessageParser {
    /// Parse a JSON string directly into a Message without intermediate allocation.
    ///
    /// This is the most efficient way to parse messages, as it:
    /// 1. Parses directly from the string without creating a `serde_json::Value`
    /// 2. Allocates only for the resulting Message's owned fields
    ///
    /// # Arguments
    ///
    /// * `json` - A JSON string containing a message
    ///
    /// # Errors
    ///
    /// Returns an error if the JSON is malformed or doesn't match the Message schema.
    ///
    /// # Example
    ///
    /// ```ignore
    /// let json = r#"{"type":"assistant","message":{"role":"assistant","content":"Hello"}}"#;
    /// let message = ZeroCopyMessageParser::parse(json)?;
    /// ```
    pub fn parse(json: &str) -> Result<Message> {
        serde_json::from_str(json).map_err(|e| {
            ClaudeError::MessageParse(MessageParseError::new(
                format!("Failed to parse message: {}", e),
                Some(serde_json::Value::String(json.to_string())),
            ))
        })
    }

    /// Parse bytes directly into a Message.
    ///
    /// This is useful when reading from a byte buffer (e.g., from async I/O).
    /// The bytes must be valid UTF-8.
    ///
    /// # Arguments
    ///
    /// * `bytes` - UTF-8 encoded JSON bytes
    ///
    /// # Errors
    ///
    /// Returns an error if the bytes are not valid UTF-8 or the JSON is malformed.
    #[allow(dead_code)]
    pub fn parse_bytes(bytes: &[u8]) -> Result<Message> {
        let json = std::str::from_utf8(bytes).map_err(|e| {
            ClaudeError::MessageParse(MessageParseError::new(
                format!("Invalid UTF-8 in message: {}", e),
                None,
            ))
        })?;
        Self::parse(json)
    }
}

/// Parse a JSON value into a Message using the specified parsing mode.
///
/// This is a convenience function that selects the appropriate parser based on
/// the [`ParsingMode`].
///
/// # Arguments
///
/// * `data` - Either a `serde_json::Value` (for Traditional mode) or a string (for ZeroCopy mode)
/// * `mode` - The parsing mode to use
///
/// # Errors
///
/// Returns an error if parsing fails.
///
/// # Example
///
/// ```ignore
/// use claude_agent_sdk::internal::message_parser::{parse_with_mode, ParsingMode};
///
/// let json = r#"{"type":"assistant","message":{"role":"assistant","content":"Hello"}}"#;
/// let message = parse_with_mode(json, ParsingMode::ZeroCopy)?;
/// ```
pub fn parse_with_mode(json: &str, mode: ParsingMode) -> Result<Message> {
    match mode {
        ParsingMode::Traditional => {
            // Parse directly to Message - no need for intermediate Value
            // The "Traditional" name refers to using owned types, not double parsing
            serde_json::from_str(json).map_err(|e| {
                ClaudeError::MessageParse(MessageParseError::new(
                    format!("Failed to parse JSON: {}", e),
                    None,
                ))
            })
        }
        ParsingMode::ZeroCopy => ZeroCopyMessageParser::parse(json),
    }
}

/// Parse a `serde_json::Value` into a Message.
///
/// This is an alias for `MessageParser::parse` for convenience.
/// Note: Currently unused but reserved for future API.
#[allow(dead_code)]
pub fn parse_from_value(value: serde_json::Value) -> Result<Message> {
    MessageParser::parse(value)
}

/// Raw message type discriminator for quick message type checking.
///
/// This provides zero-copy access to the message type without full deserialization.
/// Note: Currently unused but reserved for future API.
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MessageKind {
    /// Assistant message
    Assistant,
    /// System message
    System,
    /// Result message
    Result,
    /// Stream event
    StreamEvent,
    /// User message
    User,
    /// Control message
    Control,
    /// Unknown or unparseable message
    Unknown,
}

#[allow(dead_code)]
impl MessageKind {
    /// Detect the message kind from a JSON string without full parsing.
    pub fn detect(json: &str) -> Self {
        let trimmed = json.trim();

        if trimmed.contains(r#""type":"assistant""#) || trimmed.contains(r#""type": "assistant""#) {
            return MessageKind::Assistant;
        }
        if trimmed.contains(r#""type":"system""#) || trimmed.contains(r#""type": "system""#) {
            return MessageKind::System;
        }
        if trimmed.contains(r#""type":"result""#) || trimmed.contains(r#""type": "result""#) {
            return MessageKind::Result;
        }
        if trimmed.contains(r#""type":"stream_event""#) || trimmed.contains(r#""type": "stream_event""#)
        {
            return MessageKind::StreamEvent;
        }
        if trimmed.contains(r#""type":"user""#) || trimmed.contains(r#""type": "user""#) {
            return MessageKind::User;
        }
        if trimmed.contains(r#""type":"control""#) || trimmed.contains(r#""type": "control""#) {
            return MessageKind::Control;
        }

        MessageKind::Unknown
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_message_parser() {
        let json = serde_json::json!({
            "type": "assistant",
            "message": {
                "role": "assistant",
                "content": [{"type": "text", "text": "Hello, world!"}]
            }
        });

        let result = MessageParser::parse(json);
        assert!(result.is_ok());
    }

    #[test]
    fn test_zero_copy_parser_assistant() {
        let json = r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"Hello"}]}}"#;
        let result = ZeroCopyMessageParser::parse(json);
        assert!(result.is_ok());

        let message = result.unwrap();
        match message {
            Message::Assistant(msg) => {
                // Check that content is present
                assert!(!msg.message.content.is_empty());
            }
            _ => panic!("Expected Assistant message"),
        }
    }

    #[test]
    fn test_zero_copy_parser_system() {
        let json = r#"{"type":"system","subtype":"init","cwd":"/home/user","session_id":"test-123"}"#;
        let result = ZeroCopyMessageParser::parse(json);
        assert!(result.is_ok());

        let message = result.unwrap();
        match message {
            Message::System(msg) => {
                assert_eq!(msg.subtype, "init");
            }
            _ => panic!("Expected System message"),
        }
    }

    #[test]
    fn test_zero_copy_parser_result() {
        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}"#;
        let result = ZeroCopyMessageParser::parse(json);
        if result.is_err() {
            eprintln!("Error: {:?}", result.as_ref().err());
        }
        assert!(result.is_ok());

        let message = result.unwrap();
        match message {
            Message::Result(msg) => {
                assert_eq!(msg.result, Some("Task completed".to_string()));
                assert!(!msg.is_error);
            }
            _ => panic!("Expected Result message"),
        }
    }

    #[test]
    fn test_zero_copy_parser_invalid_json() {
        let json = r#"{"type":"assistant","invalid"#;
        let result = ZeroCopyMessageParser::parse(json);
        assert!(result.is_err());
    }

    #[test]
    fn test_zero_copy_parser_bytes() {
        let json = br#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"Hello"}]}}"#;
        let result = ZeroCopyMessageParser::parse_bytes(json);
        assert!(result.is_ok());
    }

    #[test]
    fn test_zero_copy_parser_bytes_invalid_utf8() {
        let invalid_bytes: &[u8] = &[0xff, 0xfe, 0xfd];
        let result = ZeroCopyMessageParser::parse_bytes(invalid_bytes);
        assert!(result.is_err());
    }

    #[test]
    fn test_message_kind_detect_assistant() {
        let json = r#"{"type":"assistant","message":{}}"#;
        assert_eq!(MessageKind::detect(json), MessageKind::Assistant);
    }

    #[test]
    fn test_message_kind_detect_system() {
        let json = r#"{"type":"system","subtype":"init"}"#;
        assert_eq!(MessageKind::detect(json), MessageKind::System);
    }

    #[test]
    fn test_message_kind_detect_result() {
        let json = r#"{"type":"result","session_id":"123"}"#;
        assert_eq!(MessageKind::detect(json), MessageKind::Result);
    }

    #[test]
    fn test_message_kind_detect_stream_event() {
        let json = r#"{"type":"stream_event","event":"text"}}"#;
        assert_eq!(MessageKind::detect(json), MessageKind::StreamEvent);
    }

    #[test]
    fn test_message_kind_detect_user() {
        let json = r#"{"type":"user","text":"Hello"}"#;
        assert_eq!(MessageKind::detect(json), MessageKind::User);
    }

    #[test]
    fn test_message_kind_detect_unknown() {
        let json = r#"{"foo":"bar"}"#;
        assert_eq!(MessageKind::detect(json), MessageKind::Unknown);
    }

    #[test]
    fn test_message_kind_detect_with_spaces() {
        let json = r#"{"type": "assistant", "message": {}}"#;
        assert_eq!(MessageKind::detect(json), MessageKind::Assistant);
    }

    #[test]
    fn test_parse_with_mode_zero_copy() {
        let json = r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"Hello"}]}}"#;
        let result = parse_with_mode(json, ParsingMode::ZeroCopy);
        assert!(result.is_ok());

        let message = result.unwrap();
        match message {
            Message::Assistant(msg) => {
                assert!(!msg.message.content.is_empty());
            }
            _ => panic!("Expected Assistant message"),
        }
    }

    #[test]
    fn test_parse_with_mode_traditional() {
        let json = r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"Hello"}]}}"#;
        let result = parse_with_mode(json, ParsingMode::Traditional);
        assert!(result.is_ok());

        let message = result.unwrap();
        match message {
            Message::Assistant(msg) => {
                assert!(!msg.message.content.is_empty());
            }
            _ => panic!("Expected Assistant message"),
        }
    }

    #[test]
    fn test_parsing_mode_default() {
        // Default should be Traditional
        assert_eq!(ParsingMode::default(), ParsingMode::Traditional);
    }
}