osc_codec_msgpack/
lib.rs

1//! # osc-codec-msgpack
2//!
3//! ⚠️ **EXPERIMENTAL** ⚠️  
4//! This crate is experimental and APIs may change significantly between versions.
5//!
6//! MessagePack codec for the `osc-ir` intermediate representation, enabling efficient binary
7//! serialization of OSC data structures.
8//!
9//! ## Features
10//!
11//! - **Bidirectional Conversion**: Convert `IrValue` to/from MessagePack binary format
12//! - **Efficient Storage**: Compact binary representation with MessagePack
13//! - **Bundle Support**: Full support for OSC bundles with nested structures  
14//! - **Type Preservation**: Native support for binary data, timestamps, and all OSC types
15//! - **Cross-Format Compatibility**: Works seamlessly with JSON codec for the same data
16//!
17//! ## Usage
18//!
19//! ```rust
20//! use osc_ir::{IrValue, IrBundle, IrTimetag};
21//! use osc_codec_msgpack::{to_msgpack, from_msgpack};
22//!
23//! // Create some data
24//! # #[cfg(feature = "osc10")]
25//! # {
26//! let mut bundle = IrBundle::new(IrTimetag::from_ntp(12345));
27//! bundle.add_message(IrValue::from("hello"));
28//! bundle.add_message(IrValue::from(42));
29//! bundle.add_message(IrValue::from(vec![1u8, 2, 3, 4])); // binary data
30//!
31//! let value = IrValue::Bundle(bundle);
32//!
33//! // Convert to MessagePack
34//! let msgpack_data = to_msgpack(&value);
35//! println!("Serialized {} bytes", msgpack_data.len());
36//!
37//! // Convert back from MessagePack
38//! let restored = from_msgpack(&msgpack_data);
39//! assert_eq!(value, restored);
40//! # }
41//! ```
42//!
43//! ## Performance
44//!
45//! MessagePack typically provides:
46//! - **Smaller size** than JSON (especially for binary data)
47//! - **Faster serialization/deserialization** than JSON
48//! - **Native binary support** without encoding overhead
49//!
50//! ## API Reference
51//!
52//! ### Core Functions
53//!
54//! - [`to_msgpack`] - Convert IR to MessagePack binary
55//! - [`from_msgpack`] - Convert MessagePack binary to IR
56//! - [`try_to_msgpack`] - Fallible conversion to MessagePack
57//! - [`try_from_msgpack`] - Fallible conversion from MessagePack
58
59use osc_ir::IrValue;
60
61pub type EncodeResult<T> = Result<T, rmp_serde::encode::Error>;
62pub type DecodeResult<T> = Result<T, rmp_serde::decode::Error>;
63
64pub fn try_to_msgpack(v: &IrValue) -> EncodeResult<Vec<u8>> {
65    rmp_serde::to_vec_named(v)
66}
67
68pub fn to_msgpack(v: &IrValue) -> Vec<u8> {
69    try_to_msgpack(v).expect("serialize")
70}
71
72pub fn try_from_msgpack(bytes: &[u8]) -> DecodeResult<IrValue> {
73    rmp_serde::from_slice::<IrValue>(bytes)
74}
75
76pub fn from_msgpack(bytes: &[u8]) -> IrValue {
77    try_from_msgpack(bytes).expect("deserialize")
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83    use osc_ir::{IrTimestamp, IrBundle, IrTimetag};
84
85    #[test]
86    fn roundtrip_timestamp() {
87        let value = IrValue::from(IrTimestamp {
88            seconds: 123,
89            nanos: 456,
90        });
91        let bytes = try_to_msgpack(&value).expect("encode");
92        let decoded = try_from_msgpack(&bytes).expect("decode");
93        assert_eq!(value, decoded);
94    }
95
96    #[test]
97    fn roundtrip_complex_structure() {
98        let value = IrValue::Map(vec![
99            ("msg".into(), IrValue::from("hello")),
100            ("bin".into(), IrValue::from(vec![1_u8, 2, 3])),
101            (
102                "ext".into(),
103                IrValue::Ext {
104                    type_id: -4,
105                    data: vec![0x10, 0x20, 0x30],
106                },
107            ),
108        ]);
109
110        let bytes = to_msgpack(&value);
111        let decoded = from_msgpack(&bytes);
112        assert_eq!(decoded, value);
113    }
114
115    #[test]
116    fn roundtrip_bundle() {
117        let mut bundle = IrBundle::new(IrTimetag::from_ntp(12345));
118        bundle.add_message(IrValue::from("hello"));
119        bundle.add_message(IrValue::from(42));
120
121        let mut nested_bundle = IrBundle::immediate();
122        nested_bundle.add_message(IrValue::from(true));
123        
124        bundle.add_bundle(nested_bundle);
125
126        let value = IrValue::Bundle(bundle);
127        let bytes = to_msgpack(&value);
128        let decoded = from_msgpack(&bytes);
129        assert_eq!(decoded, value);
130    }
131
132    #[test]
133    fn roundtrip_deeply_nested_bundle() {
134        // Create a deeply nested bundle structure
135        let mut root = IrBundle::immediate();
136        root.add_message(IrValue::from("root"));
137
138        let mut level1 = IrBundle::new(IrTimetag::from_ntp(1000));
139        level1.add_message(IrValue::from("level1"));
140
141        let mut level2 = IrBundle::new(IrTimetag::from_ntp(2000));
142        level2.add_message(IrValue::from("level2"));
143
144        level1.add_bundle(level2);
145        root.add_bundle(level1);
146
147        let value = IrValue::Bundle(root);
148
149        // Test roundtrip
150        let bytes = to_msgpack(&value);
151        let decoded = from_msgpack(&bytes);
152        assert_eq!(value, decoded);
153    }
154
155    #[test]
156    fn roundtrip_osc_message_like() {
157        // Construct an IrValue that matches the adapter's OSC message representation
158        let value = IrValue::Map(vec![
159            ("$type".into(), IrValue::from("osc.message")),
160            ("address".into(), IrValue::from("/test")),
161            (
162                "args".into(),
163                IrValue::Array(vec![
164                    IrValue::Integer(7),
165                    IrValue::Float(1.5),
166                    IrValue::from("text"),
167                    IrValue::Binary(vec![1_u8, 2, 3]),
168                ]),
169            ),
170        ]);
171
172        let bytes = to_msgpack(&value);
173        let decoded = from_msgpack(&bytes);
174
175        // Structure-preserving roundtrip
176        assert_eq!(decoded, value);
177
178        // Extract and validate fields similar to adapter::try_extract_message
179        let map = decoded.as_map().expect("expected map");
180        let address = map.iter().find(|(k, _)| k == "address").unwrap().1.as_str();
181        assert_eq!(address, Some("/test"));
182
183        let args = map
184            .iter()
185            .find(|(k, _)| k == "args")
186            .unwrap()
187            .1
188            .as_array()
189            .expect("expected args array");
190
191        assert_eq!(args.len(), 4);
192        assert_eq!(args[0].as_integer(), Some(7));
193        assert!((args[1].as_float().unwrap() - 1.5).abs() < f64::EPSILON);
194        assert_eq!(args[2].as_str(), Some("text"));
195        assert_eq!(args[3].as_binary(), Some(&[1_u8, 2, 3][..]));
196    }
197
198    #[test]
199    fn msgpack_bytes_are_valid_and_match_contents() {
200        use std::io::Cursor;
201        use rmpv::{decode::read_value, Value};
202
203        // Prepare an OSC-like message map as IrValue
204        let value = IrValue::Map(vec![
205            ("$type".into(), IrValue::from("osc.message")),
206            ("address".into(), IrValue::from("/validate")),
207            (
208                "args".into(),
209                IrValue::Array(vec![
210                    IrValue::Integer(123),
211                    IrValue::Float(-2.5),
212                    IrValue::from("ok"),
213                    IrValue::Binary(vec![0xAA, 0xBB]),
214                ]),
215            ),
216        ]);
217
218        // Encode to MessagePack
219        let bytes = to_msgpack(&value);
220
221        // Ensure bytes are valid MessagePack by decoding with rmpv
222        let mut cursor = Cursor::new(&bytes);
223        let root = read_value(&mut cursor).expect("must decode as msgpack Value");
224
225        // Helper: unwrap serde's externally tagged enum representation
226        fn unwrap_enum(v: &Value) -> (&str, &Value) {
227            match v {
228                // Map form: { "Variant": payload }
229                Value::Map(kv) if kv.len() == 1 => {
230                    let (k, v) = &kv[0];
231                    let name = match k { Value::String(s) => s.as_str().expect("variant name"), _ => panic!("invalid enum key") };
232                    (name, v)
233                }
234                // Array form: ["Variant", payload]
235                Value::Array(items) if items.len() == 2 => {
236                    let name = match &items[0] { Value::String(s) => s.as_str().expect("variant name"), _ => panic!("invalid enum tag array") };
237                    (name, &items[1])
238                }
239                other => panic!("unexpected enum encoding: {:?}", other),
240            }
241        }
242
243        // Root must be the IrValue::Map(enum) variant
244        let (root_variant, root_payload) = unwrap_enum(&root);
245        assert_eq!(root_variant, "Map");
246
247        // Payload is Vec<(String, IrValue)> serialized as array of 2-element arrays
248        let entries = match root_payload { Value::Array(a) => a, other => panic!("expected entries array, got {:?}", other) };
249
250        // Collect into hashmap-like view: key -> encoded IrValue
251    let get = |key: &str| -> &Value {
252            entries
253                .iter()
254                .find_map(|entry| match entry {
255                    Value::Array(items) if items.len() == 2 => match (&items[0], &items[1]) {
256                        (Value::String(s), v) if s.as_str() == Some(key) => Some(v),
257                        _ => None,
258                    },
259                    _ => None,
260                })
261                .expect("entry not found")
262        };
263
264        // $type: IrValue::String("osc.message") -> enum String with payload string
265        let (ty_variant, ty_payload) = unwrap_enum(get("$type"));
266        assert_eq!(ty_variant, "String");
267        assert!(matches!(ty_payload, Value::String(s) if s.as_str() == Some("osc.message")));
268
269        // address: IrValue::String("/validate")
270        let (addr_variant, addr_payload) = unwrap_enum(get("address"));
271        assert_eq!(addr_variant, "String");
272        assert!(matches!(addr_payload, Value::String(s) if s.as_str() == Some("/validate")));
273
274        // args: IrValue::Array([...]) -> enum Array with payload array of encoded IrValue
275        let (args_variant, args_payload) = unwrap_enum(get("args"));
276        assert_eq!(args_variant, "Array");
277        let args = match args_payload { Value::Array(a) => a, other => panic!("expected args payload array, got {:?}", other) };
278        assert_eq!(args.len(), 4);
279
280        // 0: Integer(123)
281        let (v0_variant, v0_payload) = unwrap_enum(&args[0]);
282        assert_eq!(v0_variant, "Integer");
283        assert!(matches!(v0_payload, Value::Integer(i) if i.as_i64() == Some(123)));
284
285        // 1: Float(-2.5)
286        let (v1_variant, v1_payload) = unwrap_enum(&args[1]);
287        assert_eq!(v1_variant, "Float");
288        assert!(matches!(v1_payload, Value::F64(x) if (*x + 2.5).abs() < f64::EPSILON));
289
290        // 2: String("ok")
291        let (v2_variant, v2_payload) = unwrap_enum(&args[2]);
292        assert_eq!(v2_variant, "String");
293        assert!(matches!(v2_payload, Value::String(s) if s.as_str() == Some("ok")));
294
295        // 3: Binary([0xAA, 0xBB])
296        let (v3_variant, v3_payload) = unwrap_enum(&args[3]);
297        assert_eq!(v3_variant, "Binary");
298        assert!(matches!(v3_payload, Value::Binary(b) if b.as_slice() == [0xAA, 0xBB]));
299    }
300}