osc_types10/
lib.rs

1#![forbid(unsafe_code)]
2#![deny(missing_docs, unreachable_pub, rust_2018_idioms)]
3#![cfg_attr(docsrs, feature(doc_cfg))]
4#![cfg_attr(not(feature = "std"), no_std)]
5#![doc = r#"
6# osc-types10
7**⚠ Experimental / Not for production use**
8
9Defines message and bundle types for Open Sound Control 1.0.
10- Pre-release (`0.1.0-alpha`)
11- Breaking changes may occur frequently
12- `no_std` compatible (optional)
13
14## Example
15```rust
16use osc_types10::{Message, OscType};
17let msg = Message::new("/example", vec![OscType::String("abc")]);
18println!("{msg:?}");
19```
20"#]
21
22#[cfg(not(feature = "std"))]
23extern crate alloc;
24
25#[cfg(feature = "std")]
26use std::vec::Vec;
27
28#[cfg(not(feature = "std"))]
29use alloc::vec::Vec;
30
31/// OSC packet - either a message or a bundle
32#[derive(Debug, Clone, PartialEq)]
33pub enum OscPacket<'a> {
34    /// An OSC message
35    Message(Message<'a>),
36    /// An OSC bundle
37    Bundle(Bundle<'a>),
38}
39
40/// OSC argument types as defined in OSC 1.0 specification
41#[derive(Debug, Clone, PartialEq)]
42pub enum OscType<'a> {
43    /// 32-bit integer (i)
44    Int(i32),
45    /// 32-bit IEEE 754 float (f)
46    Float(f32),
47    /// Null-terminated string (s)
48    String(&'a str),
49    /// Binary blob (b)
50    Blob(&'a [u8]),
51}
52
53/// OSC Message as defined in OSC 1.0 specification
54#[derive(Debug, Clone, PartialEq)]
55pub struct Message<'a> {
56    /// OSC address pattern
57    pub address: &'a str,
58    /// Arguments of the message
59    pub args: Vec<OscType<'a>>,
60}
61
62impl<'a> Message<'a> {
63    /// Create a new OSC message
64    pub fn new(address: &'a str, args: Vec<OscType<'a>>) -> Self {
65        Self { address, args }
66    }
67
68    /// Create a new OSC message with string arguments (convenience method)
69    pub fn with_strings(address: &'a str, string_args: Vec<&'a str>) -> Self {
70        let args = string_args.into_iter().map(OscType::String).collect();
71        Self::new(address, args)
72    }
73}
74
75/// OSC Bundle as defined in OSC 1.0 specification
76///
77/// Bundles can contain both messages and nested bundles, allowing for hierarchical organization
78/// of OSC data with precise timing control.
79#[derive(Debug, Clone, PartialEq)]
80pub struct Bundle<'a> {
81    /// OSC time tag (64-bit NTP timestamp)
82    pub timetag: u64,
83    /// Packets contained in the bundle (messages and/or nested bundles)
84    pub packets: Vec<OscPacket<'a>>,
85}
86
87impl<'a> Bundle<'a> {
88    /// Create a new OSC bundle
89    pub fn new(timetag: u64, packets: Vec<OscPacket<'a>>) -> Self {
90        Self { timetag, packets }
91    }
92
93    /// Create a new OSC bundle with only messages (convenience method)
94    pub fn with_messages(timetag: u64, messages: Vec<Message<'a>>) -> Self {
95        let packets = messages.into_iter().map(OscPacket::Message).collect();
96        Self::new(timetag, packets)
97    }
98
99    /// Create a new empty bundle
100    pub fn empty(timetag: u64) -> Self {
101        Self::new(timetag, Vec::new())
102    }
103
104    /// Add a message to the bundle
105    pub fn add_message(&mut self, message: Message<'a>) {
106        self.packets.push(OscPacket::Message(message));
107    }
108
109    /// Add a nested bundle to the bundle
110    pub fn add_bundle(&mut self, bundle: Bundle<'a>) {
111        self.packets.push(OscPacket::Bundle(bundle));
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::{Bundle, Message, OscPacket, OscType};
118
119    #[cfg(not(feature = "std"))]
120    use alloc::vec;
121
122    #[test]
123    fn message_new_sets_address_and_args() {
124        let msg = Message::with_strings("/test", vec!["one", "two"]);
125
126        assert_eq!(msg.address, "/test");
127        assert_eq!(
128            msg.args,
129            vec![OscType::String("one"), OscType::String("two")]
130        );
131    }
132
133    #[test]
134    fn message_equality_compares_contents() {
135        let lhs = Message::with_strings("/foo", vec!["a", "b"]);
136        let rhs = Message::with_strings("/foo", vec!["a", "b"]);
137        let different_address = Message::with_strings("/bar", vec!["a", "b"]);
138        let different_args = Message::with_strings("/foo", vec!["a"]);
139
140        assert_eq!(lhs, rhs);
141        assert_ne!(lhs, different_address);
142        assert_ne!(lhs, different_args);
143    }
144
145    #[test]
146    fn message_supports_mixed_types() {
147        let msg = Message::new(
148            "/mixed",
149            vec![
150                OscType::Int(42),
151                OscType::Float(3.14),
152                OscType::String("hello"),
153                OscType::Blob(&[0x01, 0x02, 0x03]),
154            ],
155        );
156
157        assert_eq!(msg.address, "/mixed");
158        assert_eq!(msg.args.len(), 4);
159    }
160
161    #[test]
162    fn bundle_new_sets_timetag_and_packets() {
163        let messages = vec![
164            Message::with_strings("/bundle/one", vec!["1"]),
165            Message::with_strings("/bundle/two", vec!["2"]),
166        ];
167        let bundle = Bundle::with_messages(42, messages.clone());
168
169        assert_eq!(bundle.timetag, 42);
170        // Check that packets contain the expected messages
171        assert_eq!(bundle.packets.len(), 2);
172        if let OscPacket::Message(ref msg) = bundle.packets[0] {
173            assert_eq!(msg, &messages[0]);
174        } else {
175            panic!("Expected message in bundle");
176        }
177    }
178
179    #[test]
180    fn bundle_equality_compares_contents() {
181        let messages = vec![
182            Message::with_strings("/bundle", vec!["a"]),
183            Message::with_strings("/bundle", vec!["b"]),
184        ];
185        let lhs = Bundle::with_messages(1, messages.clone());
186        let rhs = Bundle::with_messages(1, messages);
187        let different_timetag =
188            Bundle::with_messages(2, vec![Message::with_strings("/bundle", vec!["a"])]);
189        let different_messages =
190            Bundle::with_messages(1, vec![Message::with_strings("/bundle", vec!["c"])]);
191
192        assert_eq!(lhs, rhs);
193        assert_ne!(lhs, different_timetag);
194        assert_ne!(lhs, different_messages);
195    }
196
197    #[test]
198    fn bundle_supports_nested_bundles() {
199        let inner_bundle = Bundle::with_messages(
200            100,
201            vec![Message::with_strings("/inner/msg", vec!["inner"])],
202        );
203        let mut outer_bundle = Bundle::empty(200);
204        outer_bundle.add_message(Message::with_strings("/outer/msg", vec!["outer"]));
205        outer_bundle.add_bundle(inner_bundle.clone());
206
207        assert_eq!(outer_bundle.timetag, 200);
208        assert_eq!(outer_bundle.packets.len(), 2);
209
210        // Check the outer message
211        if let OscPacket::Message(ref msg) = outer_bundle.packets[0] {
212            assert_eq!(msg.address, "/outer/msg");
213        } else {
214            panic!("Expected message at index 0");
215        }
216
217        // Check the nested bundle
218        if let OscPacket::Bundle(ref bundle) = outer_bundle.packets[1] {
219            assert_eq!(bundle, &inner_bundle);
220        } else {
221            panic!("Expected bundle at index 1");
222        }
223    }
224
225    #[test]
226    fn bundle_add_methods_work_correctly() {
227        let mut bundle = Bundle::empty(42);
228
229        let msg1 = Message::with_strings("/test1", vec!["a"]);
230        let msg2 = Message::with_strings("/test2", vec!["b"]);
231        let nested_bundle = Bundle::with_messages(100, vec![msg2.clone()]);
232
233        bundle.add_message(msg1.clone());
234        bundle.add_bundle(nested_bundle.clone());
235
236        assert_eq!(bundle.packets.len(), 2);
237
238        // Verify message was added correctly
239        if let OscPacket::Message(ref msg) = bundle.packets[0] {
240            assert_eq!(msg, &msg1);
241        } else {
242            panic!("Expected message at index 0");
243        }
244
245        // Verify bundle was added correctly
246        if let OscPacket::Bundle(ref b) = bundle.packets[1] {
247            assert_eq!(b, &nested_bundle);
248        } else {
249            panic!("Expected bundle at index 1");
250        }
251    }
252
253    #[test]
254    fn bundle_with_messages_convenience_method() {
255        let messages = vec![
256            Message::with_strings("/msg1", vec!["hello"]),
257            Message::with_strings("/msg2", vec!["world"]),
258        ];
259
260        let bundle = Bundle::with_messages(42, messages.clone());
261
262        assert_eq!(bundle.timetag, 42);
263        assert_eq!(bundle.packets.len(), 2);
264
265        for (i, expected_msg) in messages.iter().enumerate() {
266            if let OscPacket::Message(ref msg) = bundle.packets[i] {
267                assert_eq!(msg, expected_msg);
268            } else {
269                panic!("Expected message at index {}", i);
270            }
271        }
272    }
273}