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