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}