Skip to main content

clawft_plugin/
message.rs

1//! Message payload types for channel communication.
2//!
3//! Defines [`MessagePayload`], supporting text, structured data, and binary
4//! payloads. The `Binary` variant is reserved for voice/audio data
5//! (Workstream G forward-compatibility).
6
7use serde::{Deserialize, Serialize};
8
9/// Message payload types for channel communication.
10///
11/// Supports text, structured data, and binary payloads. The `Binary`
12/// variant is reserved for voice/audio data (Workstream G).
13#[non_exhaustive]
14#[derive(Debug, Clone, Serialize, Deserialize)]
15#[serde(tag = "type", rename_all = "snake_case")]
16pub enum MessagePayload {
17    /// Plain text message.
18    Text {
19        /// The text content.
20        content: String,
21    },
22    /// Structured data (JSON object).
23    Structured {
24        /// The structured data.
25        data: serde_json::Value,
26    },
27    /// Binary data with MIME type (e.g., audio/wav for voice).
28    Binary {
29        /// MIME type of the binary data (e.g., `"audio/wav"`, `"audio/opus"`).
30        mime_type: String,
31        /// Raw binary data, base64-encoded for JSON serialization.
32        #[serde(with = "base64_bytes")]
33        data: Vec<u8>,
34    },
35}
36
37/// Serde helper for base64-encoding binary data in JSON.
38mod base64_bytes {
39    use serde::{Deserialize, Deserializer, Serializer};
40
41    /// Custom base64 encoding using a simple approach without pulling in
42    /// the `base64` crate. For JSON serialization we use an array of bytes.
43    pub fn serialize<S>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error>
44    where
45        S: Serializer,
46    {
47        serializer.serialize_bytes(bytes)
48    }
49
50    pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
51    where
52        D: Deserializer<'de>,
53    {
54        // Accept both byte arrays and sequences of numbers.
55        let bytes: Vec<u8> = Vec::deserialize(deserializer)?;
56        Ok(bytes)
57    }
58}
59
60impl MessagePayload {
61    /// Create a text payload.
62    pub fn text(content: impl Into<String>) -> Self {
63        Self::Text {
64            content: content.into(),
65        }
66    }
67
68    /// Create a structured data payload.
69    pub fn structured(data: serde_json::Value) -> Self {
70        Self::Structured { data }
71    }
72
73    /// Create a binary payload with a MIME type.
74    pub fn binary(mime_type: impl Into<String>, data: Vec<u8>) -> Self {
75        Self::Binary {
76            mime_type: mime_type.into(),
77            data,
78        }
79    }
80
81    /// Returns `true` if this is a text payload.
82    pub fn is_text(&self) -> bool {
83        matches!(self, Self::Text { .. })
84    }
85
86    /// Returns `true` if this is a binary payload.
87    pub fn is_binary(&self) -> bool {
88        matches!(self, Self::Binary { .. })
89    }
90
91    /// Extract the text content, if this is a text payload.
92    pub fn as_text(&self) -> Option<&str> {
93        match self {
94            Self::Text { content } => Some(content),
95            _ => None,
96        }
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn text_payload_creation() {
106        let payload = MessagePayload::text("hello");
107        assert!(payload.is_text());
108        assert!(!payload.is_binary());
109        assert_eq!(payload.as_text(), Some("hello"));
110    }
111
112    #[test]
113    fn structured_payload_creation() {
114        let data = serde_json::json!({"key": "value"});
115        let payload = MessagePayload::structured(data.clone());
116        assert!(!payload.is_text());
117        assert!(!payload.is_binary());
118        assert_eq!(payload.as_text(), None);
119    }
120
121    #[test]
122    fn binary_payload_creation() {
123        let payload = MessagePayload::binary("audio/wav", vec![0u8; 16]);
124        assert!(payload.is_binary());
125        assert!(!payload.is_text());
126        assert_eq!(payload.as_text(), None);
127    }
128
129    #[test]
130    fn text_payload_serde_roundtrip() {
131        let payload = MessagePayload::text("hello world");
132        let json = serde_json::to_string(&payload).unwrap();
133        let restored: MessagePayload = serde_json::from_str(&json).unwrap();
134        assert_eq!(restored.as_text(), Some("hello world"));
135    }
136
137    #[test]
138    fn structured_payload_serde_roundtrip() {
139        let data = serde_json::json!({"key": "value", "count": 42});
140        let payload = MessagePayload::structured(data.clone());
141        let json = serde_json::to_string(&payload).unwrap();
142        let restored: MessagePayload = serde_json::from_str(&json).unwrap();
143        match restored {
144            MessagePayload::Structured { data: d } => {
145                assert_eq!(d["key"], "value");
146                assert_eq!(d["count"], 42);
147            }
148            _ => panic!("expected Structured variant"),
149        }
150    }
151
152    #[test]
153    fn binary_payload_serde_roundtrip() {
154        let original_data = vec![0u8, 1, 2, 3, 255, 128];
155        let payload = MessagePayload::binary("audio/wav", original_data.clone());
156        let json = serde_json::to_string(&payload).unwrap();
157        let restored: MessagePayload = serde_json::from_str(&json).unwrap();
158        match restored {
159            MessagePayload::Binary { mime_type, data } => {
160                assert_eq!(mime_type, "audio/wav");
161                assert_eq!(data, original_data);
162            }
163            _ => panic!("expected Binary variant"),
164        }
165    }
166
167    #[test]
168    fn message_payload_is_send_sync() {
169        fn assert_send_sync<T: Send + Sync>() {}
170        assert_send_sync::<MessagePayload>();
171    }
172}