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::serialize(trade).map_err(|e| SerializationError {
195            message: e.to_string(),
196        })
197    }
198
199    fn serialize_book_change(
200        &self,
201        event: &PriceLevelChangedEvent,
202    ) -> Result<Vec<u8>, SerializationError> {
203        bincode::serialize(event).map_err(|e| SerializationError {
204            message: e.to_string(),
205        })
206    }
207
208    fn deserialize_trade(&self, data: &[u8]) -> Result<TradeResult, SerializationError> {
209        bincode::deserialize(data).map_err(|e| SerializationError {
210            message: e.to_string(),
211        })
212    }
213
214    fn deserialize_book_change(
215        &self,
216        data: &[u8],
217    ) -> Result<PriceLevelChangedEvent, SerializationError> {
218        bincode::deserialize(data).map_err(|e| SerializationError {
219            message: e.to_string(),
220        })
221    }
222
223    #[inline]
224    fn content_type(&self) -> &'static str {
225        "application/x-bincode"
226    }
227}
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232    use pricelevel::{Id, MatchResult, Side};
233
234    fn make_trade_result() -> TradeResult {
235        let order_id = Id::new_uuid();
236        let match_result = MatchResult::new(order_id, 100);
237        TradeResult::new("BTC/USD".to_string(), match_result)
238    }
239
240    fn make_book_change() -> PriceLevelChangedEvent {
241        PriceLevelChangedEvent {
242            side: Side::Buy,
243            price: 50_000_000,
244            quantity: 1_000,
245        }
246    }
247
248    // ─── JSON tests ─────────────────────────────────────────────────────
249
250    #[test]
251    fn test_json_serialize_trade() {
252        let serializer = JsonEventSerializer::new();
253        let trade = make_trade_result();
254        let result = serializer.serialize_trade(&trade);
255        assert!(result.is_ok());
256        let bytes = result.unwrap_or_default();
257        assert!(!bytes.is_empty());
258
259        let json_str = String::from_utf8(bytes).unwrap_or_default();
260        assert!(json_str.contains("BTC/USD"));
261    }
262
263    #[test]
264    fn test_json_roundtrip_trade() {
265        let serializer = JsonEventSerializer::new();
266        let trade = make_trade_result();
267        let bytes = serializer.serialize_trade(&trade);
268        assert!(bytes.is_ok());
269        let bytes = bytes.unwrap_or_default();
270
271        let decoded = serializer.deserialize_trade(&bytes);
272        assert!(decoded.is_ok());
273        let decoded = decoded.unwrap_or_else(|_| make_trade_result());
274        assert_eq!(decoded.symbol, trade.symbol);
275        assert_eq!(decoded.total_maker_fees, trade.total_maker_fees);
276        assert_eq!(decoded.total_taker_fees, trade.total_taker_fees);
277    }
278
279    #[test]
280    fn test_json_serialize_book_change() {
281        let serializer = JsonEventSerializer::new();
282        let event = make_book_change();
283        let result = serializer.serialize_book_change(&event);
284        assert!(result.is_ok());
285        let bytes = result.unwrap_or_default();
286        assert!(!bytes.is_empty());
287    }
288
289    #[test]
290    fn test_json_roundtrip_book_change() {
291        let serializer = JsonEventSerializer::new();
292        let event = make_book_change();
293        let bytes = serializer.serialize_book_change(&event);
294        assert!(bytes.is_ok());
295        let bytes = bytes.unwrap_or_default();
296
297        let decoded = serializer.deserialize_book_change(&bytes);
298        assert!(decoded.is_ok());
299        let decoded = decoded.unwrap_or_else(|_| make_book_change());
300        assert_eq!(decoded, event);
301    }
302
303    #[test]
304    fn test_json_content_type() {
305        let serializer = JsonEventSerializer::new();
306        assert_eq!(serializer.content_type(), "application/json");
307    }
308
309    #[test]
310    fn test_json_deserialize_trade_error() {
311        let serializer = JsonEventSerializer::new();
312        let result = serializer.deserialize_trade(b"not valid json");
313        assert!(result.is_err());
314    }
315
316    #[test]
317    fn test_json_deserialize_book_change_error() {
318        let serializer = JsonEventSerializer::new();
319        let result = serializer.deserialize_book_change(b"not valid json");
320        assert!(result.is_err());
321    }
322
323    #[test]
324    fn test_serialization_error_display() {
325        let err = SerializationError {
326            message: "test error".to_string(),
327        };
328        let display = format!("{err}");
329        assert!(display.contains("event serialization error"));
330        assert!(display.contains("test error"));
331    }
332
333    // ─── Bincode tests ──────────────────────────────────────────────────
334
335    #[cfg(feature = "bincode")]
336    mod bincode_tests {
337        use super::*;
338
339        #[test]
340        fn test_bincode_serialize_trade() {
341            let serializer = BincodeEventSerializer::new();
342            let trade = make_trade_result();
343            let result = serializer.serialize_trade(&trade);
344            assert!(result.is_ok());
345            let bytes = result.unwrap_or_default();
346            assert!(!bytes.is_empty());
347
348            // Bincode should be more compact than JSON
349            let json_serializer = JsonEventSerializer::new();
350            let json_bytes = json_serializer.serialize_trade(&trade).unwrap_or_default();
351            assert!(
352                bytes.len() < json_bytes.len(),
353                "bincode ({}) should be smaller than json ({})",
354                bytes.len(),
355                json_bytes.len()
356            );
357        }
358
359        #[test]
360        fn test_bincode_roundtrip_trade() {
361            let serializer = BincodeEventSerializer::new();
362            let trade = make_trade_result();
363            let bytes = serializer.serialize_trade(&trade);
364            assert!(bytes.is_ok());
365            let bytes = bytes.unwrap_or_default();
366
367            let decoded = serializer.deserialize_trade(&bytes);
368            assert!(decoded.is_ok());
369            let decoded = decoded.unwrap_or_else(|_| make_trade_result());
370            assert_eq!(decoded.symbol, trade.symbol);
371            assert_eq!(decoded.total_maker_fees, trade.total_maker_fees);
372            assert_eq!(decoded.total_taker_fees, trade.total_taker_fees);
373        }
374
375        #[test]
376        fn test_bincode_serialize_book_change() {
377            let serializer = BincodeEventSerializer::new();
378            let event = make_book_change();
379            let result = serializer.serialize_book_change(&event);
380            assert!(result.is_ok());
381            let bytes = result.unwrap_or_default();
382            assert!(!bytes.is_empty());
383        }
384
385        #[test]
386        fn test_bincode_roundtrip_book_change() {
387            let serializer = BincodeEventSerializer::new();
388            let event = make_book_change();
389            let bytes = serializer.serialize_book_change(&event);
390            assert!(bytes.is_ok());
391            let bytes = bytes.unwrap_or_default();
392
393            let decoded = serializer.deserialize_book_change(&bytes);
394            assert!(decoded.is_ok());
395            let decoded = decoded.unwrap_or_else(|_| make_book_change());
396            assert_eq!(decoded, event);
397        }
398
399        #[test]
400        fn test_bincode_content_type() {
401            let serializer = BincodeEventSerializer::new();
402            assert_eq!(serializer.content_type(), "application/x-bincode");
403        }
404
405        #[test]
406        fn test_bincode_deserialize_trade_error() {
407            let serializer = BincodeEventSerializer::new();
408            let result = serializer.deserialize_trade(b"\x00\x01");
409            assert!(result.is_err());
410        }
411
412        #[test]
413        fn test_bincode_deserialize_book_change_error() {
414            let serializer = BincodeEventSerializer::new();
415            let result = serializer.deserialize_book_change(b"\x00\x01");
416            assert!(result.is_err());
417        }
418
419        #[test]
420        fn test_bincode_smaller_than_json_book_change() {
421            let event = make_book_change();
422            let bincode_ser = BincodeEventSerializer::new();
423            let json_ser = JsonEventSerializer::new();
424
425            let bin_bytes = bincode_ser
426                .serialize_book_change(&event)
427                .unwrap_or_default();
428            let json_bytes = json_ser.serialize_book_change(&event).unwrap_or_default();
429
430            assert!(
431                bin_bytes.len() < json_bytes.len(),
432                "bincode ({}) should be smaller than json ({})",
433                bin_bytes.len(),
434                json_bytes.len()
435            );
436        }
437    }
438}