osc_ir/
lib.rs

1//! # osc-ir
2//!
3//! ⚠️ **EXPERIMENTAL** ⚠️  
4//! This crate is experimental and APIs may change significantly between versions.
5//!
6//! A protocol-agnostic Intermediate Representation (IR) for OSC-adjacent data structures,
7//! designed to work seamlessly with JSON, MessagePack, and other serialization formats.
8//!
9//! ## Features
10//!
11//! - **OSC Version Support**: Configurable OSC 1.0 and OSC 1.1 support via feature flags
12//! - **no_std Compatible**: Core functionality works without std (requires `alloc` feature for owned containers)
13//! - **Bundle Support**: Full OSC Bundle implementation with nested bundle support  
14//! - **Flexible Types**: Support for all OSC types including timestamps, binary data, and extensible types
15//! - **Serde Integration**: Optional serde support for JSON/MessagePack serialization
16//!
17//! ## Basic Usage
18//!
19//! ```rust
20//! use osc_ir::{IrValue, IrBundle, IrTimetag};
21//!
22//! // Create basic values
23//! let message = IrValue::from("hello world");
24//! let number = IrValue::from(42);
25//! let boolean = IrValue::from(true);
26//!
27//! // Create arrays
28//! let array = IrValue::from(vec![
29//!     IrValue::from(1),
30//!     IrValue::from(2), 
31//!     IrValue::from(3)
32//! ]);
33//!
34//! // Create bundles with timetags
35//! # #[cfg(feature = "osc10")]
36//! # {
37//! let mut bundle = IrBundle::new(IrTimetag::from_ntp(12345));
38//! bundle.add_message(message);
39//! bundle.add_message(number);
40//!
41//! let bundle_value = IrValue::Bundle(bundle);
42//! # }
43//! ```
44
45#![cfg_attr(not(test), no_std)]
46
47extern crate alloc;
48use alloc::{boxed::Box, string::String, vec::Vec};
49use core::fmt;
50
51#[cfg(feature = "serde")]
52use serde::{Deserialize, Serialize};
53
54/// MessagePack-friendly timestamp; interoperable with JSON via RFC3339 if needed.
55#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
56#[derive(Clone, Copy, Debug, PartialEq, Eq)]
57pub struct IrTimestamp {
58    pub seconds: i64,
59    pub nanos: u32,
60}
61
62/// OSC-compatible timetag for bundle scheduling.
63/// A value of 1 indicates "immediately", larger values represent NTP-style timestamps.
64/// Available with OSC 1.0+ support.
65#[cfg(feature = "osc10")]
66#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
67#[derive(Clone, Copy, Debug, PartialEq, Eq)]
68pub struct IrTimetag {
69    pub value: u64,
70}
71
72#[cfg(feature = "osc10")]
73impl IrTimetag {
74    /// Creates a timetag for immediate execution
75    pub fn immediate() -> Self {
76        Self { value: 1 }
77    }
78
79    /// Creates a timetag from an NTP-style timestamp
80    pub fn from_ntp(ntp_time: u64) -> Self {
81        Self { value: ntp_time }
82    }
83
84    /// Returns true if this timetag indicates immediate execution
85    pub fn is_immediate(&self) -> bool {
86        self.value == 1
87    }
88}
89
90/// An element that can be contained within an OSC bundle.
91/// Can be either a message (represented as an IrValue) or a nested bundle.
92/// Available with OSC 1.0+ support.
93#[cfg(feature = "osc10")]
94#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
95#[derive(Clone, Debug, PartialEq)]
96pub enum IrBundleElement {
97    /// A message or other data structure
98    Message(IrValue),
99    /// A nested bundle
100    Bundle(IrBundle),
101}
102
103/// OSC Bundle structure supporting nested bundles with timetags.
104/// Available with OSC 1.0+ support.
105#[cfg(feature = "osc10")]
106#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
107#[derive(Clone, Debug, PartialEq)]
108pub struct IrBundle {
109    /// When this bundle should be executed
110    pub timetag: IrTimetag,
111    /// Elements contained in this bundle (messages or nested bundles)
112    pub elements: Vec<IrBundleElement>,
113}
114
115#[cfg(feature = "osc10")]
116impl IrBundle {
117    /// Creates a new bundle with immediate execution
118    pub fn immediate() -> Self {
119        Self {
120            timetag: IrTimetag::immediate(),
121            elements: Vec::new(),
122        }
123    }
124
125    /// Creates a new bundle with the specified timetag
126    pub fn new(timetag: IrTimetag) -> Self {
127        Self {
128            timetag,
129            elements: Vec::new(),
130        }
131    }
132
133    /// Adds a message to this bundle
134    pub fn add_message(&mut self, message: IrValue) {
135        self.elements.push(IrBundleElement::Message(message));
136    }
137
138    /// Adds a nested bundle to this bundle
139    pub fn add_bundle(&mut self, bundle: IrBundle) {
140        self.elements.push(IrBundleElement::Bundle(bundle));
141    }
142
143    /// Adds an element to this bundle
144    pub fn add_element(&mut self, element: IrBundleElement) {
145        self.elements.push(element);
146    }
147
148    /// Returns true if this bundle is empty (has no elements)
149    pub fn is_empty(&self) -> bool {
150        self.elements.is_empty()
151    }
152
153    /// Returns the number of elements in this bundle
154    pub fn len(&self) -> usize {
155        self.elements.len()
156    }
157
158    /// Returns true if this bundle should be executed immediately
159    pub fn is_immediate(&self) -> bool {
160        self.timetag.is_immediate()
161    }
162}
163
164#[cfg(feature = "osc10")]
165impl IrBundleElement {
166    /// Returns true if this element is a message
167    pub fn is_message(&self) -> bool {
168        matches!(self, IrBundleElement::Message(_))
169    }
170
171    /// Returns true if this element is a bundle
172    pub fn is_bundle(&self) -> bool {
173        matches!(self, IrBundleElement::Bundle(_))
174    }
175
176    /// Returns a reference to the message if this element is a message
177    pub fn as_message(&self) -> Option<&IrValue> {
178        match self {
179            IrBundleElement::Message(msg) => Some(msg),
180            _ => None,
181        }
182    }
183
184    /// Returns a reference to the bundle if this element is a bundle
185    pub fn as_bundle(&self) -> Option<&IrBundle> {
186        match self {
187            IrBundleElement::Bundle(bundle) => Some(bundle),
188            _ => None,
189        }
190    }
191}
192
193/// Protocol-agnostic value space.
194#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
195#[derive(Clone, Debug, PartialEq, Default)]
196pub enum IrValue {
197    #[default]
198    Null,
199    Bool(bool),
200    Integer(i64),
201    Float(f64),
202    String(Box<str>),
203    #[cfg_attr(feature = "serde", serde(with = "serde_bytes"))]
204    Binary(Vec<u8>),
205    Array(Vec<IrValue>),
206    /// Map keys are Strings for JSON compatibility.
207    Map(Vec<(String, IrValue)>),
208    Timestamp(IrTimestamp),
209    /// MessagePack Ext type compatibility; also useful to carry OSC-specific tags.
210    Ext {
211        type_id: i8,
212        #[cfg_attr(feature = "serde", serde(with = "serde_bytes"))]
213        data: Vec<u8>,
214    },
215    /// OSC Bundle with timetag and nested elements
216    /// Available with OSC 1.0+ support.
217    #[cfg(feature = "osc10")]
218    Bundle(IrBundle),
219    /// OSC 1.1 Color type (RGBA)
220    /// Available with OSC 1.1+ support.
221    #[cfg(feature = "osc11")]
222    Color {
223        r: u8,
224        g: u8,
225        b: u8,
226        a: u8,
227    },
228    /// OSC 1.1 MIDI message
229    /// Available with OSC 1.1+ support.
230    #[cfg(feature = "osc11")]
231    Midi {
232        port: u8,
233        status: u8,
234        data1: u8,
235        data2: u8,
236    },
237}
238
239impl fmt::Display for IrValue {
240    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
241        write!(f, "{:?}", self)
242    }
243}
244
245impl IrValue {
246    pub fn null() -> Self {
247        IrValue::Null
248    }
249
250    pub fn is_null(&self) -> bool {
251        matches!(self, IrValue::Null)
252    }
253
254    pub fn as_bool(&self) -> Option<bool> {
255        match self {
256            IrValue::Bool(v) => Some(*v),
257            _ => None,
258        }
259    }
260
261    pub fn as_integer(&self) -> Option<i64> {
262        match self {
263            IrValue::Integer(v) => Some(*v),
264            _ => None,
265        }
266    }
267
268    pub fn as_float(&self) -> Option<f64> {
269        match self {
270            IrValue::Float(v) => Some(*v),
271            _ => None,
272        }
273    }
274
275    pub fn as_str(&self) -> Option<&str> {
276        match self {
277            IrValue::String(v) => Some(v.as_ref()),
278            _ => None,
279        }
280    }
281
282    pub fn as_binary(&self) -> Option<&[u8]> {
283        match self {
284            IrValue::Binary(v) => Some(v.as_slice()),
285            _ => None,
286        }
287    }
288
289    pub fn as_array(&self) -> Option<&[IrValue]> {
290        match self {
291            IrValue::Array(v) => Some(v.as_slice()),
292            _ => None,
293        }
294    }
295
296    pub fn as_map(&self) -> Option<&[(String, IrValue)]> {
297        match self {
298            IrValue::Map(v) => Some(v.as_slice()),
299            _ => None,
300        }
301    }
302
303    pub fn as_timestamp(&self) -> Option<&IrTimestamp> {
304        match self {
305            IrValue::Timestamp(v) => Some(v),
306            _ => None,
307        }
308    }
309
310    pub fn as_ext(&self) -> Option<(i8, &[u8])> {
311        match self {
312            IrValue::Ext { type_id, data } => Some((*type_id, data.as_slice())),
313            _ => None,
314        }
315    }
316
317    #[cfg(feature = "osc10")]
318    pub fn as_bundle(&self) -> Option<&IrBundle> {
319        match self {
320            IrValue::Bundle(bundle) => Some(bundle),
321            _ => None,
322        }
323    }
324
325    #[cfg(feature = "osc11")]
326    pub fn as_color(&self) -> Option<(u8, u8, u8, u8)> {
327        match self {
328            IrValue::Color { r, g, b, a } => Some((*r, *g, *b, *a)),
329            _ => None,
330        }
331    }
332
333    #[cfg(feature = "osc11")]
334    pub fn as_midi(&self) -> Option<(u8, u8, u8, u8)> {
335        match self {
336            IrValue::Midi { port, status, data1, data2 } => Some((*port, *status, *data1, *data2)),
337            _ => None,
338        }
339    }
340}
341
342impl From<()> for IrValue {
343    fn from(_: ()) -> Self {
344        IrValue::Null
345    }
346}
347
348impl From<bool> for IrValue {
349    fn from(v: bool) -> Self {
350        IrValue::Bool(v)
351    }
352}
353
354impl From<i8> for IrValue {
355    fn from(v: i8) -> Self {
356        IrValue::Integer(v as i64)
357    }
358}
359
360impl From<i16> for IrValue {
361    fn from(v: i16) -> Self {
362        IrValue::Integer(v as i64)
363    }
364}
365
366impl From<i32> for IrValue {
367    fn from(v: i32) -> Self {
368        IrValue::Integer(v as i64)
369    }
370}
371
372impl From<i64> for IrValue {
373    fn from(v: i64) -> Self {
374        IrValue::Integer(v)
375    }
376}
377
378impl From<isize> for IrValue {
379    fn from(v: isize) -> Self {
380        IrValue::Integer(v as i64)
381    }
382}
383
384impl From<u8> for IrValue {
385    fn from(v: u8) -> Self {
386        IrValue::Integer(v as i64)
387    }
388}
389
390impl From<u16> for IrValue {
391    fn from(v: u16) -> Self {
392        IrValue::Integer(v as i64)
393    }
394}
395
396impl From<u32> for IrValue {
397    fn from(v: u32) -> Self {
398        IrValue::Integer(v as i64)
399    }
400}
401
402impl From<f32> for IrValue {
403    fn from(v: f32) -> Self {
404        IrValue::Float(v as f64)
405    }
406}
407
408impl From<f64> for IrValue {
409    fn from(v: f64) -> Self {
410        IrValue::Float(v)
411    }
412}
413
414impl From<String> for IrValue {
415    fn from(v: String) -> Self {
416        IrValue::String(v.into_boxed_str())
417    }
418}
419
420impl From<Box<str>> for IrValue {
421    fn from(v: Box<str>) -> Self {
422        IrValue::String(v)
423    }
424}
425
426impl From<&str> for IrValue {
427    fn from(v: &str) -> Self {
428        IrValue::String(v.into())
429    }
430}
431
432impl From<Vec<u8>> for IrValue {
433    fn from(v: Vec<u8>) -> Self {
434        IrValue::Binary(v)
435    }
436}
437
438impl From<&[u8]> for IrValue {
439    fn from(v: &[u8]) -> Self {
440        IrValue::Binary(v.to_vec())
441    }
442}
443
444impl From<Vec<IrValue>> for IrValue {
445    fn from(v: Vec<IrValue>) -> Self {
446        IrValue::Array(v)
447    }
448}
449
450impl From<Vec<(String, IrValue)>> for IrValue {
451    fn from(v: Vec<(String, IrValue)>) -> Self {
452        IrValue::Map(v)
453    }
454}
455
456impl From<IrTimestamp> for IrValue {
457    fn from(v: IrTimestamp) -> Self {
458        IrValue::Timestamp(v)
459    }
460}
461
462#[cfg(feature = "osc10")]
463impl From<IrBundle> for IrValue {
464    fn from(v: IrBundle) -> Self {
465        IrValue::Bundle(v)
466    }
467}
468
469#[cfg(feature = "osc10")]
470impl From<IrTimetag> for IrBundle {
471    fn from(timetag: IrTimetag) -> Self {
472        IrBundle {
473            timetag,
474            elements: Vec::new(),
475        }
476    }
477}
478
479#[cfg(feature = "osc10")]
480impl From<IrValue> for IrBundleElement {
481    fn from(value: IrValue) -> Self {
482        IrBundleElement::Message(value)
483    }
484}
485
486#[cfg(feature = "osc10")]
487impl From<IrBundle> for IrBundleElement {
488    fn from(bundle: IrBundle) -> Self {
489        IrBundleElement::Bundle(bundle)
490    }
491}
492
493impl IrValue {
494    /// Creates a new OSC 1.1 Color value
495    #[cfg(feature = "osc11")]
496    pub fn color(r: u8, g: u8, b: u8, a: u8) -> Self {
497        IrValue::Color { r, g, b, a }
498    }
499
500    /// Creates a new OSC 1.1 MIDI message value
501    #[cfg(feature = "osc11")]
502    pub fn midi(port: u8, status: u8, data1: u8, data2: u8) -> Self {
503        IrValue::Midi { port, status, data1, data2 }
504    }
505}
506
507#[cfg(test)]
508mod tests {
509    use super::*;
510    use alloc::vec;
511
512    #[test]
513    fn conversions_work() {
514        assert_eq!(IrValue::from(true).as_bool(), Some(true));
515        assert_eq!(IrValue::from(42_i32).as_integer(), Some(42));
516        assert_eq!(IrValue::from(3.5_f32).as_float(), Some(3.5));
517        assert_eq!(IrValue::from("hi").as_str(), Some("hi"));
518        assert_eq!(IrValue::from(vec![1_u8, 2]).as_binary(), Some(&[1, 2][..]));
519        let arr = IrValue::from(vec![IrValue::from(1_i32), IrValue::from(2_i32)]);
520        assert_eq!(arr.as_array().unwrap().len(), 2);
521        let ts = IrTimestamp {
522            seconds: 1,
523            nanos: 2,
524        };
525        assert_eq!(IrValue::from(ts).as_timestamp(), Some(&ts));
526    }
527
528    #[test]
529    fn ext_and_default_helpers() {
530        let ext = IrValue::Ext {
531            type_id: 9,
532            data: vec![0xAA, 0xBB],
533        };
534        assert_eq!(ext.as_ext(), Some((9, &[0xAA, 0xBB][..])));
535
536        let default = IrValue::default();
537        assert!(default.is_null());
538        assert!(default.as_array().is_none());
539    }
540
541    #[test]
542    #[cfg(feature = "osc10")]
543    fn bundle_creation_and_nesting() {
544        // Create an immediate bundle
545        let mut bundle = IrBundle::immediate();
546        assert!(bundle.is_immediate());
547        assert!(bundle.is_empty());
548        assert_eq!(bundle.len(), 0);
549
550        // Add a message
551        bundle.add_message(IrValue::from("hello"));
552        assert!(!bundle.is_empty());
553        assert_eq!(bundle.len(), 1);
554
555        // Create a nested bundle
556        let mut nested_bundle = IrBundle::new(IrTimetag::from_ntp(1000));
557        assert!(!nested_bundle.is_immediate());
558        nested_bundle.add_message(IrValue::from(42));
559        nested_bundle.add_message(IrValue::from(true));
560
561        // Add the nested bundle to the main bundle
562        bundle.add_bundle(nested_bundle);
563        assert_eq!(bundle.len(), 2);
564
565        // Test element access
566        assert!(bundle.elements[0].is_message());
567        assert!(!bundle.elements[0].is_bundle());
568        assert_eq!(bundle.elements[0].as_message().unwrap().as_str(), Some("hello"));
569
570        assert!(!bundle.elements[1].is_message());
571        assert!(bundle.elements[1].is_bundle());
572        let nested = bundle.elements[1].as_bundle().unwrap();
573        assert_eq!(nested.len(), 2);
574        assert_eq!(nested.timetag.value, 1000);
575    }
576
577    #[test]
578    #[cfg(feature = "osc10")]
579    fn bundle_conversions() {
580        // Test IrBundle -> IrValue conversion
581        let bundle = IrBundle::immediate();
582        let value = IrValue::from(bundle.clone());
583        assert_eq!(value.as_bundle(), Some(&bundle));
584
585        // Test IrValue -> IrBundleElement conversion
586        let message = IrValue::from("test");
587        let element = IrBundleElement::from(message.clone());
588        assert!(element.is_message());
589        assert_eq!(element.as_message(), Some(&message));
590
591        // Test IrBundle -> IrBundleElement conversion
592        let element = IrBundleElement::from(bundle.clone());
593        assert!(element.is_bundle());
594        assert_eq!(element.as_bundle(), Some(&bundle));
595    }
596
597    #[test]
598    #[cfg(feature = "osc10")]
599    fn timetag_functionality() {
600        let immediate = IrTimetag::immediate();
601        assert!(immediate.is_immediate());
602        assert_eq!(immediate.value, 1);
603
604        let ntp_time = IrTimetag::from_ntp(12345678);
605        assert!(!ntp_time.is_immediate());
606        assert_eq!(ntp_time.value, 12345678);
607    }
608
609    #[test]
610    #[cfg(feature = "osc10")]
611    fn complex_nested_bundle_structure() {
612        // Create a complex nested structure
613        let mut root_bundle = IrBundle::immediate();
614        
615        // Add some messages
616        root_bundle.add_message(IrValue::from("root message 1"));
617        root_bundle.add_message(IrValue::from(100));
618        
619        // Create first nested bundle
620        let mut nested1 = IrBundle::new(IrTimetag::from_ntp(2000));
621        nested1.add_message(IrValue::from("nested1 message"));
622        
623        // Create second nested bundle with its own nested bundle
624        let mut nested2 = IrBundle::new(IrTimetag::from_ntp(3000));
625        nested2.add_message(IrValue::from("nested2 message"));
626        
627        let mut deeply_nested = IrBundle::new(IrTimetag::from_ntp(4000));
628    deeply_nested.add_message(IrValue::from("deeply nested message"));
629    deeply_nested.add_message(IrValue::from(core::f64::consts::PI));
630        
631        nested2.add_bundle(deeply_nested);
632        
633        // Add nested bundles to root
634        root_bundle.add_bundle(nested1);
635        root_bundle.add_bundle(nested2);
636        
637        // Verify structure
638        assert_eq!(root_bundle.len(), 4); // 2 messages + 2 bundles
639        assert!(root_bundle.elements[0].is_message());
640        assert!(root_bundle.elements[1].is_message());
641        assert!(root_bundle.elements[2].is_bundle());
642        assert!(root_bundle.elements[3].is_bundle());
643        
644        // Check the second nested bundle contains a bundle
645        let nested2_ref = root_bundle.elements[3].as_bundle().unwrap();
646        assert_eq!(nested2_ref.len(), 2); // 1 message + 1 bundle
647        assert!(nested2_ref.elements[1].is_bundle());
648        
649        // Check deeply nested bundle
650        let deeply_nested_ref = nested2_ref.elements[1].as_bundle().unwrap();
651        assert_eq!(deeply_nested_ref.len(), 2);
652        assert_eq!(deeply_nested_ref.timetag.value, 4000);
653    }
654
655    #[test]
656    #[cfg(feature = "osc11")]
657    fn osc_1_1_types() {
658        // Test Color type
659        let color = IrValue::color(255, 128, 64, 255);
660        assert_eq!(color.as_color(), Some((255, 128, 64, 255)));
661        
662        // Test MIDI type
663        let midi = IrValue::midi(0, 144, 60, 127); // Note on, middle C, velocity 127
664        assert_eq!(midi.as_midi(), Some((0, 144, 60, 127)));
665        
666        // Test that non-matching types return None
667        assert!(color.as_midi().is_none());
668        assert!(midi.as_color().is_none());
669        assert!(IrValue::from(42).as_color().is_none());
670        assert!(IrValue::from("test").as_midi().is_none());
671    }
672}