1#![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 #[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}