osc_adapter_osc_types/
lib.rs

1//! # osc-adapter-osc-types
2//!
3//! ⚠️ **EXPERIMENTAL** ⚠️  
4//! This crate is experimental and APIs may change significantly between versions.
5//!
6//! Bidirectional adapter between `osc-ir` intermediate representation and `rust-osc-types`
7//! for seamless conversion between OSC data formats.
8//!
9//! ## Features
10//!
11//! - **Bidirectional Conversion**: Convert between `IrValue` and OSC types from `rust-osc-types`
12//! - **OSC Version Support**: Support for both OSC 1.0 and OSC 1.1 via feature flags
13//! - **Message Conversion**: Convert OSC messages to/from IR representation
14//! - **Type Preservation**: Maintain type information during conversion
15//! - **no_std Compatible**: Works in no_std environments with `alloc`
16//!
17//! ## Usage
18//!
19//! This crate provides bidirectional conversion between `osc-ir` and `rust-osc-types`.
20//! The exact API depends on which OSC version features are enabled.
21//!
22//! ```rust
23//! use osc_ir::IrValue;
24//!
25//! // Create basic IR values that can be converted
26//! let frequency = IrValue::from(440.0);
27//! let waveform = IrValue::from("sine");
28//! let message_args = vec![frequency, waveform];
29//! 
30//! // These values can be used with the version-specific adapters
31//! assert_eq!(message_args.len(), 2);
32//! ```
33
34#![cfg_attr(not(test), no_std)]
35
36extern crate alloc;
37
38#[cfg(any(feature = "osc10", feature = "osc11"))]
39use alloc::{string::String, vec, vec::Vec};
40#[cfg(any(feature = "osc10", feature = "osc11"))]
41use osc_ir::IrValue;
42
43#[cfg(any(feature = "osc10", feature = "osc11"))]
44const MESSAGE_TYPE_TAG: &str = "osc.message";
45
46#[cfg(any(feature = "osc10", feature = "osc11"))]
47fn message_to_ir_map(address: &str, args: Vec<IrValue>) -> IrValue {
48    IrValue::Map(vec![
49        (String::from("$type"), IrValue::from(MESSAGE_TYPE_TAG)),
50        (String::from("address"), IrValue::from(address)),
51        (String::from("args"), IrValue::Array(args)),
52    ])
53}
54
55#[cfg(any(feature = "osc10", feature = "osc11"))]
56fn try_extract_message<'a>(value: &'a IrValue) -> Option<(&'a str, &'a [IrValue])> {
57    let map = value.as_map()?;
58    let mut address: Option<&'a str> = None;
59    let mut args: Option<&'a [IrValue]> = None;
60    let mut has_type_tag = false;
61
62    for (key, entry) in map.iter() {
63        match key.as_str() {
64            "$type" => {
65                let tag = entry.as_str()?;
66                if tag != MESSAGE_TYPE_TAG {
67                    return None;
68                }
69                has_type_tag = true;
70            }
71            "address" => {
72                address = entry.as_str();
73            }
74            "args" => {
75                args = entry.as_array();
76            }
77            _ => {}
78        }
79    }
80
81    if map.iter().any(|(k, _)| k == "$type") && !has_type_tag {
82        return None;
83    }
84
85    let address = address?;
86    let args = args.unwrap_or(&[]);
87    Some((address, args))
88}
89
90#[cfg(feature = "osc10")]
91pub mod v10 {
92    use super::*;
93    use osc_types10 as osc;
94
95    fn arg_to_ir(arg: &osc::OscType) -> IrValue {
96        match arg {
97            osc::OscType::Int(v) => IrValue::Integer(*v as i64),
98            osc::OscType::Float(v) => IrValue::Float(*v as f64),
99            osc::OscType::String(s) => IrValue::from(*s),
100            osc::OscType::Blob(bytes) => IrValue::Binary(bytes.to_vec()),
101        }
102    }
103
104    fn ir_to_arg(value: &IrValue) -> Option<osc::OscType<'_>> {
105        match value {
106            IrValue::Integer(i) => i32::try_from(*i).ok().map(osc::OscType::Int),
107            IrValue::Float(f) => Some(osc::OscType::Float(*f as f32)),
108            IrValue::String(s) => Some(osc::OscType::String(s.as_ref())),
109            IrValue::Binary(bytes) => Some(osc::OscType::Blob(bytes.as_slice())),
110            // OSC 1.1 types not yet supported in conversion
111            #[cfg(feature = "osc11")]
112            IrValue::Color { .. } | IrValue::Midi { .. } => None,
113            _ => None,
114        }
115    }
116
117    pub fn message_to_ir(message: &osc::Message) -> IrValue {
118        let args = message.args.iter().map(arg_to_ir).collect::<Vec<_>>();
119        message_to_ir_map(message.address, args)
120    }
121
122    pub fn ir_to_message(value: &IrValue) -> Option<osc::Message<'_>> {
123        let (address, args) = try_extract_message(value)?;
124        let mut osc_args = Vec::with_capacity(args.len());
125        for arg in args {
126            osc_args.push(ir_to_arg(arg)?);
127        }
128        Some(osc::Message {
129            address,
130            args: osc_args,
131        })
132    }
133}
134
135#[cfg(feature = "osc11")]
136pub mod v11 {
137    use super::*;
138    use osc_types11 as osc;
139
140    fn arg_to_ir(arg: &osc::OscType) -> IrValue {
141        match arg {
142            osc::OscType::Int(v) => IrValue::Integer(*v as i64),
143            osc::OscType::Float(v) => IrValue::Float(*v as f64),
144            osc::OscType::String(s) => IrValue::from(*s),
145            osc::OscType::Blob(bytes) => IrValue::Binary(bytes.to_vec()),
146        }
147    }
148
149    fn ir_to_arg(value: &IrValue) -> Option<osc::OscType<'_>> {
150        match value {
151            IrValue::Integer(i) => i32::try_from(*i).ok().map(osc::OscType::Int),
152            IrValue::Float(f) => Some(osc::OscType::Float(*f as f32)),
153            IrValue::String(s) => Some(osc::OscType::String(s.as_ref())),
154            IrValue::Binary(bytes) => Some(osc::OscType::Blob(bytes.as_slice())),
155            IrValue::Color { .. } | IrValue::Midi { .. } => None,
156            _ => None,
157        }
158    }
159
160    pub fn message_to_ir(message: &osc::Message) -> IrValue {
161        let args = message.args.iter().map(arg_to_ir).collect::<Vec<_>>();
162        message_to_ir_map(message.address, args)
163    }
164
165    pub fn ir_to_message(value: &IrValue) -> Option<osc::Message<'_>> {
166        let (address, args) = try_extract_message(value)?;
167        let mut osc_args = Vec::with_capacity(args.len());
168        for arg in args {
169            osc_args.push(ir_to_arg(arg)?);
170        }
171        Some(osc::Message {
172            address,
173            args: osc_args,
174        })
175    }
176}
177
178#[cfg(all(test, any(feature = "osc10", feature = "osc11")))]
179mod tests {
180    use super::*;
181
182    #[cfg(feature = "osc10")]
183    mod osc10 {
184        use super::*;
185        use alloc::borrow::ToOwned;
186
187        #[test]
188        fn message_to_ir_encodes_metadata_and_args() {
189            use osc_types10 as osc;
190
191            let message = osc::Message {
192                address: "/basic",
193                args: vec![
194                    osc::OscType::Int(42),
195                    osc::OscType::Float(0.5),
196                    osc::OscType::String("text"),
197                    osc::OscType::Blob(&[1, 2, 3]),
198                ],
199            };
200
201            let ir = v10::message_to_ir(&message);
202            let entries = match ir {
203                IrValue::Map(entries) => entries,
204                _ => panic!("expected map"),
205            };
206
207            assert_eq!(entries.len(), 3);
208
209            let ty = entries.iter().find(|(key, _)| key == "$type").unwrap();
210            assert_eq!(ty.1, IrValue::from(MESSAGE_TYPE_TAG));
211
212            let address = entries.iter().find(|(key, _)| key == "address").unwrap();
213            assert_eq!(address.1, IrValue::from("/basic"));
214
215            let args_entry = entries.iter().find(|(key, _)| key == "args").unwrap();
216            let args = match &args_entry.1 {
217                IrValue::Array(values) => values,
218                _ => panic!("expected args array"),
219            };
220
221            let expected = vec![
222                IrValue::Integer(42),
223                IrValue::Float(0.5),
224                IrValue::from("text"),
225                IrValue::Binary(vec![1, 2, 3]),
226            ];
227            assert_eq!(args, &expected);
228        }
229
230        #[test]
231        fn ir_roundtrips_to_message() {
232            use osc_types10 as osc;
233
234            let ir = IrValue::Map(vec![
235                ("$type".to_owned(), IrValue::from(MESSAGE_TYPE_TAG)),
236                ("address".to_owned(), IrValue::from("/roundtrip")),
237                (
238                    "args".to_owned(),
239                    IrValue::Array(vec![
240                        IrValue::Integer(7),
241                        IrValue::Float(-1.25),
242                        IrValue::from("value"),
243                        IrValue::Binary(vec![9, 8, 7]),
244                    ]),
245                ),
246            ]);
247
248            let message = v10::ir_to_message(&ir).expect("expected successful conversion");
249            assert_eq!(message.address, "/roundtrip");
250            assert!(matches!(message.args[0], osc::OscType::Int(7)));
251            assert!(
252                matches!(message.args[1], osc::OscType::Float(f) if (f + 1.25).abs() < f32::EPSILON)
253            );
254            assert!(matches!(message.args[2], osc::OscType::String("value")));
255            assert!(matches!(message.args[3], osc::OscType::Blob(slice) if slice == [9, 8, 7]));
256        }
257
258        #[test]
259        fn ir_to_message_rejects_unknown_arguments() {
260            let ir = IrValue::Map(vec![
261                ("address".to_owned(), IrValue::from("/invalid")),
262                ("args".to_owned(), IrValue::Array(vec![IrValue::Bool(true)])),
263            ]);
264
265            assert!(v10::ir_to_message(&ir).is_none());
266        }
267    }
268
269    #[test]
270    fn mismatched_type_tag_is_rejected() {
271        let value = IrValue::Map(vec![
272            ("$type".into(), IrValue::from("osc.bundle")),
273            ("address".into(), IrValue::from("/bad")),
274            ("args".into(), IrValue::Array(vec![])),
275        ]);
276
277        assert!(try_extract_message(&value).is_none());
278    }
279
280    #[test]
281    fn missing_type_tag_defaults_to_message() {
282        let value = IrValue::Map(vec![
283            ("address".into(), IrValue::from("/no-tag")),
284            ("args".into(), IrValue::Array(vec![])),
285        ]);
286
287        let extracted = try_extract_message(&value).expect("expected message extraction");
288        assert_eq!(extracted.0, "/no-tag");
289        assert!(extracted.1.is_empty());
290    }
291
292    #[cfg(feature = "osc11")]
293    mod osc11 {
294        use super::*;
295
296        #[test]
297        fn ir_to_message_rejects_color_and_midi() {
298            let ir = IrValue::Map(vec![
299                ("address".into(), IrValue::from("/unsupported")),
300                (
301                    "args".into(),
302                    IrValue::Array(vec![
303                        IrValue::Color {
304                            r: 0,
305                            g: 1,
306                            b: 2,
307                            a: 3,
308                        },
309                        IrValue::Midi {
310                            port: 1,
311                            status: 2,
312                            data1: 3,
313                            data2: 4,
314                        },
315                    ]),
316                ),
317            ]);
318
319            assert!(v11::ir_to_message(&ir).is_none());
320        }
321    }
322}