Skip to main content

orderbook_rs/orderbook/
serialization.rs

1//! Pluggable event serialization for NATS publishers and consumers.
2//!
3//! This module provides the [`EventSerializer`] trait and two built-in
4//! implementations:
5//!
6//! - [`JsonEventSerializer`] — human-readable JSON (always available)
7//! - `BincodeEventSerializer` — compact binary format (requires the
8//!   `bincode` feature)
9//!
10//! Publishers such as `NatsTradePublisher` (requires the `nats` feature)
11//! accept any `Arc<dyn EventSerializer>` so the serialization format can be
12//! chosen at construction time without changing downstream code.
13//!
14//! # Feature Gate
15//!
16//! The `BincodeEventSerializer` requires the `bincode` feature:
17//!
18//! ```toml
19//! [dependencies]
20//! orderbook-rs = { version = "0.6", features = ["bincode"] }
21//! ```
22
23use crate::orderbook::book_change_event::PriceLevelChangedEvent;
24use crate::orderbook::trade::TradeResult;
25
26/// Errors that can occur during event serialization or deserialization.
27#[derive(Debug)]
28pub struct SerializationError {
29    /// Human-readable description of the failure.
30    pub message: String,
31}
32
33impl std::fmt::Display for SerializationError {
34    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35        write!(f, "event serialization error: {}", self.message)
36    }
37}
38
39impl std::error::Error for SerializationError {}
40
41/// A pluggable serializer for order book events.
42///
43/// Implementations convert [`TradeResult`] and [`PriceLevelChangedEvent`]
44/// to and from byte buffers. The format (JSON, Bincode, etc.) is an
45/// implementation detail, allowing publishers and consumers to negotiate
46/// the most efficient wire format.
47///
48/// # Thread Safety
49///
50/// Implementations must be `Send + Sync` so they can be shared across
51/// async task boundaries via `Arc<dyn EventSerializer>`.
52pub trait EventSerializer: Send + Sync + std::fmt::Debug {
53    /// Serialize a [`TradeResult`] into a byte buffer.
54    ///
55    /// # Errors
56    ///
57    /// Returns [`SerializationError`] if the event cannot be serialized.
58    fn serialize_trade(&self, trade: &TradeResult) -> Result<Vec<u8>, SerializationError>;
59
60    /// Serialize a [`PriceLevelChangedEvent`] into a byte buffer.
61    ///
62    /// # Errors
63    ///
64    /// Returns [`SerializationError`] if the event cannot be serialized.
65    fn serialize_book_change(
66        &self,
67        event: &PriceLevelChangedEvent,
68    ) -> Result<Vec<u8>, SerializationError>;
69
70    /// Deserialize a [`TradeResult`] from a byte buffer.
71    ///
72    /// # Errors
73    ///
74    /// Returns [`SerializationError`] if the bytes are malformed or
75    /// incompatible with the expected format.
76    fn deserialize_trade(&self, data: &[u8]) -> Result<TradeResult, SerializationError>;
77
78    /// Deserialize a [`PriceLevelChangedEvent`] from a byte buffer.
79    ///
80    /// # Errors
81    ///
82    /// Returns [`SerializationError`] if the bytes are malformed or
83    /// incompatible with the expected format.
84    fn deserialize_book_change(
85        &self,
86        data: &[u8],
87    ) -> Result<PriceLevelChangedEvent, SerializationError>;
88
89    /// Returns the MIME-like content type identifier for this format.
90    ///
91    /// Consumers can use this value to select the correct deserializer.
92    /// Examples: `"application/json"`, `"application/x-bincode"`.
93    #[must_use]
94    fn content_type(&self) -> &'static str;
95}
96
97// ─── JSON ───────────────────────────────────────────────────────────────────
98
99/// JSON event serializer using `serde_json`.
100///
101/// This is the default serializer, producing human-readable JSON payloads.
102/// It is always available (no feature gate) since `serde_json` is a
103/// required dependency.
104///
105/// # Content Type
106///
107/// `"application/json"`
108#[derive(Debug, Clone, Copy, Default)]
109pub struct JsonEventSerializer;
110
111impl JsonEventSerializer {
112    /// Create a new JSON event serializer.
113    #[must_use]
114    #[inline]
115    pub fn new() -> Self {
116        Self
117    }
118}
119
120impl EventSerializer for JsonEventSerializer {
121    fn serialize_trade(&self, trade: &TradeResult) -> Result<Vec<u8>, SerializationError> {
122        serde_json::to_vec(trade).map_err(|e| SerializationError {
123            message: e.to_string(),
124        })
125    }
126
127    fn serialize_book_change(
128        &self,
129        event: &PriceLevelChangedEvent,
130    ) -> Result<Vec<u8>, SerializationError> {
131        serde_json::to_vec(event).map_err(|e| SerializationError {
132            message: e.to_string(),
133        })
134    }
135
136    fn deserialize_trade(&self, data: &[u8]) -> Result<TradeResult, SerializationError> {
137        serde_json::from_slice(data).map_err(|e| SerializationError {
138            message: e.to_string(),
139        })
140    }
141
142    fn deserialize_book_change(
143        &self,
144        data: &[u8],
145    ) -> Result<PriceLevelChangedEvent, SerializationError> {
146        serde_json::from_slice(data).map_err(|e| SerializationError {
147            message: e.to_string(),
148        })
149    }
150
151    #[inline]
152    fn content_type(&self) -> &'static str {
153        "application/json"
154    }
155}
156
157// ─── Bincode ────────────────────────────────────────────────────────────────
158
159/// Bincode event serializer for compact binary payloads.
160///
161/// Produces significantly smaller payloads than JSON with much lower
162/// serialization latency (typically < 500 ns per event). The trade-off
163/// is that the output is not human-readable.
164///
165/// # Feature Gate
166///
167/// Requires the `bincode` feature:
168///
169/// ```toml
170/// [dependencies]
171/// orderbook-rs = { version = "0.6", features = ["bincode"] }
172/// ```
173///
174/// # Content Type
175///
176/// `"application/x-bincode"`
177#[cfg(feature = "bincode")]
178#[derive(Debug, Clone, Copy, Default)]
179pub struct BincodeEventSerializer;
180
181#[cfg(feature = "bincode")]
182impl BincodeEventSerializer {
183    /// Create a new Bincode event serializer.
184    #[must_use]
185    #[inline]
186    pub fn new() -> Self {
187        Self
188    }
189}
190
191#[cfg(feature = "bincode")]
192impl EventSerializer for BincodeEventSerializer {
193    fn serialize_trade(&self, trade: &TradeResult) -> Result<Vec<u8>, SerializationError> {
194        bincode::serde::encode_to_vec(trade, bincode::config::standard()).map_err(|e| {
195            SerializationError {
196                message: e.to_string(),
197            }
198        })
199    }
200
201    fn serialize_book_change(
202        &self,
203        event: &PriceLevelChangedEvent,
204    ) -> Result<Vec<u8>, SerializationError> {
205        bincode::serde::encode_to_vec(event, bincode::config::standard()).map_err(|e| {
206            SerializationError {
207                message: e.to_string(),
208            }
209        })
210    }
211
212    fn deserialize_trade(&self, data: &[u8]) -> Result<TradeResult, SerializationError> {
213        let (value, bytes_read) =
214            bincode::serde::decode_from_slice::<TradeResult, _>(data, bincode::config::standard())
215                .map_err(|e| SerializationError {
216                    message: e.to_string(),
217                })?;
218        if bytes_read != data.len() {
219            return Err(SerializationError {
220                message: format!(
221                    "trailing bytes after trade payload: consumed {bytes_read} of {}",
222                    data.len()
223                ),
224            });
225        }
226        Ok(value)
227    }
228
229    fn deserialize_book_change(
230        &self,
231        data: &[u8],
232    ) -> Result<PriceLevelChangedEvent, SerializationError> {
233        let (value, bytes_read) = bincode::serde::decode_from_slice::<PriceLevelChangedEvent, _>(
234            data,
235            bincode::config::standard(),
236        )
237        .map_err(|e| SerializationError {
238            message: e.to_string(),
239        })?;
240        if bytes_read != data.len() {
241            return Err(SerializationError {
242                message: format!(
243                    "trailing bytes after book-change payload: consumed {bytes_read} of {}",
244                    data.len()
245                ),
246            });
247        }
248        Ok(value)
249    }
250
251    #[inline]
252    fn content_type(&self) -> &'static str {
253        "application/x-bincode"
254    }
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260    use pricelevel::{Id, MatchResult, Side};
261
262    fn make_trade_result() -> TradeResult {
263        let order_id = Id::new_uuid();
264        let match_result = MatchResult::new(order_id, 100);
265        TradeResult::new("BTC/USD".to_string(), match_result)
266    }
267
268    fn make_book_change() -> PriceLevelChangedEvent {
269        PriceLevelChangedEvent {
270            side: Side::Buy,
271            price: 50_000_000,
272            quantity: 1_000,
273            engine_seq: 0,
274        }
275    }
276
277    // ─── JSON tests ─────────────────────────────────────────────────────
278
279    #[test]
280    fn test_json_serialize_trade() {
281        let serializer = JsonEventSerializer::new();
282        let trade = make_trade_result();
283        let result = serializer.serialize_trade(&trade);
284        assert!(result.is_ok());
285        let bytes = result.unwrap_or_default();
286        assert!(!bytes.is_empty());
287
288        let json_str = String::from_utf8(bytes).unwrap_or_default();
289        assert!(json_str.contains("BTC/USD"));
290    }
291
292    #[test]
293    fn test_json_roundtrip_trade() {
294        let serializer = JsonEventSerializer::new();
295        let trade = make_trade_result();
296        let bytes = serializer.serialize_trade(&trade);
297        assert!(bytes.is_ok());
298        let bytes = bytes.unwrap_or_default();
299
300        let decoded = serializer.deserialize_trade(&bytes);
301        assert!(decoded.is_ok());
302        let decoded = decoded.unwrap_or_else(|_| make_trade_result());
303        assert_eq!(decoded.symbol, trade.symbol);
304        assert_eq!(decoded.total_maker_fees, trade.total_maker_fees);
305        assert_eq!(decoded.total_taker_fees, trade.total_taker_fees);
306    }
307
308    #[test]
309    fn test_json_serialize_book_change() {
310        let serializer = JsonEventSerializer::new();
311        let event = make_book_change();
312        let result = serializer.serialize_book_change(&event);
313        assert!(result.is_ok());
314        let bytes = result.unwrap_or_default();
315        assert!(!bytes.is_empty());
316    }
317
318    #[test]
319    fn test_json_roundtrip_book_change() {
320        let serializer = JsonEventSerializer::new();
321        let event = make_book_change();
322        let bytes = serializer.serialize_book_change(&event);
323        assert!(bytes.is_ok());
324        let bytes = bytes.unwrap_or_default();
325
326        let decoded = serializer.deserialize_book_change(&bytes);
327        assert!(decoded.is_ok());
328        let decoded = decoded.unwrap_or_else(|_| make_book_change());
329        assert_eq!(decoded, event);
330    }
331
332    #[test]
333    fn test_json_content_type() {
334        let serializer = JsonEventSerializer::new();
335        assert_eq!(serializer.content_type(), "application/json");
336    }
337
338    #[test]
339    fn test_json_deserialize_trade_error() {
340        let serializer = JsonEventSerializer::new();
341        let result = serializer.deserialize_trade(b"not valid json");
342        assert!(result.is_err());
343    }
344
345    #[test]
346    fn test_json_deserialize_book_change_error() {
347        let serializer = JsonEventSerializer::new();
348        let result = serializer.deserialize_book_change(b"not valid json");
349        assert!(result.is_err());
350    }
351
352    #[test]
353    fn test_serialization_error_display() {
354        let err = SerializationError {
355            message: "test error".to_string(),
356        };
357        let display = format!("{err}");
358        assert!(display.contains("event serialization error"));
359        assert!(display.contains("test error"));
360    }
361
362    // ─── Bincode tests ──────────────────────────────────────────────────
363
364    #[cfg(feature = "bincode")]
365    mod bincode_tests {
366        use super::*;
367
368        #[test]
369        fn test_bincode_serialize_trade() {
370            let serializer = BincodeEventSerializer::new();
371            let trade = make_trade_result();
372            let result = serializer.serialize_trade(&trade);
373            assert!(result.is_ok());
374            let bytes = result.unwrap_or_default();
375            assert!(!bytes.is_empty());
376
377            // Bincode should be more compact than JSON
378            let json_serializer = JsonEventSerializer::new();
379            let json_bytes = json_serializer.serialize_trade(&trade).unwrap_or_default();
380            assert!(
381                bytes.len() < json_bytes.len(),
382                "bincode ({}) should be smaller than json ({})",
383                bytes.len(),
384                json_bytes.len()
385            );
386        }
387
388        #[test]
389        fn test_bincode_roundtrip_trade() {
390            let serializer = BincodeEventSerializer::new();
391            let trade = make_trade_result();
392            let bytes = serializer.serialize_trade(&trade);
393            assert!(bytes.is_ok());
394            let bytes = bytes.unwrap_or_default();
395
396            let decoded = serializer.deserialize_trade(&bytes);
397            assert!(decoded.is_ok());
398            let decoded = decoded.unwrap_or_else(|_| make_trade_result());
399            assert_eq!(decoded.symbol, trade.symbol);
400            assert_eq!(decoded.total_maker_fees, trade.total_maker_fees);
401            assert_eq!(decoded.total_taker_fees, trade.total_taker_fees);
402        }
403
404        #[test]
405        fn test_bincode_serialize_book_change() {
406            let serializer = BincodeEventSerializer::new();
407            let event = make_book_change();
408            let result = serializer.serialize_book_change(&event);
409            assert!(result.is_ok());
410            let bytes = result.unwrap_or_default();
411            assert!(!bytes.is_empty());
412        }
413
414        #[test]
415        fn test_bincode_roundtrip_book_change() {
416            let serializer = BincodeEventSerializer::new();
417            let event = make_book_change();
418            let bytes = serializer.serialize_book_change(&event);
419            assert!(bytes.is_ok());
420            let bytes = bytes.unwrap_or_default();
421
422            let decoded = serializer.deserialize_book_change(&bytes);
423            assert!(decoded.is_ok());
424            let decoded = decoded.unwrap_or_else(|_| make_book_change());
425            assert_eq!(decoded, event);
426        }
427
428        #[test]
429        fn test_bincode_content_type() {
430            let serializer = BincodeEventSerializer::new();
431            assert_eq!(serializer.content_type(), "application/x-bincode");
432        }
433
434        #[test]
435        fn test_bincode_deserialize_trade_error() {
436            let serializer = BincodeEventSerializer::new();
437            let result = serializer.deserialize_trade(b"\x00\x01");
438            assert!(result.is_err());
439        }
440
441        #[test]
442        fn test_bincode_deserialize_book_change_error() {
443            let serializer = BincodeEventSerializer::new();
444            let result = serializer.deserialize_book_change(b"\x00\x01");
445            assert!(result.is_err());
446        }
447
448        #[test]
449        fn test_bincode_smaller_than_json_book_change() {
450            let event = make_book_change();
451            let bincode_ser = BincodeEventSerializer::new();
452            let json_ser = JsonEventSerializer::new();
453
454            let bin_bytes = bincode_ser
455                .serialize_book_change(&event)
456                .unwrap_or_default();
457            let json_bytes = json_ser.serialize_book_change(&event).unwrap_or_default();
458
459            assert!(
460                bin_bytes.len() < json_bytes.len(),
461                "bincode ({}) should be smaller than json ({})",
462                bin_bytes.len(),
463                json_bytes.len()
464            );
465        }
466    }
467}