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///
28/// A typed enum that preserves the underlying serde / bincode failure rather
29/// than flattening it to a string. It bridges into
30/// [`OrderBookError`](crate::orderbook::OrderBookError) via `From`, so an
31/// [`EventSerializer`] failure can be `?`-propagated on paths that return
32/// `OrderBookError`.
33#[derive(Debug, thiserror::Error)]
34pub enum SerializationError {
35    /// JSON (`serde_json`) serialization or deserialization failed.
36    #[error("JSON serialization error: {0}")]
37    Json(#[from] serde_json::Error),
38
39    /// Binary (`bincode`) serialization or deserialization failed.
40    #[error("bincode serialization error: {0}")]
41    Bincode(String),
42
43    /// The decoded payload had unexpected trailing bytes (corruption or a
44    /// format mismatch).
45    #[error("{0}")]
46    TrailingBytes(String),
47}
48
49/// A pluggable serializer for order book events.
50///
51/// Implementations convert [`TradeResult`] and [`PriceLevelChangedEvent`]
52/// to and from byte buffers. The format (JSON, Bincode, etc.) is an
53/// implementation detail, allowing publishers and consumers to negotiate
54/// the most efficient wire format.
55///
56/// # Thread Safety
57///
58/// Implementations must be `Send + Sync` so they can be shared across
59/// async task boundaries via `Arc<dyn EventSerializer>`.
60pub trait EventSerializer: Send + Sync + std::fmt::Debug {
61    /// Serialize a [`TradeResult`] into a byte buffer.
62    ///
63    /// # Errors
64    ///
65    /// Returns [`SerializationError`] if the event cannot be serialized.
66    fn serialize_trade(&self, trade: &TradeResult) -> Result<Vec<u8>, SerializationError>;
67
68    /// Serialize a [`PriceLevelChangedEvent`] into a byte buffer.
69    ///
70    /// # Errors
71    ///
72    /// Returns [`SerializationError`] if the event cannot be serialized.
73    fn serialize_book_change(
74        &self,
75        event: &PriceLevelChangedEvent,
76    ) -> Result<Vec<u8>, SerializationError>;
77
78    /// Deserialize a [`TradeResult`] 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_trade(&self, data: &[u8]) -> Result<TradeResult, SerializationError>;
85
86    /// Deserialize a [`PriceLevelChangedEvent`] from a byte buffer.
87    ///
88    /// # Errors
89    ///
90    /// Returns [`SerializationError`] if the bytes are malformed or
91    /// incompatible with the expected format.
92    fn deserialize_book_change(
93        &self,
94        data: &[u8],
95    ) -> Result<PriceLevelChangedEvent, SerializationError>;
96
97    /// Returns the MIME-like content type identifier for this format.
98    ///
99    /// Consumers can use this value to select the correct deserializer.
100    /// Examples: `"application/json"`, `"application/x-bincode"`.
101    #[must_use]
102    fn content_type(&self) -> &'static str;
103}
104
105// ─── JSON ───────────────────────────────────────────────────────────────────
106
107/// JSON event serializer using `serde_json`.
108///
109/// This is the default serializer, producing human-readable JSON payloads.
110/// It is always available (no feature gate) since `serde_json` is a
111/// required dependency.
112///
113/// # Content Type
114///
115/// `"application/json"`
116#[derive(Debug, Clone, Copy, Default)]
117pub struct JsonEventSerializer;
118
119impl JsonEventSerializer {
120    /// Create a new JSON event serializer.
121    #[must_use]
122    #[inline]
123    pub fn new() -> Self {
124        Self
125    }
126}
127
128impl EventSerializer for JsonEventSerializer {
129    fn serialize_trade(&self, trade: &TradeResult) -> Result<Vec<u8>, SerializationError> {
130        serde_json::to_vec(trade).map_err(SerializationError::Json)
131    }
132
133    fn serialize_book_change(
134        &self,
135        event: &PriceLevelChangedEvent,
136    ) -> Result<Vec<u8>, SerializationError> {
137        serde_json::to_vec(event).map_err(SerializationError::Json)
138    }
139
140    fn deserialize_trade(&self, data: &[u8]) -> Result<TradeResult, SerializationError> {
141        serde_json::from_slice(data).map_err(SerializationError::Json)
142    }
143
144    fn deserialize_book_change(
145        &self,
146        data: &[u8],
147    ) -> Result<PriceLevelChangedEvent, SerializationError> {
148        serde_json::from_slice(data).map_err(SerializationError::Json)
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())
195            .map_err(|e| SerializationError::Bincode(e.to_string()))
196    }
197
198    fn serialize_book_change(
199        &self,
200        event: &PriceLevelChangedEvent,
201    ) -> Result<Vec<u8>, SerializationError> {
202        bincode::serde::encode_to_vec(event, bincode::config::standard())
203            .map_err(|e| SerializationError::Bincode(e.to_string()))
204    }
205
206    fn deserialize_trade(&self, data: &[u8]) -> Result<TradeResult, SerializationError> {
207        let (value, bytes_read) =
208            bincode::serde::decode_from_slice::<TradeResult, _>(data, bincode::config::standard())
209                .map_err(|e| SerializationError::Bincode(e.to_string()))?;
210        if bytes_read != data.len() {
211            return Err(SerializationError::TrailingBytes(format!(
212                "trailing bytes after trade payload: consumed {bytes_read} of {}",
213                data.len()
214            )));
215        }
216        Ok(value)
217    }
218
219    fn deserialize_book_change(
220        &self,
221        data: &[u8],
222    ) -> Result<PriceLevelChangedEvent, SerializationError> {
223        let (value, bytes_read) = bincode::serde::decode_from_slice::<PriceLevelChangedEvent, _>(
224            data,
225            bincode::config::standard(),
226        )
227        .map_err(|e| SerializationError::Bincode(e.to_string()))?;
228        if bytes_read != data.len() {
229            return Err(SerializationError::TrailingBytes(format!(
230                "trailing bytes after book-change payload: consumed {bytes_read} of {}",
231                data.len()
232            )));
233        }
234        Ok(value)
235    }
236
237    #[inline]
238    fn content_type(&self) -> &'static str {
239        "application/x-bincode"
240    }
241}
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246    use pricelevel::{Id, MatchResult, Quantity, Side};
247
248    fn make_trade_result() -> TradeResult {
249        let order_id = Id::new_uuid();
250        let match_result = MatchResult::new(order_id, Quantity::new(100));
251        TradeResult::new("BTC/USD".to_string(), match_result)
252    }
253
254    fn make_book_change() -> PriceLevelChangedEvent {
255        PriceLevelChangedEvent {
256            side: Side::Buy,
257            price: 50_000_000,
258            quantity: 1_000,
259            engine_seq: 0,
260        }
261    }
262
263    // ─── JSON tests ─────────────────────────────────────────────────────
264
265    #[test]
266    fn test_json_serialize_trade() {
267        let serializer = JsonEventSerializer::new();
268        let trade = make_trade_result();
269        let result = serializer.serialize_trade(&trade);
270        assert!(result.is_ok());
271        let bytes = result.unwrap_or_default();
272        assert!(!bytes.is_empty());
273
274        let json_str = String::from_utf8(bytes).unwrap_or_default();
275        assert!(json_str.contains("BTC/USD"));
276    }
277
278    #[test]
279    fn test_json_roundtrip_trade() {
280        let serializer = JsonEventSerializer::new();
281        let trade = make_trade_result();
282        let bytes = serializer.serialize_trade(&trade);
283        assert!(bytes.is_ok());
284        let bytes = bytes.unwrap_or_default();
285
286        let decoded = serializer.deserialize_trade(&bytes);
287        assert!(decoded.is_ok());
288        let decoded = decoded.unwrap_or_else(|_| make_trade_result());
289        assert_eq!(decoded.symbol, trade.symbol);
290        assert_eq!(decoded.total_maker_fees, trade.total_maker_fees);
291        assert_eq!(decoded.total_taker_fees, trade.total_taker_fees);
292    }
293
294    #[test]
295    fn test_json_serialize_book_change() {
296        let serializer = JsonEventSerializer::new();
297        let event = make_book_change();
298        let result = serializer.serialize_book_change(&event);
299        assert!(result.is_ok());
300        let bytes = result.unwrap_or_default();
301        assert!(!bytes.is_empty());
302    }
303
304    #[test]
305    fn test_json_roundtrip_book_change() {
306        let serializer = JsonEventSerializer::new();
307        let event = make_book_change();
308        let bytes = serializer.serialize_book_change(&event);
309        assert!(bytes.is_ok());
310        let bytes = bytes.unwrap_or_default();
311
312        let decoded = serializer.deserialize_book_change(&bytes);
313        assert!(decoded.is_ok());
314        let decoded = decoded.unwrap_or_else(|_| make_book_change());
315        assert_eq!(decoded, event);
316    }
317
318    #[test]
319    fn test_json_content_type() {
320        let serializer = JsonEventSerializer::new();
321        assert_eq!(serializer.content_type(), "application/json");
322    }
323
324    #[test]
325    fn test_json_deserialize_trade_error() {
326        let serializer = JsonEventSerializer::new();
327        let result = serializer.deserialize_trade(b"not valid json");
328        assert!(result.is_err());
329    }
330
331    #[test]
332    fn test_json_deserialize_book_change_error() {
333        let serializer = JsonEventSerializer::new();
334        let result = serializer.deserialize_book_change(b"not valid json");
335        assert!(result.is_err());
336    }
337
338    #[test]
339    fn test_serialization_error_display_preserves_underlying() {
340        // The Json variant preserves the typed serde error.
341        let serde_err = serde_json::from_str::<i32>("not a number").unwrap_err();
342        let err = SerializationError::Json(serde_err);
343        let display = format!("{err}");
344        assert!(display.contains("JSON serialization error"));
345
346        let trailing = SerializationError::TrailingBytes("consumed 3 of 5".to_string());
347        assert!(format!("{trailing}").contains("consumed 3 of 5"));
348    }
349
350    #[test]
351    fn test_serialization_error_propagates_into_orderbook_error() {
352        use crate::orderbook::error::OrderBookError;
353
354        // A function returning OrderBookError can `?` a SerializationError
355        // thanks to the `From` bridge.
356        fn run() -> Result<i32, OrderBookError> {
357            let parsed: i32 = serde_json::from_str("nope").map_err(SerializationError::Json)?;
358            Ok(parsed)
359        }
360
361        match run() {
362            Err(OrderBookError::SerializationError { .. }) => {}
363            other => panic!("expected OrderBookError::SerializationError, got {other:?}"),
364        }
365    }
366
367    // ─── Bincode tests ──────────────────────────────────────────────────
368
369    #[cfg(feature = "bincode")]
370    mod bincode_tests {
371        use super::*;
372
373        #[test]
374        fn test_bincode_serialize_trade() {
375            let serializer = BincodeEventSerializer::new();
376            let trade = make_trade_result();
377            let result = serializer.serialize_trade(&trade);
378            assert!(result.is_ok());
379            let bytes = result.unwrap_or_default();
380            assert!(!bytes.is_empty());
381
382            // Bincode should be more compact than JSON
383            let json_serializer = JsonEventSerializer::new();
384            let json_bytes = json_serializer.serialize_trade(&trade).unwrap_or_default();
385            assert!(
386                bytes.len() < json_bytes.len(),
387                "bincode ({}) should be smaller than json ({})",
388                bytes.len(),
389                json_bytes.len()
390            );
391        }
392
393        #[test]
394        fn test_bincode_roundtrip_trade() {
395            let serializer = BincodeEventSerializer::new();
396            let trade = make_trade_result();
397            let bytes = serializer.serialize_trade(&trade);
398            assert!(bytes.is_ok());
399            let bytes = bytes.unwrap_or_default();
400
401            let decoded = serializer.deserialize_trade(&bytes);
402            assert!(decoded.is_ok());
403            let decoded = decoded.unwrap_or_else(|_| make_trade_result());
404            assert_eq!(decoded.symbol, trade.symbol);
405            assert_eq!(decoded.total_maker_fees, trade.total_maker_fees);
406            assert_eq!(decoded.total_taker_fees, trade.total_taker_fees);
407        }
408
409        #[test]
410        fn test_bincode_serialize_book_change() {
411            let serializer = BincodeEventSerializer::new();
412            let event = make_book_change();
413            let result = serializer.serialize_book_change(&event);
414            assert!(result.is_ok());
415            let bytes = result.unwrap_or_default();
416            assert!(!bytes.is_empty());
417        }
418
419        #[test]
420        fn test_bincode_roundtrip_book_change() {
421            let serializer = BincodeEventSerializer::new();
422            let event = make_book_change();
423            let bytes = serializer.serialize_book_change(&event);
424            assert!(bytes.is_ok());
425            let bytes = bytes.unwrap_or_default();
426
427            let decoded = serializer.deserialize_book_change(&bytes);
428            assert!(decoded.is_ok());
429            let decoded = decoded.unwrap_or_else(|_| make_book_change());
430            assert_eq!(decoded, event);
431        }
432
433        #[test]
434        fn test_bincode_content_type() {
435            let serializer = BincodeEventSerializer::new();
436            assert_eq!(serializer.content_type(), "application/x-bincode");
437        }
438
439        #[test]
440        fn test_bincode_deserialize_trade_error() {
441            let serializer = BincodeEventSerializer::new();
442            let result = serializer.deserialize_trade(b"\x00\x01");
443            assert!(result.is_err());
444        }
445
446        #[test]
447        fn test_bincode_deserialize_book_change_error() {
448            let serializer = BincodeEventSerializer::new();
449            let result = serializer.deserialize_book_change(b"\x00\x01");
450            assert!(result.is_err());
451        }
452
453        #[test]
454        fn test_bincode_smaller_than_json_book_change() {
455            let event = make_book_change();
456            let bincode_ser = BincodeEventSerializer::new();
457            let json_ser = JsonEventSerializer::new();
458
459            let bin_bytes = bincode_ser
460                .serialize_book_change(&event)
461                .unwrap_or_default();
462            let json_bytes = json_ser.serialize_book_change(&event).unwrap_or_default();
463
464            assert!(
465                bin_bytes.len() < json_bytes.len(),
466                "bincode ({}) should be smaller than json ({})",
467                bin_bytes.len(),
468                json_bytes.len()
469            );
470        }
471    }
472}