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        // This handles any additional variants when osc11 is enabled
90        _ => J::Null,
91    }
92}
93
94/// Convert serde_json::Value -> IrBundleElement.
95fn bundle_element_from_json(j: &J) -> IrBundleElement {
96    if let J::Object(map) = j {
97        if let Some(J::String(element_type)) = map.get("type") {
98            match element_type.as_str() {
99                "message" => {
100                    if let Some(data) = map.get("data") {
101                        return IrBundleElement::Message(from_json(data));
102                    }
103                }
104                "bundle" => {
105                    if let Some(data) = map.get("data") {
106                        if let IrValue::Bundle(bundle) = from_json(data) {
107                            return IrBundleElement::Bundle(bundle);
108                        }
109                    }
110                }
111                _ => {}
112            }
113        }
114    }
115    // Fallback: treat as message
116    IrBundleElement::Message(from_json(j))
117}
118
119/// Convert serde_json::Value -> IR (best-effort; special objects recognized by $type markers).
120pub fn from_json(j: &J) -> IrValue {
121    match j {
122        J::Null => IrValue::Null,
123        J::Bool(b) => IrValue::Bool(*b),
124        J::Number(n) => n.as_i64().map(IrValue::Integer)
125            .or_else(|| n.as_f64().map(IrValue::Float))
126            .unwrap_or(IrValue::Null),
127        J::String(s) => IrValue::String(s.clone().into_boxed_str()),
128        J::Array(xs) => IrValue::Array(xs.iter().map(from_json).collect()),
129        J::Object(map) => {
130            if let Some(J::String(tag)) = map.get("$type") {
131                match tag.as_str() {
132                    "timestamp" => {
133                        let sec = map.get("seconds").and_then(|v| v.as_i64()).unwrap_or(0);
134                        let ns = map.get("nanos").and_then(|v| v.as_u64()).unwrap_or(0) as u32;
135                        IrValue::Timestamp(IrTimestamp{ seconds: sec, nanos: ns })
136                    }
137                    "binary" => {
138                        let data = map.get("data").and_then(|v| v.as_str()).map(|s| 
139                            base64::engine::general_purpose::STANDARD.decode(s).unwrap_or_default()).unwrap_or_default();
140                        IrValue::Binary(data)
141                    }
142                    "ext" => {
143                        let ext = map.get("ext").and_then(|v| v.as_i64()).unwrap_or(0) as i8;
144                        let data = map.get("data").and_then(|v| v.as_str()).map(|s| 
145                            base64::engine::general_purpose::STANDARD.decode(s).unwrap_or_default()).unwrap_or_default();
146                        IrValue::Ext{ type_id: ext, data }
147                    }
148                    "bundle" => {
149                        let timetag_value = map.get("timetag").and_then(|v| v.as_u64()).unwrap_or(1);
150                        let timetag = IrTimetag { value: timetag_value };
151                        let elements = map.get("elements").and_then(|v| v.as_array())
152                            .map(|arr| arr.iter().map(bundle_element_from_json).collect())
153                            .unwrap_or_default();
154                        IrValue::Bundle(IrBundle { timetag, elements })
155                    }
156                    _ => IrValue::Map(map.iter().map(|(k,v)| (k.clone(), from_json(v))).collect())
157                }
158            } else {
159                IrValue::Map(map.iter().map(|(k,v)| (k.clone(), from_json(v))).collect())
160            }
161        }
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168    use osc_ir::{IrBundle, IrTimetag};
169
170    #[test]
171    fn test_bundle_json_roundtrip() {
172        // Create a bundle with messages and nested bundle
173        let mut bundle = IrBundle::new(IrTimetag::from_ntp(12345));
174        bundle.add_message(IrValue::from("hello"));
175        bundle.add_message(IrValue::from(42));
176
177        let mut nested_bundle = IrBundle::immediate();
178    nested_bundle.add_message(IrValue::from(true));
179    nested_bundle.add_message(IrValue::from(core::f64::consts::PI));
180        
181        bundle.add_bundle(nested_bundle);
182
183        let value = IrValue::Bundle(bundle.clone());
184
185        // Convert to JSON and back
186        let json = to_json(&value);
187        let decoded = from_json(&json);
188
189        assert_eq!(value, decoded);
190
191        // Verify the structure is preserved
192        if let IrValue::Bundle(decoded_bundle) = decoded {
193            assert_eq!(decoded_bundle.timetag.value, 12345);
194            assert_eq!(decoded_bundle.elements.len(), 3);
195            
196            // Check first message
197            assert!(decoded_bundle.elements[0].is_message());
198            assert_eq!(
199                decoded_bundle.elements[0].as_message().unwrap().as_str(),
200                Some("hello")
201            );
202            
203            // Check second message
204            assert!(decoded_bundle.elements[1].is_message());
205            assert_eq!(
206                decoded_bundle.elements[1].as_message().unwrap().as_integer(),
207                Some(42)
208            );
209            
210            // Check nested bundle
211            assert!(decoded_bundle.elements[2].is_bundle());
212            let nested = decoded_bundle.elements[2].as_bundle().unwrap();
213            assert!(nested.is_immediate());
214            assert_eq!(nested.elements.len(), 2);
215        } else {
216            panic!("Expected Bundle variant");
217        }
218    }
219
220    #[test]
221    fn test_deeply_nested_bundle_json() {
222        // Create a deeply nested bundle structure
223        let mut root = IrBundle::immediate();
224        root.add_message(IrValue::from("root"));
225
226        let mut level1 = IrBundle::new(IrTimetag::from_ntp(1000));
227        level1.add_message(IrValue::from("level1"));
228
229        let mut level2 = IrBundle::new(IrTimetag::from_ntp(2000));
230        level2.add_message(IrValue::from("level2"));
231
232        level1.add_bundle(level2);
233        root.add_bundle(level1);
234
235        let value = IrValue::Bundle(root);
236
237        // Test roundtrip
238        let json = to_json(&value);
239        let decoded = from_json(&json);
240        assert_eq!(value, decoded);
241    }
242}