Skip to main content

monocoque_core/
message_builder.rs

1//! Ergonomic message builder for constructing `ZeroMQ` multipart messages.
2//!
3//! This module provides a fluent API for building multipart messages with
4//! automatic frame handling and type conversions.
5
6use bytes::Bytes;
7
8const MAX_PREALLOCATED_FRAMES: usize = 1024;
9
10/// Builder for constructing `ZeroMQ` multipart messages.
11///
12/// Provides a fluent API for adding frames to a message with automatic
13/// conversions from common types (strings, bytes, JSON, etc.).
14///
15/// # Examples
16///
17/// ```
18/// use monocoque_core::message_builder::Message;
19///
20/// // Simple text frames
21/// let msg = Message::new()
22///     .push_str("topic")
23///     .push_str("Hello, World!")
24///     .into_frames();
25///
26/// // Mixed types
27/// let msg = Message::new()
28///     .push(&b"routing_id"[..])
29///     .push_str("command:execute")
30///     .push(&[1u8, 2, 3, 4][..])
31///     .into_frames();
32/// ```
33///
34/// ## With Serde JSON (optional)
35///
36/// ```ignore
37/// #[derive(Serialize)]
38/// struct Task {
39///     id: u64,
40///     name: String,
41/// }
42///
43/// let task = Task { id: 42, name: "Process data".to_string() };
44/// let msg = Message::new()
45///     .push_str("tasks")
46///     .push_json(&task)?
47///     .into_frames();
48/// ```
49#[derive(Debug, Clone, Default)]
50pub struct Message {
51    frames: Vec<Bytes>,
52}
53
54impl Message {
55    /// Create a new empty message builder.
56    ///
57    /// # Examples
58    ///
59    /// ```
60    /// use monocoque_core::message_builder::Message;
61    ///
62    /// let msg = Message::new();
63    /// ```
64    #[must_use]
65    pub const fn new() -> Self {
66        Self { frames: Vec::new() }
67    }
68
69    /// Create a message with pre-allocated capacity.
70    ///
71    /// Useful when you know the number of frames in advance to avoid reallocations.
72    ///
73    /// # Examples
74    ///
75    /// ```
76    /// use monocoque_core::message_builder::Message;
77    ///
78    /// let msg = Message::with_capacity(4)
79    ///     .push_str("frame1")
80    ///     .push_str("frame2")
81    ///     .push_str("frame3")
82    ///     .push_str("frame4");
83    /// ```
84    #[must_use]
85    pub fn with_capacity(capacity: usize) -> Self {
86        Self {
87            frames: Vec::with_capacity(capacity.min(MAX_PREALLOCATED_FRAMES)),
88        }
89    }
90
91    /// Add a frame from any type that can be converted to `Bytes`.
92    ///
93    /// # Examples
94    ///
95    /// ```
96    /// use monocoque_core::message_builder::Message;
97    /// use bytes::Bytes;
98    ///
99    /// let msg = Message::new()
100    ///     .push(&b"raw bytes"[..])
101    ///     .push(vec![1, 2, 3])
102    ///     .push(Bytes::from_static(b"static"));
103    /// ```
104    pub fn push(mut self, frame: impl Into<Bytes>) -> Self {
105        self.frames.push(frame.into());
106        self
107    }
108
109    /// Add a string frame.
110    ///
111    /// Convenience method for adding UTF-8 strings.
112    ///
113    /// # Examples
114    ///
115    /// ```
116    /// use monocoque_core::message_builder::Message;
117    ///
118    /// let msg = Message::new()
119    ///     .push_str("Hello")
120    ///     .push_str("World");
121    /// ```
122    #[must_use]
123    pub fn push_str(mut self, s: &str) -> Self {
124        self.frames.push(Bytes::copy_from_slice(s.as_bytes()));
125        self
126    }
127
128    /// Add an empty frame.
129    ///
130    /// Empty frames are often used as delimiters in `ZeroMQ` envelope patterns.
131    ///
132    /// # Examples
133    ///
134    /// ```
135    /// use monocoque_core::message_builder::Message;
136    ///
137    /// // ROUTER envelope: [identity, empty, body]
138    /// let msg = Message::new()
139    ///     .push(&b"client-123"[..])
140    ///     .push_empty()
141    ///     .push_str("Hello");
142    /// ```
143    #[must_use]
144    pub fn push_empty(mut self) -> Self {
145        self.frames.push(Bytes::new());
146        self
147    }
148
149    /// Add a frame containing JSON-serialized data.
150    ///
151    /// Requires the `serde` feature to be enabled.
152    ///
153    /// # Errors
154    ///
155    /// Returns an error if serialization fails.
156    ///
157    /// # Examples
158    ///
159    /// ```ignore
160    /// use monocoque_core::message_builder::Message;
161    /// use serde::Serialize;
162    ///
163    /// #[derive(Serialize)]
164    /// struct Data {
165    ///     value: i32,
166    /// }
167    ///
168    /// let data = Data { value: 42 };
169    /// let msg = Message::new()
170    ///     .push_str("data")
171    ///     .push_json(&data)?
172    ///     .into_frames();
173    /// # Ok::<(), Box<dyn std::error::Error>>(())
174    /// ```
175    #[cfg(feature = "serde")]
176    pub fn push_json<T: serde::Serialize>(mut self, value: &T) -> Result<Self, serde_json::Error> {
177        let json = serde_json::to_vec(value)?;
178        self.frames.push(Bytes::from(json));
179        Ok(self)
180    }
181
182    /// Add a frame containing MessagePack-serialized data.
183    ///
184    /// Requires the `msgpack` feature to be enabled. MessagePack is more compact
185    /// than JSON and often preferred for binary protocols.
186    ///
187    /// # Errors
188    ///
189    /// Returns an error if serialization fails.
190    #[cfg(feature = "msgpack")]
191    pub fn push_msgpack<T: serde::Serialize>(
192        mut self,
193        value: &T,
194    ) -> Result<Self, rmp_serde::encode::Error> {
195        let msgpack = rmp_serde::to_vec(value)?;
196        self.frames.push(Bytes::from(msgpack));
197        Ok(self)
198    }
199
200    /// Add a frame containing a big-endian u32.
201    ///
202    /// Useful for protocol headers, message IDs, etc.
203    ///
204    /// # Examples
205    ///
206    /// ```
207    /// use monocoque_core::message_builder::Message;
208    ///
209    /// let msg = Message::new()
210    ///     .push_u32(12345) // Message ID
211    ///     .push_str("payload");
212    /// ```
213    #[must_use]
214    pub fn push_u32(mut self, value: u32) -> Self {
215        self.frames
216            .push(Bytes::copy_from_slice(&value.to_be_bytes()));
217        self
218    }
219
220    /// Add a frame containing a big-endian u64.
221    #[must_use]
222    pub fn push_u64(mut self, value: u64) -> Self {
223        self.frames
224            .push(Bytes::copy_from_slice(&value.to_be_bytes()));
225        self
226    }
227
228    /// Get the number of frames in the message.
229    ///
230    /// # Examples
231    ///
232    /// ```
233    /// use monocoque_core::message_builder::Message;
234    ///
235    /// let msg = Message::new()
236    ///     .push_str("frame1")
237    ///     .push_str("frame2");
238    ///
239    /// assert_eq!(msg.len(), 2);
240    /// ```
241    #[must_use]
242    pub fn len(&self) -> usize {
243        self.frames.len()
244    }
245
246    /// Check if the message is empty (has no frames).
247    #[must_use]
248    pub fn is_empty(&self) -> bool {
249        self.frames.is_empty()
250    }
251
252    /// Consume the builder and return the frames as a `Vec<Bytes>`.
253    ///
254    /// This is the final step to get the message ready for sending.
255    ///
256    /// # Examples
257    ///
258    /// ```
259    /// use monocoque_core::message_builder::Message;
260    ///
261    /// let frames = Message::new()
262    ///     .push_str("Hello")
263    ///     .push_str("World")
264    ///     .into_frames();
265    /// assert_eq!(frames.len(), 2);
266    /// ```
267    #[must_use]
268    pub fn into_frames(self) -> Vec<Bytes> {
269        self.frames
270    }
271
272    /// Get a reference to the frames without consuming the builder.
273    #[must_use]
274    pub fn frames(&self) -> &[Bytes] {
275        &self.frames
276    }
277
278    /// Create a message from existing frames.
279    ///
280    /// # Examples
281    ///
282    /// ```
283    /// use monocoque_core::message_builder::Message;
284    /// use bytes::Bytes;
285    ///
286    /// let frames = vec![
287    ///     Bytes::from_static(b"frame1"),
288    ///     Bytes::from_static(b"frame2"),
289    /// ];
290    ///
291    /// let msg = Message::from_frames(frames);
292    /// assert_eq!(msg.len(), 2);
293    /// ```
294    #[must_use]
295    pub const fn from_frames(frames: Vec<Bytes>) -> Self {
296        Self { frames }
297    }
298}
299
300impl From<Vec<Bytes>> for Message {
301    fn from(frames: Vec<Bytes>) -> Self {
302        Self::from_frames(frames)
303    }
304}
305
306impl From<Message> for Vec<Bytes> {
307    fn from(msg: Message) -> Self {
308        msg.into_frames()
309    }
310}
311
312#[cfg(test)]
313mod tests {
314    use super::*;
315
316    #[test]
317    fn test_empty_message() {
318        let msg = Message::new();
319        assert_eq!(msg.len(), 0);
320        assert!(msg.is_empty());
321    }
322
323    #[test]
324    fn test_build_message() {
325        let msg = Message::new()
326            .push_str("topic")
327            .push_str("Hello")
328            .push(Vec::from(&b"World"[..]));
329
330        assert_eq!(msg.len(), 3);
331        assert!(!msg.is_empty());
332
333        let frames = msg.into_frames();
334        assert_eq!(frames[0], Bytes::from_static(b"topic"));
335        assert_eq!(frames[1], Bytes::from_static(b"Hello"));
336        assert_eq!(frames[2], Bytes::from_static(b"World"));
337    }
338
339    #[test]
340    fn test_push_empty() {
341        let msg = Message::new()
342            .push(Vec::from(&b"id"[..]))
343            .push_empty()
344            .push_str("body");
345
346        let frames = msg.into_frames();
347        assert_eq!(frames.len(), 3);
348        assert_eq!(frames[1].len(), 0);
349    }
350
351    #[test]
352    fn test_push_integers() {
353        let msg = Message::new().push_u32(12345).push_u64(67890);
354
355        let frames = msg.into_frames();
356        assert_eq!(frames[0].len(), 4);
357        assert_eq!(frames[1].len(), 8);
358
359        let val32 = u32::from_be_bytes(frames[0].as_ref().try_into().unwrap());
360        assert_eq!(val32, 12345);
361
362        let val64 = u64::from_be_bytes(frames[1].as_ref().try_into().unwrap());
363        assert_eq!(val64, 67890);
364    }
365
366    #[test]
367    fn test_with_capacity() {
368        let msg = Message::with_capacity(10);
369        assert_eq!(msg.len(), 0);
370        assert!(msg.frames.capacity() >= 10);
371    }
372
373    #[test]
374    fn test_with_capacity_rejects_unbounded_capacity_without_panicking() {
375        let result = std::panic::catch_unwind(|| Message::with_capacity(usize::MAX));
376        assert!(result.is_ok());
377    }
378
379    #[test]
380    fn test_from_frames() {
381        let frames = vec![Bytes::from_static(b"a"), Bytes::from_static(b"b")];
382        let msg = Message::from_frames(frames.clone());
383        assert_eq!(msg.len(), 2);
384        assert_eq!(msg.frames(), &frames[..]);
385    }
386}