Skip to main content

shape_runtime/stdlib/
msgpack_module.rs

1//! Native `msgpack` module for MessagePack encoding and decoding.
2//!
3//! Exports: msgpack.encode(value), msgpack.decode(data),
4//!          msgpack.encode_bytes(value), msgpack.decode_bytes(data)
5
6use crate::module_exports::{ModuleContext, ModuleExports, ModuleFunction, ModuleParam};
7use shape_value::ValueWord;
8use std::sync::Arc;
9
10/// Convert a `serde_json::Value` into an untyped `ValueWord`.
11///
12/// This mirrors `json_value_to_nanboxed` from the json module.
13fn json_value_to_valueword(value: serde_json::Value) -> ValueWord {
14    match value {
15        serde_json::Value::Null => ValueWord::none(),
16        serde_json::Value::Bool(b) => ValueWord::from_bool(b),
17        serde_json::Value::Number(n) => {
18            if let Some(i) = n.as_i64() {
19                ValueWord::from_i64(i)
20            } else {
21                ValueWord::from_f64(n.as_f64().unwrap_or(0.0))
22            }
23        }
24        serde_json::Value::String(s) => ValueWord::from_string(Arc::new(s)),
25        serde_json::Value::Array(arr) => {
26            let items: Vec<ValueWord> = arr.into_iter().map(json_value_to_valueword).collect();
27            ValueWord::from_array(Arc::new(items))
28        }
29        serde_json::Value::Object(map) => {
30            let mut keys = Vec::with_capacity(map.len());
31            let mut values = Vec::with_capacity(map.len());
32            for (k, v) in map.into_iter() {
33                keys.push(ValueWord::from_string(Arc::new(k)));
34                values.push(json_value_to_valueword(v));
35            }
36            ValueWord::from_hashmap_pairs(keys, values)
37        }
38    }
39}
40
41/// Create the `msgpack` module with MessagePack encoding and decoding functions.
42pub fn create_msgpack_module() -> ModuleExports {
43    let mut module = ModuleExports::new("msgpack");
44    module.description = "MessagePack binary serialization".to_string();
45
46    // msgpack.encode(value: any) -> Result<string>
47    // Encodes a value to MessagePack and returns a hex-encoded string.
48    module.add_function_with_schema(
49        "encode",
50        |args: &[ValueWord], _ctx: &ModuleContext| {
51            let value = args
52                .first()
53                .ok_or_else(|| "msgpack.encode() requires a value argument".to_string())?;
54
55            let json_value = value.to_json_value();
56            let bytes = rmp_serde::to_vec(&json_value)
57                .map_err(|e| format!("msgpack.encode() failed: {}", e))?;
58            let hex_str = hex::encode(&bytes);
59
60            Ok(ValueWord::from_ok(ValueWord::from_string(Arc::new(
61                hex_str,
62            ))))
63        },
64        ModuleFunction {
65            description: "Encode a value to MessagePack (hex-encoded string)".to_string(),
66            params: vec![ModuleParam {
67                name: "value".to_string(),
68                type_name: "any".to_string(),
69                required: true,
70                description: "Value to encode".to_string(),
71                ..Default::default()
72            }],
73            return_type: Some("Result<string>".to_string()),
74        },
75    );
76
77    // msgpack.decode(data: string) -> Result<any>
78    // Decodes a hex-encoded MessagePack string to a value.
79    module.add_function_with_schema(
80        "decode",
81        |args: &[ValueWord], _ctx: &ModuleContext| {
82            let hex_str = args
83                .first()
84                .and_then(|a| a.as_str())
85                .ok_or_else(|| "msgpack.decode() requires a string argument".to_string())?;
86
87            let bytes =
88                hex::decode(hex_str).map_err(|e| format!("msgpack.decode() invalid hex: {}", e))?;
89            let json_value: serde_json::Value = rmp_serde::from_slice(&bytes)
90                .map_err(|e| format!("msgpack.decode() failed: {}", e))?;
91
92            Ok(ValueWord::from_ok(json_value_to_valueword(json_value)))
93        },
94        ModuleFunction {
95            description: "Decode a hex-encoded MessagePack string to a value".to_string(),
96            params: vec![ModuleParam {
97                name: "data".to_string(),
98                type_name: "string".to_string(),
99                required: true,
100                description: "Hex-encoded MessagePack data".to_string(),
101                ..Default::default()
102            }],
103            return_type: Some("Result<any>".to_string()),
104        },
105    );
106
107    // msgpack.encode_bytes(value: any) -> Result<Array<int>>
108    // Encodes a value to MessagePack and returns raw bytes as an array of ints.
109    module.add_function_with_schema(
110        "encode_bytes",
111        |args: &[ValueWord], _ctx: &ModuleContext| {
112            let value = args
113                .first()
114                .ok_or_else(|| "msgpack.encode_bytes() requires a value argument".to_string())?;
115
116            let json_value = value.to_json_value();
117            let bytes = rmp_serde::to_vec(&json_value)
118                .map_err(|e| format!("msgpack.encode_bytes() failed: {}", e))?;
119
120            let items: Vec<ValueWord> = bytes
121                .iter()
122                .map(|&b| ValueWord::from_i64(b as i64))
123                .collect();
124
125            Ok(ValueWord::from_ok(ValueWord::from_array(Arc::new(items))))
126        },
127        ModuleFunction {
128            description: "Encode a value to MessagePack as a byte array".to_string(),
129            params: vec![ModuleParam {
130                name: "value".to_string(),
131                type_name: "any".to_string(),
132                required: true,
133                description: "Value to encode".to_string(),
134                ..Default::default()
135            }],
136            return_type: Some("Result<Array<int>>".to_string()),
137        },
138    );
139
140    // msgpack.decode_bytes(data: Array<int>) -> Result<any>
141    // Decodes MessagePack from a byte array to a value.
142    module.add_function_with_schema(
143        "decode_bytes",
144        |args: &[ValueWord], _ctx: &ModuleContext| {
145            let arr = args.first().and_then(|a| a.as_any_array()).ok_or_else(|| {
146                "msgpack.decode_bytes() requires an Array<int> argument".to_string()
147            })?;
148
149            let generic = arr.to_generic();
150            let bytes: Result<Vec<u8>, String> = generic
151                .iter()
152                .enumerate()
153                .map(|(i, v)| {
154                    v.as_i64()
155                        .or_else(|| v.as_f64().map(|f| f as i64))
156                        .and_then(|n| u8::try_from(n).ok())
157                        .ok_or_else(|| {
158                            format!(
159                                "msgpack.decode_bytes() element at index {} is not a valid byte",
160                                i
161                            )
162                        })
163                })
164                .collect();
165            let bytes = bytes?;
166
167            let json_value: serde_json::Value = rmp_serde::from_slice(&bytes)
168                .map_err(|e| format!("msgpack.decode_bytes() failed: {}", e))?;
169
170            Ok(ValueWord::from_ok(json_value_to_valueword(json_value)))
171        },
172        ModuleFunction {
173            description: "Decode MessagePack from a byte array to a value".to_string(),
174            params: vec![ModuleParam {
175                name: "data".to_string(),
176                type_name: "Array<int>".to_string(),
177                required: true,
178                description: "Array of byte values (0-255)".to_string(),
179                ..Default::default()
180            }],
181            return_type: Some("Result<any>".to_string()),
182        },
183    );
184
185    module
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191
192    fn test_ctx() -> crate::module_exports::ModuleContext<'static> {
193        let registry = Box::leak(Box::new(crate::type_schema::TypeSchemaRegistry::new()));
194        crate::module_exports::ModuleContext {
195            schemas: registry,
196            invoke_callable: None,
197            raw_invoker: None,
198            function_hashes: None,
199            vm_state: None,
200            granted_permissions: None,
201            scope_constraints: None,
202            set_pending_resume: None,
203            set_pending_frame_resume: None,
204        }
205    }
206
207    #[test]
208    fn test_msgpack_module_creation() {
209        let module = create_msgpack_module();
210        assert_eq!(module.name, "msgpack");
211        assert!(module.has_export("encode"));
212        assert!(module.has_export("decode"));
213        assert!(module.has_export("encode_bytes"));
214        assert!(module.has_export("decode_bytes"));
215    }
216
217    #[test]
218    fn test_encode_decode_roundtrip_string() {
219        let module = create_msgpack_module();
220        let encode_fn = module.get_export("encode").unwrap();
221        let decode_fn = module.get_export("decode").unwrap();
222        let ctx = test_ctx();
223
224        let input = ValueWord::from_string(Arc::new("hello".to_string()));
225        let encoded = encode_fn(&[input], &ctx).unwrap();
226        let hex_str = encoded.as_ok_inner().expect("should be Ok");
227
228        let decoded = decode_fn(&[hex_str.clone()], &ctx).unwrap();
229        let inner = decoded.as_ok_inner().expect("should be Ok");
230        assert_eq!(inner.as_str(), Some("hello"));
231    }
232
233    #[test]
234    fn test_encode_decode_roundtrip_number() {
235        let module = create_msgpack_module();
236        let encode_fn = module.get_export("encode").unwrap();
237        let decode_fn = module.get_export("decode").unwrap();
238        let ctx = test_ctx();
239
240        let input = ValueWord::from_f64(42.5);
241        let encoded = encode_fn(&[input], &ctx).unwrap();
242        let hex_str = encoded.as_ok_inner().expect("should be Ok");
243
244        let decoded = decode_fn(&[hex_str.clone()], &ctx).unwrap();
245        let inner = decoded.as_ok_inner().expect("should be Ok");
246        assert_eq!(inner.as_f64(), Some(42.5));
247    }
248
249    #[test]
250    fn test_encode_decode_roundtrip_bool() {
251        let module = create_msgpack_module();
252        let encode_fn = module.get_export("encode").unwrap();
253        let decode_fn = module.get_export("decode").unwrap();
254        let ctx = test_ctx();
255
256        let input = ValueWord::from_bool(true);
257        let encoded = encode_fn(&[input], &ctx).unwrap();
258        let hex_str = encoded.as_ok_inner().expect("should be Ok");
259
260        let decoded = decode_fn(&[hex_str.clone()], &ctx).unwrap();
261        let inner = decoded.as_ok_inner().expect("should be Ok");
262        assert_eq!(inner.as_bool(), Some(true));
263    }
264
265    #[test]
266    fn test_encode_decode_roundtrip_null() {
267        let module = create_msgpack_module();
268        let encode_fn = module.get_export("encode").unwrap();
269        let decode_fn = module.get_export("decode").unwrap();
270        let ctx = test_ctx();
271
272        let input = ValueWord::none();
273        let encoded = encode_fn(&[input], &ctx).unwrap();
274        let hex_str = encoded.as_ok_inner().expect("should be Ok");
275
276        let decoded = decode_fn(&[hex_str.clone()], &ctx).unwrap();
277        let inner = decoded.as_ok_inner().expect("should be Ok");
278        assert!(inner.is_none());
279    }
280
281    #[test]
282    fn test_encode_decode_roundtrip_array() {
283        let module = create_msgpack_module();
284        let encode_fn = module.get_export("encode").unwrap();
285        let decode_fn = module.get_export("decode").unwrap();
286        let ctx = test_ctx();
287
288        let input = ValueWord::from_array(Arc::new(vec![
289            ValueWord::from_i64(1),
290            ValueWord::from_i64(2),
291            ValueWord::from_i64(3),
292        ]));
293        let encoded = encode_fn(&[input], &ctx).unwrap();
294        let hex_str = encoded.as_ok_inner().expect("should be Ok");
295
296        let decoded = decode_fn(&[hex_str.clone()], &ctx).unwrap();
297        let inner = decoded.as_ok_inner().expect("should be Ok");
298        let arr = inner.as_any_array().expect("should be array").to_generic();
299        assert_eq!(arr.len(), 3);
300        assert_eq!(arr[0].as_i64(), Some(1));
301        assert_eq!(arr[1].as_i64(), Some(2));
302        assert_eq!(arr[2].as_i64(), Some(3));
303    }
304
305    #[test]
306    fn test_encode_decode_roundtrip_object() {
307        let module = create_msgpack_module();
308        let encode_fn = module.get_export("encode").unwrap();
309        let decode_fn = module.get_export("decode").unwrap();
310        let ctx = test_ctx();
311
312        let input = ValueWord::from_hashmap_pairs(
313            vec![
314                ValueWord::from_string(Arc::new("name".to_string())),
315                ValueWord::from_string(Arc::new("age".to_string())),
316            ],
317            vec![
318                ValueWord::from_string(Arc::new("Alice".to_string())),
319                ValueWord::from_i64(30),
320            ],
321        );
322        let encoded = encode_fn(&[input], &ctx).unwrap();
323        let hex_str = encoded.as_ok_inner().expect("should be Ok");
324
325        let decoded = decode_fn(&[hex_str.clone()], &ctx).unwrap();
326        let inner = decoded.as_ok_inner().expect("should be Ok");
327        let (keys, _values, _index) = inner.as_hashmap().expect("should be hashmap");
328        assert_eq!(keys.len(), 2);
329    }
330
331    #[test]
332    fn test_encode_bytes_decode_bytes_roundtrip() {
333        let module = create_msgpack_module();
334        let encode_bytes_fn = module.get_export("encode_bytes").unwrap();
335        let decode_bytes_fn = module.get_export("decode_bytes").unwrap();
336        let ctx = test_ctx();
337
338        let input = ValueWord::from_string(Arc::new("test".to_string()));
339        let encoded = encode_bytes_fn(&[input], &ctx).unwrap();
340        let byte_arr = encoded.as_ok_inner().expect("should be Ok");
341
342        // Verify it's an array of ints
343        let arr = byte_arr
344            .as_any_array()
345            .expect("should be array")
346            .to_generic();
347        assert!(!arr.is_empty());
348        for v in arr.iter() {
349            let byte_val = v.as_i64().expect("should be int");
350            assert!((0..=255).contains(&byte_val));
351        }
352
353        let decoded = decode_bytes_fn(&[byte_arr.clone()], &ctx).unwrap();
354        let inner = decoded.as_ok_inner().expect("should be Ok");
355        assert_eq!(inner.as_str(), Some("test"));
356    }
357
358    #[test]
359    fn test_decode_invalid_hex() {
360        let module = create_msgpack_module();
361        let decode_fn = module.get_export("decode").unwrap();
362        let ctx = test_ctx();
363
364        let input = ValueWord::from_string(Arc::new("not_valid_hex!@#".to_string()));
365        let result = decode_fn(&[input], &ctx);
366        assert!(result.is_err());
367    }
368
369    #[test]
370    fn test_decode_invalid_msgpack() {
371        let module = create_msgpack_module();
372        let decode_fn = module.get_export("decode").unwrap();
373        let ctx = test_ctx();
374
375        // Valid hex but not valid msgpack
376        let input = ValueWord::from_string(Arc::new("deadbeef".to_string()));
377        let result = decode_fn(&[input], &ctx);
378        assert!(result.is_err());
379    }
380
381    #[test]
382    fn test_decode_requires_string() {
383        let module = create_msgpack_module();
384        let decode_fn = module.get_export("decode").unwrap();
385        let ctx = test_ctx();
386
387        let result = decode_fn(&[ValueWord::from_f64(42.0)], &ctx);
388        assert!(result.is_err());
389    }
390
391    #[test]
392    fn test_decode_bytes_requires_array() {
393        let module = create_msgpack_module();
394        let decode_bytes_fn = module.get_export("decode_bytes").unwrap();
395        let ctx = test_ctx();
396
397        let result = decode_bytes_fn(&[ValueWord::from_f64(42.0)], &ctx);
398        assert!(result.is_err());
399    }
400
401    #[test]
402    fn test_decode_bytes_invalid_byte_values() {
403        let module = create_msgpack_module();
404        let decode_bytes_fn = module.get_export("decode_bytes").unwrap();
405        let ctx = test_ctx();
406
407        // Array with value > 255
408        let input = ValueWord::from_array(Arc::new(vec![ValueWord::from_i64(300)]));
409        let result = decode_bytes_fn(&[input], &ctx);
410        assert!(result.is_err());
411    }
412
413    #[test]
414    fn test_hex_and_bytes_encode_same_data() {
415        let module = create_msgpack_module();
416        let encode_fn = module.get_export("encode").unwrap();
417        let encode_bytes_fn = module.get_export("encode_bytes").unwrap();
418        let ctx = test_ctx();
419
420        let input = ValueWord::from_i64(42);
421        let hex_result = encode_fn(&[input.clone()], &ctx).unwrap();
422        let hex_str = hex_result.as_ok_inner().expect("should be Ok");
423        let hex_bytes = hex::decode(hex_str.as_str().unwrap()).unwrap();
424
425        let bytes_result = encode_bytes_fn(&[input], &ctx).unwrap();
426        let byte_arr = bytes_result.as_ok_inner().expect("should be Ok");
427        let arr = byte_arr
428            .as_any_array()
429            .expect("should be array")
430            .to_generic();
431        let arr_bytes: Vec<u8> = arr.iter().map(|v| v.as_i64().unwrap() as u8).collect();
432
433        assert_eq!(hex_bytes, arr_bytes);
434    }
435
436    #[test]
437    fn test_schemas() {
438        let module = create_msgpack_module();
439
440        let encode_schema = module.get_schema("encode").unwrap();
441        assert_eq!(encode_schema.params.len(), 1);
442        assert_eq!(encode_schema.params[0].name, "value");
443        assert!(encode_schema.params[0].required);
444        assert_eq!(encode_schema.return_type.as_deref(), Some("Result<string>"));
445
446        let decode_schema = module.get_schema("decode").unwrap();
447        assert_eq!(decode_schema.params.len(), 1);
448        assert_eq!(decode_schema.params[0].name, "data");
449        assert!(decode_schema.params[0].required);
450        assert_eq!(decode_schema.return_type.as_deref(), Some("Result<any>"));
451
452        let encode_bytes_schema = module.get_schema("encode_bytes").unwrap();
453        assert_eq!(encode_bytes_schema.params.len(), 1);
454        assert_eq!(
455            encode_bytes_schema.return_type.as_deref(),
456            Some("Result<Array<int>>")
457        );
458
459        let decode_bytes_schema = module.get_schema("decode_bytes").unwrap();
460        assert_eq!(decode_bytes_schema.params.len(), 1);
461        assert_eq!(decode_bytes_schema.params[0].type_name, "Array<int>");
462        assert_eq!(
463            decode_bytes_schema.return_type.as_deref(),
464            Some("Result<any>")
465        );
466    }
467
468    #[test]
469    fn test_encode_decode_nested_structure() {
470        let module = create_msgpack_module();
471        let encode_fn = module.get_export("encode").unwrap();
472        let decode_fn = module.get_export("decode").unwrap();
473        let ctx = test_ctx();
474
475        // Build nested: { "users": [{"name": "Alice"}, {"name": "Bob"}] }
476        let user1 = ValueWord::from_hashmap_pairs(
477            vec![ValueWord::from_string(Arc::new("name".to_string()))],
478            vec![ValueWord::from_string(Arc::new("Alice".to_string()))],
479        );
480        let user2 = ValueWord::from_hashmap_pairs(
481            vec![ValueWord::from_string(Arc::new("name".to_string()))],
482            vec![ValueWord::from_string(Arc::new("Bob".to_string()))],
483        );
484        let input = ValueWord::from_hashmap_pairs(
485            vec![ValueWord::from_string(Arc::new("users".to_string()))],
486            vec![ValueWord::from_array(Arc::new(vec![user1, user2]))],
487        );
488
489        let encoded = encode_fn(&[input], &ctx).unwrap();
490        let hex_str = encoded.as_ok_inner().expect("should be Ok");
491
492        let decoded = decode_fn(&[hex_str.clone()], &ctx).unwrap();
493        let inner = decoded.as_ok_inner().expect("should be Ok");
494        let (keys, _values, _index) = inner.as_hashmap().expect("should be hashmap");
495        assert_eq!(keys.len(), 1);
496        assert_eq!(keys[0].as_str(), Some("users"));
497    }
498}