Skip to main content

procwire_client/codec/
msgpack.rs

1//! MsgPack codec using `rmp-serde`.
2//!
3//! **CRITICAL**: Always use `to_vec_named`, NEVER `to_vec`!
4//! Node.js `@msgpack/msgpack` expects struct-as-map format.
5//!
6//! # Why `to_vec_named`?
7//!
8//! - `to_vec` serializes structs as arrays (positional)
9//! - `to_vec_named` serializes structs as maps (with field names)
10//! - Node.js `@msgpack/msgpack` expects the map format
11//! - Using `to_vec` will cause deserialization failures in Node.js
12//!
13//! # Example
14//!
15//! ```
16//! use procwire_client::codec::MsgPackCodec;
17//! use serde::{Serialize, Deserialize};
18//!
19//! #[derive(Serialize, Deserialize, PartialEq, Debug)]
20//! struct Message {
21//!     id: u32,
22//!     content: String,
23//! }
24//!
25//! let msg = Message { id: 42, content: "hello".to_string() };
26//! let encoded = MsgPackCodec::encode(&msg).unwrap();
27//! let decoded: Message = MsgPackCodec::decode(&encoded).unwrap();
28//! assert_eq!(decoded, msg);
29//! ```
30
31use crate::error::Result;
32
33/// MessagePack codec for structured data.
34///
35/// Uses `rmp_serde::to_vec_named` for Node.js compatibility.
36/// This ensures structs are serialized as maps (with field names)
37/// rather than arrays (positional).
38pub struct MsgPackCodec;
39
40impl MsgPackCodec {
41    /// Encode a value to MsgPack bytes.
42    ///
43    /// Uses `to_vec_named` for struct-as-map format (Node.js compatible).
44    ///
45    /// # Errors
46    ///
47    /// Returns error if the value cannot be serialized.
48    #[inline]
49    pub fn encode<T: serde::Serialize>(value: &T) -> Result<Vec<u8>> {
50        // CRITICAL: to_vec_named, NOT to_vec!
51        Ok(rmp_serde::to_vec_named(value)?)
52    }
53
54    /// Decode MsgPack bytes to a value.
55    ///
56    /// # Errors
57    ///
58    /// Returns error if the bytes cannot be deserialized to type T.
59    #[inline]
60    pub fn decode<T: serde::de::DeserializeOwned>(bytes: &[u8]) -> Result<T> {
61        Ok(rmp_serde::from_slice(bytes)?)
62    }
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68    use serde::{Deserialize, Serialize};
69
70    #[derive(Serialize, Deserialize, PartialEq, Debug)]
71    struct TestStruct {
72        id: u32,
73        name: String,
74        active: bool,
75    }
76
77    #[test]
78    fn test_encode_decode_struct() {
79        let original = TestStruct {
80            id: 42,
81            name: "test".to_string(),
82            active: true,
83        };
84
85        let encoded = MsgPackCodec::encode(&original).unwrap();
86        let decoded: TestStruct = MsgPackCodec::decode(&encoded).unwrap();
87
88        assert_eq!(decoded, original);
89    }
90
91    #[test]
92    fn test_encode_decode_primitives() {
93        // String
94        let s = "hello world";
95        let encoded = MsgPackCodec::encode(&s).unwrap();
96        let decoded: String = MsgPackCodec::decode(&encoded).unwrap();
97        assert_eq!(decoded, s);
98
99        // Number
100        let n: i64 = 12345;
101        let encoded = MsgPackCodec::encode(&n).unwrap();
102        let decoded: i64 = MsgPackCodec::decode(&encoded).unwrap();
103        assert_eq!(decoded, n);
104
105        // Boolean
106        let b = true;
107        let encoded = MsgPackCodec::encode(&b).unwrap();
108        let decoded: bool = MsgPackCodec::decode(&encoded).unwrap();
109        assert_eq!(decoded, b);
110    }
111
112    #[test]
113    fn test_encode_decode_collections() {
114        // Vec
115        let vec = vec![1, 2, 3, 4, 5];
116        let encoded = MsgPackCodec::encode(&vec).unwrap();
117        let decoded: Vec<i32> = MsgPackCodec::decode(&encoded).unwrap();
118        assert_eq!(decoded, vec);
119
120        // HashMap
121        use std::collections::HashMap;
122        let mut map = HashMap::new();
123        map.insert("key1".to_string(), 100);
124        map.insert("key2".to_string(), 200);
125
126        let encoded = MsgPackCodec::encode(&map).unwrap();
127        let decoded: HashMap<String, i32> = MsgPackCodec::decode(&encoded).unwrap();
128        assert_eq!(decoded, map);
129    }
130
131    #[test]
132    fn test_encode_decode_nested() {
133        #[derive(Serialize, Deserialize, PartialEq, Debug)]
134        struct Inner {
135            value: i32,
136        }
137
138        #[derive(Serialize, Deserialize, PartialEq, Debug)]
139        struct Outer {
140            inner: Inner,
141            items: Vec<String>,
142        }
143
144        let original = Outer {
145            inner: Inner { value: 999 },
146            items: vec!["a".to_string(), "b".to_string()],
147        };
148
149        let encoded = MsgPackCodec::encode(&original).unwrap();
150        let decoded: Outer = MsgPackCodec::decode(&encoded).unwrap();
151
152        assert_eq!(decoded, original);
153    }
154
155    #[test]
156    fn test_encode_decode_option() {
157        let some_val: Option<i32> = Some(42);
158        let encoded = MsgPackCodec::encode(&some_val).unwrap();
159        let decoded: Option<i32> = MsgPackCodec::decode(&encoded).unwrap();
160        assert_eq!(decoded, some_val);
161
162        let none_val: Option<i32> = None;
163        let encoded = MsgPackCodec::encode(&none_val).unwrap();
164        let decoded: Option<i32> = MsgPackCodec::decode(&encoded).unwrap();
165        assert_eq!(decoded, none_val);
166    }
167
168    #[test]
169    fn test_to_vec_named_produces_map_format() {
170        // Verify that structs are serialized as maps (with field names)
171        // not as arrays (positional)
172        let test = TestStruct {
173            id: 1,
174            name: "x".to_string(),
175            active: false,
176        };
177
178        let encoded = MsgPackCodec::encode(&test).unwrap();
179
180        // MsgPack map format starts with 0x83 (fixmap with 3 elements)
181        // Array format would start with 0x93 (fixarray with 3 elements)
182        assert_eq!(
183            encoded[0] & 0xF0,
184            0x80,
185            "Expected map format (0x8X), got {:02X}",
186            encoded[0]
187        );
188    }
189
190    #[test]
191    fn test_decode_error_on_invalid_data() {
192        let invalid = b"not valid msgpack";
193        let result: Result<TestStruct> = MsgPackCodec::decode(invalid);
194        assert!(result.is_err());
195    }
196
197    #[test]
198    fn test_empty_struct() {
199        #[derive(Serialize, Deserialize, PartialEq, Debug)]
200        struct Empty {}
201
202        let empty = Empty {};
203        let encoded = MsgPackCodec::encode(&empty).unwrap();
204        let decoded: Empty = MsgPackCodec::decode(&encoded).unwrap();
205        assert_eq!(decoded, empty);
206    }
207
208    #[test]
209    fn test_nodejs_interop_simple_object() {
210        // This test verifies that our MsgPack output is compatible with Node.js @msgpack/msgpack
211        // The bytes below were generated by Node.js: msgpack.encode({id: 1, name: "test"})
212        // Note: Node.js encodes as map, and we use to_vec_named which also produces map format
213
214        #[derive(Serialize, Deserialize, PartialEq, Debug)]
215        struct SimpleObj {
216            id: i32,
217            name: String,
218        }
219
220        let obj = SimpleObj {
221            id: 1,
222            name: "test".to_string(),
223        };
224
225        let encoded = MsgPackCodec::encode(&obj).unwrap();
226
227        // Verify it's a map (0x82 = fixmap with 2 elements)
228        assert_eq!(encoded[0], 0x82, "Expected fixmap with 2 elements");
229
230        // Decode back to verify roundtrip
231        let decoded: SimpleObj = MsgPackCodec::decode(&encoded).unwrap();
232        assert_eq!(decoded, obj);
233    }
234
235    #[test]
236    fn test_nodejs_interop_number_types() {
237        // Test various number types that Node.js might send
238        // u8
239        let n: u8 = 255;
240        let encoded = MsgPackCodec::encode(&n).unwrap();
241        let decoded: u8 = MsgPackCodec::decode(&encoded).unwrap();
242        assert_eq!(decoded, n);
243
244        // i32 (negative)
245        let n: i32 = -12345;
246        let encoded = MsgPackCodec::encode(&n).unwrap();
247        let decoded: i32 = MsgPackCodec::decode(&encoded).unwrap();
248        assert_eq!(decoded, n);
249
250        // f64
251        let n: f64 = 3.14159;
252        let encoded = MsgPackCodec::encode(&n).unwrap();
253        let decoded: f64 = MsgPackCodec::decode(&encoded).unwrap();
254        assert!((decoded - n).abs() < f64::EPSILON);
255    }
256
257    #[test]
258    fn test_nodejs_interop_array() {
259        // Node.js arrays should decode correctly
260        let arr = vec!["hello", "world", "test"];
261        let encoded = MsgPackCodec::encode(&arr).unwrap();
262
263        // Verify it's an array (0x93 = fixarray with 3 elements)
264        assert_eq!(encoded[0], 0x93, "Expected fixarray with 3 elements");
265
266        let decoded: Vec<String> = MsgPackCodec::decode(&encoded).unwrap();
267        assert_eq!(decoded, arr);
268    }
269
270    #[test]
271    fn test_nodejs_interop_null() {
272        // Node.js null → Rust Option::None
273        let val: Option<i32> = None;
274        let encoded = MsgPackCodec::encode(&val).unwrap();
275
276        // MsgPack nil is 0xc0
277        assert_eq!(encoded, vec![0xc0], "None should encode as msgpack nil");
278
279        let decoded: Option<i32> = MsgPackCodec::decode(&encoded).unwrap();
280        assert_eq!(decoded, None);
281    }
282
283    #[test]
284    fn test_nodejs_interop_binary_buffer() {
285        // Node.js Buffer should be decodable as Vec<u8>
286        // Binary format in msgpack: 0xc4 (bin8) + length + data
287        let data: Vec<u8> = vec![0x01, 0x02, 0x03, 0x04, 0x05];
288        let encoded = MsgPackCodec::encode(&serde_bytes::Bytes::new(&data)).unwrap();
289
290        // Should start with bin8 format
291        assert_eq!(encoded[0], 0xc4, "Expected bin8 format");
292
293        let decoded: serde_bytes::ByteBuf = MsgPackCodec::decode(&encoded).unwrap();
294        assert_eq!(decoded.as_ref(), &data);
295    }
296}