osc_codec_json/
lib.rs

1//! # osc-codec-json
2//!
3//! ⚠️ **EXPERIMENTAL** ⚠️  
4//! This crate is experimental and APIs may change significantly between versions.
5//!
6//! JSON codec for the `osc-ir` intermediate representation, enabling seamless conversion
7//! between OSC data structures and JSON format.
8//!
9//! ## Features
10//!
11//! - **Bidirectional Conversion**: Convert `IrValue` to/from JSON
12//! - **Bundle Support**: Full support for OSC bundles with nested structures
13//! - **Type Preservation**: Special handling for binary data, timestamps, and extended types
14//! - **OSC Compatibility**: Support for OSC 1.0 and 1.1 features via feature flags
15//!
16//! ## Usage
17//!
18//! ```rust
19//! use osc_ir::{IrValue, IrBundle, IrTimetag};
20//! use osc_codec_json::{to_json, from_json};
21//!
22//! // Create some data
23//! # #[cfg(feature = "osc10")]
24//! # {
25//! let mut bundle = IrBundle::new(IrTimetag::from_ntp(12345));
26//! bundle.add_message(IrValue::from("hello"));
27//! bundle.add_message(IrValue::from(42));
28//!
29//! let value = IrValue::Bundle(bundle);
30//!
31//! // Convert to JSON
32//! let json = to_json(&value);
33//! println!("{}", serde_json::to_string_pretty(&json).unwrap());
34//!
35//! // Convert back from JSON
36//! let restored = from_json(&json);
37//! assert_eq!(value, restored);
38//! # }
39//! ```
40
41use osc_ir::{IrValue, IrTimestamp, IrBundle, IrBundleElement, IrTimetag};
42use serde_json::Value as J;
43use base64::Engine;
44
45/// Convert IrBundleElement -> serde_json::Value.
46fn bundle_element_to_json(element: &IrBundleElement) -> J {
47    match element {
48        IrBundleElement::Message(msg) => J::Object([
49            ("type".to_string(), J::from("message")),
50            ("data".to_string(), to_json(msg)),
51        ].into_iter().collect()),
52        IrBundleElement::Bundle(bundle) => J::Object([
53            ("type".to_string(), J::from("bundle")),
54            ("data".to_string(), to_json(&IrValue::Bundle(bundle.clone()))),
55        ].into_iter().collect()),
56    }
57}
58
59/// Convert IR -> serde_json::Value.
60pub fn to_json(v: &IrValue) -> J {
61    match v {
62        IrValue::Null => J::Null,
63        IrValue::Bool(b) => J::Bool(*b),
64        IrValue::Integer(i) => J::from(*i),
65        IrValue::Float(x) => J::from(*x),
66        IrValue::String(s) => J::from(s.as_ref()),
67        IrValue::Binary(bytes) => J::Object([
68            ("$type".to_string(), J::from("binary")),
69            ("data".to_string(), J::from(base64::engine::general_purpose::STANDARD.encode(bytes))),
70        ].into_iter().collect()),
71        IrValue::Array(xs) => J::Array(xs.iter().map(to_json).collect()),
72        IrValue::Map(entries) => J::Object(entries.iter().map(|(k, v)| (k.clone(), to_json(v))).collect()),
73        IrValue::Timestamp(IrTimestamp{seconds, nanos}) => J::Object([
74            ("$type".to_string(), J::from("timestamp")),
75            ("seconds".to_string(), J::from(*seconds)),
76            ("nanos".to_string(), J::from(*nanos as u64)),
77        ].into_iter().collect()),
78        IrValue::Ext{ type_id, data } => J::Object([
79            ("$type".to_string(), J::from("ext")),
80            ("ext".to_string(), J::from(*type_id as i64)),
81            ("data".to_string(), J::from(base64::engine::general_purpose::STANDARD.encode(data))),
82        ].into_iter().collect()),
83        IrValue::Bundle(bundle) => J::Object([
84            ("$type".to_string(), J::from("bundle")),
85            ("timetag".to_string(), J::from(bundle.timetag.value)),
86            ("elements".to_string(), J::Array(bundle.elements.iter().map(bundle_element_to_json).collect())),
87        ].into_iter().collect()),
88        // OSC 1.1 Color and MIDI types - currently serialized as null
89        // These will be properly implemented in a future version
90        #[cfg(feature = "osc11")]
91        IrValue::Color { .. } => J::Null,
92        #[cfg(feature = "osc11")]
93        IrValue::Midi { .. } => J::Null,
94    }
95}
96
97/// Convert serde_json::Value -> IrBundleElement.
98fn bundle_element_from_json(j: &J) -> IrBundleElement {
99    if let J::Object(map) = j {
100        if let Some(J::String(element_type)) = map.get("type") {
101            match element_type.as_str() {
102                "message" => {
103                    if let Some(data) = map.get("data") {
104                        return IrBundleElement::Message(from_json(data));
105                    }
106                }
107                "bundle" => {
108                    if let Some(data) = map.get("data") {
109                        if let IrValue::Bundle(bundle) = from_json(data) {
110                            return IrBundleElement::Bundle(bundle);
111                        }
112                    }
113                }
114                _ => {}
115            }
116        }
117    }
118    // Fallback: treat as message
119    IrBundleElement::Message(from_json(j))
120}
121
122/// Convert serde_json::Value -> IR (best-effort; special objects recognized by $type markers).
123pub fn from_json(j: &J) -> IrValue {
124    match j {
125        J::Null => IrValue::Null,
126        J::Bool(b) => IrValue::Bool(*b),
127        J::Number(n) => n.as_i64().map(IrValue::Integer)
128            .or_else(|| n.as_f64().map(IrValue::Float))
129            .unwrap_or(IrValue::Null),
130        J::String(s) => IrValue::String(s.clone().into_boxed_str()),
131        J::Array(xs) => IrValue::Array(xs.iter().map(from_json).collect()),
132        J::Object(map) => {
133            if let Some(J::String(tag)) = map.get("$type") {
134                match tag.as_str() {
135                    "timestamp" => {
136                        let sec = map.get("seconds").and_then(|v| v.as_i64()).unwrap_or(0);
137                        let ns = map.get("nanos").and_then(|v| v.as_u64()).unwrap_or(0) as u32;
138                        IrValue::Timestamp(IrTimestamp{ seconds: sec, nanos: ns })
139                    }
140                    "binary" => {
141                        let data = map.get("data").and_then(|v| v.as_str()).map(|s| 
142                            base64::engine::general_purpose::STANDARD.decode(s).unwrap_or_default()).unwrap_or_default();
143                        IrValue::Binary(data)
144                    }
145                    "ext" => {
146                        let ext = map.get("ext").and_then(|v| v.as_i64()).unwrap_or(0) as i8;
147                        let data = map.get("data").and_then(|v| v.as_str()).map(|s| 
148                            base64::engine::general_purpose::STANDARD.decode(s).unwrap_or_default()).unwrap_or_default();
149                        IrValue::Ext{ type_id: ext, data }
150                    }
151                    "bundle" => {
152                        let timetag_value = map.get("timetag").and_then(|v| v.as_u64()).unwrap_or(1);
153                        let timetag = IrTimetag { value: timetag_value };
154                        let elements = map.get("elements").and_then(|v| v.as_array())
155                            .map(|arr| arr.iter().map(bundle_element_from_json).collect())
156                            .unwrap_or_default();
157                        IrValue::Bundle(IrBundle { timetag, elements })
158                    }
159                    _ => IrValue::Map(map.iter().map(|(k,v)| (k.clone(), from_json(v))).collect())
160                }
161            } else {
162                IrValue::Map(map.iter().map(|(k,v)| (k.clone(), from_json(v))).collect())
163            }
164        }
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171    use osc_ir::{IrBundle, IrTimetag};
172
173    #[test]
174    fn test_bundle_json_roundtrip() {
175        // Create a bundle with messages and nested bundle
176        let mut bundle = IrBundle::new(IrTimetag::from_ntp(12345));
177        bundle.add_message(IrValue::from("hello"));
178        bundle.add_message(IrValue::from(42));
179
180        let mut nested_bundle = IrBundle::immediate();
181    nested_bundle.add_message(IrValue::from(true));
182    nested_bundle.add_message(IrValue::from(core::f64::consts::PI));
183        
184        bundle.add_bundle(nested_bundle);
185
186        let value = IrValue::Bundle(bundle.clone());
187
188        // Convert to JSON and back
189        let json = to_json(&value);
190        let decoded = from_json(&json);
191
192        assert_eq!(value, decoded);
193
194        // Verify the structure is preserved
195        if let IrValue::Bundle(decoded_bundle) = decoded {
196            assert_eq!(decoded_bundle.timetag.value, 12345);
197            assert_eq!(decoded_bundle.elements.len(), 3);
198            
199            // Check first message
200            assert!(decoded_bundle.elements[0].is_message());
201            assert_eq!(
202                decoded_bundle.elements[0].as_message().unwrap().as_str(),
203                Some("hello")
204            );
205            
206            // Check second message
207            assert!(decoded_bundle.elements[1].is_message());
208            assert_eq!(
209                decoded_bundle.elements[1].as_message().unwrap().as_integer(),
210                Some(42)
211            );
212            
213            // Check nested bundle
214            assert!(decoded_bundle.elements[2].is_bundle());
215            let nested = decoded_bundle.elements[2].as_bundle().unwrap();
216            assert!(nested.is_immediate());
217            assert_eq!(nested.elements.len(), 2);
218        } else {
219            panic!("Expected Bundle variant");
220        }
221    }
222
223    #[test]
224    fn test_deeply_nested_bundle_json() {
225        // Create a deeply nested bundle structure
226        let mut root = IrBundle::immediate();
227        root.add_message(IrValue::from("root"));
228
229        let mut level1 = IrBundle::new(IrTimetag::from_ntp(1000));
230        level1.add_message(IrValue::from("level1"));
231
232        let mut level2 = IrBundle::new(IrTimetag::from_ntp(2000));
233        level2.add_message(IrValue::from("level2"));
234
235        level1.add_bundle(level2);
236        root.add_bundle(level1);
237
238        let value = IrValue::Bundle(root);
239
240        // Test roundtrip
241        let json = to_json(&value);
242        let decoded = from_json(&json);
243        assert_eq!(value, decoded);
244    }
245}