use crate::orderbook::book_change_event::PriceLevelChangedEvent;
use crate::orderbook::trade::TradeResult;
#[derive(Debug)]
pub struct SerializationError {
pub message: String,
}
impl std::fmt::Display for SerializationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "event serialization error: {}", self.message)
}
}
impl std::error::Error for SerializationError {}
pub trait EventSerializer: Send + Sync + std::fmt::Debug {
fn serialize_trade(&self, trade: &TradeResult) -> Result<Vec<u8>, SerializationError>;
fn serialize_book_change(
&self,
event: &PriceLevelChangedEvent,
) -> Result<Vec<u8>, SerializationError>;
fn deserialize_trade(&self, data: &[u8]) -> Result<TradeResult, SerializationError>;
fn deserialize_book_change(
&self,
data: &[u8],
) -> Result<PriceLevelChangedEvent, SerializationError>;
#[must_use]
fn content_type(&self) -> &'static str;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct JsonEventSerializer;
impl JsonEventSerializer {
#[must_use]
#[inline]
pub fn new() -> Self {
Self
}
}
impl EventSerializer for JsonEventSerializer {
fn serialize_trade(&self, trade: &TradeResult) -> Result<Vec<u8>, SerializationError> {
serde_json::to_vec(trade).map_err(|e| SerializationError {
message: e.to_string(),
})
}
fn serialize_book_change(
&self,
event: &PriceLevelChangedEvent,
) -> Result<Vec<u8>, SerializationError> {
serde_json::to_vec(event).map_err(|e| SerializationError {
message: e.to_string(),
})
}
fn deserialize_trade(&self, data: &[u8]) -> Result<TradeResult, SerializationError> {
serde_json::from_slice(data).map_err(|e| SerializationError {
message: e.to_string(),
})
}
fn deserialize_book_change(
&self,
data: &[u8],
) -> Result<PriceLevelChangedEvent, SerializationError> {
serde_json::from_slice(data).map_err(|e| SerializationError {
message: e.to_string(),
})
}
#[inline]
fn content_type(&self) -> &'static str {
"application/json"
}
}
#[cfg(feature = "bincode")]
#[derive(Debug, Clone, Copy, Default)]
pub struct BincodeEventSerializer;
#[cfg(feature = "bincode")]
impl BincodeEventSerializer {
#[must_use]
#[inline]
pub fn new() -> Self {
Self
}
}
#[cfg(feature = "bincode")]
impl EventSerializer for BincodeEventSerializer {
fn serialize_trade(&self, trade: &TradeResult) -> Result<Vec<u8>, SerializationError> {
bincode::serde::encode_to_vec(trade, bincode::config::standard()).map_err(|e| {
SerializationError {
message: e.to_string(),
}
})
}
fn serialize_book_change(
&self,
event: &PriceLevelChangedEvent,
) -> Result<Vec<u8>, SerializationError> {
bincode::serde::encode_to_vec(event, bincode::config::standard()).map_err(|e| {
SerializationError {
message: e.to_string(),
}
})
}
fn deserialize_trade(&self, data: &[u8]) -> Result<TradeResult, SerializationError> {
let (value, bytes_read) =
bincode::serde::decode_from_slice::<TradeResult, _>(data, bincode::config::standard())
.map_err(|e| SerializationError {
message: e.to_string(),
})?;
if bytes_read != data.len() {
return Err(SerializationError {
message: format!(
"trailing bytes after trade payload: consumed {bytes_read} of {}",
data.len()
),
});
}
Ok(value)
}
fn deserialize_book_change(
&self,
data: &[u8],
) -> Result<PriceLevelChangedEvent, SerializationError> {
let (value, bytes_read) = bincode::serde::decode_from_slice::<PriceLevelChangedEvent, _>(
data,
bincode::config::standard(),
)
.map_err(|e| SerializationError {
message: e.to_string(),
})?;
if bytes_read != data.len() {
return Err(SerializationError {
message: format!(
"trailing bytes after book-change payload: consumed {bytes_read} of {}",
data.len()
),
});
}
Ok(value)
}
#[inline]
fn content_type(&self) -> &'static str {
"application/x-bincode"
}
}
#[cfg(test)]
mod tests {
use super::*;
use pricelevel::{Id, MatchResult, Side};
fn make_trade_result() -> TradeResult {
let order_id = Id::new_uuid();
let match_result = MatchResult::new(order_id, 100);
TradeResult::new("BTC/USD".to_string(), match_result)
}
fn make_book_change() -> PriceLevelChangedEvent {
PriceLevelChangedEvent {
side: Side::Buy,
price: 50_000_000,
quantity: 1_000,
engine_seq: 0,
}
}
#[test]
fn test_json_serialize_trade() {
let serializer = JsonEventSerializer::new();
let trade = make_trade_result();
let result = serializer.serialize_trade(&trade);
assert!(result.is_ok());
let bytes = result.unwrap_or_default();
assert!(!bytes.is_empty());
let json_str = String::from_utf8(bytes).unwrap_or_default();
assert!(json_str.contains("BTC/USD"));
}
#[test]
fn test_json_roundtrip_trade() {
let serializer = JsonEventSerializer::new();
let trade = make_trade_result();
let bytes = serializer.serialize_trade(&trade);
assert!(bytes.is_ok());
let bytes = bytes.unwrap_or_default();
let decoded = serializer.deserialize_trade(&bytes);
assert!(decoded.is_ok());
let decoded = decoded.unwrap_or_else(|_| make_trade_result());
assert_eq!(decoded.symbol, trade.symbol);
assert_eq!(decoded.total_maker_fees, trade.total_maker_fees);
assert_eq!(decoded.total_taker_fees, trade.total_taker_fees);
}
#[test]
fn test_json_serialize_book_change() {
let serializer = JsonEventSerializer::new();
let event = make_book_change();
let result = serializer.serialize_book_change(&event);
assert!(result.is_ok());
let bytes = result.unwrap_or_default();
assert!(!bytes.is_empty());
}
#[test]
fn test_json_roundtrip_book_change() {
let serializer = JsonEventSerializer::new();
let event = make_book_change();
let bytes = serializer.serialize_book_change(&event);
assert!(bytes.is_ok());
let bytes = bytes.unwrap_or_default();
let decoded = serializer.deserialize_book_change(&bytes);
assert!(decoded.is_ok());
let decoded = decoded.unwrap_or_else(|_| make_book_change());
assert_eq!(decoded, event);
}
#[test]
fn test_json_content_type() {
let serializer = JsonEventSerializer::new();
assert_eq!(serializer.content_type(), "application/json");
}
#[test]
fn test_json_deserialize_trade_error() {
let serializer = JsonEventSerializer::new();
let result = serializer.deserialize_trade(b"not valid json");
assert!(result.is_err());
}
#[test]
fn test_json_deserialize_book_change_error() {
let serializer = JsonEventSerializer::new();
let result = serializer.deserialize_book_change(b"not valid json");
assert!(result.is_err());
}
#[test]
fn test_serialization_error_display() {
let err = SerializationError {
message: "test error".to_string(),
};
let display = format!("{err}");
assert!(display.contains("event serialization error"));
assert!(display.contains("test error"));
}
#[cfg(feature = "bincode")]
mod bincode_tests {
use super::*;
#[test]
fn test_bincode_serialize_trade() {
let serializer = BincodeEventSerializer::new();
let trade = make_trade_result();
let result = serializer.serialize_trade(&trade);
assert!(result.is_ok());
let bytes = result.unwrap_or_default();
assert!(!bytes.is_empty());
let json_serializer = JsonEventSerializer::new();
let json_bytes = json_serializer.serialize_trade(&trade).unwrap_or_default();
assert!(
bytes.len() < json_bytes.len(),
"bincode ({}) should be smaller than json ({})",
bytes.len(),
json_bytes.len()
);
}
#[test]
fn test_bincode_roundtrip_trade() {
let serializer = BincodeEventSerializer::new();
let trade = make_trade_result();
let bytes = serializer.serialize_trade(&trade);
assert!(bytes.is_ok());
let bytes = bytes.unwrap_or_default();
let decoded = serializer.deserialize_trade(&bytes);
assert!(decoded.is_ok());
let decoded = decoded.unwrap_or_else(|_| make_trade_result());
assert_eq!(decoded.symbol, trade.symbol);
assert_eq!(decoded.total_maker_fees, trade.total_maker_fees);
assert_eq!(decoded.total_taker_fees, trade.total_taker_fees);
}
#[test]
fn test_bincode_serialize_book_change() {
let serializer = BincodeEventSerializer::new();
let event = make_book_change();
let result = serializer.serialize_book_change(&event);
assert!(result.is_ok());
let bytes = result.unwrap_or_default();
assert!(!bytes.is_empty());
}
#[test]
fn test_bincode_roundtrip_book_change() {
let serializer = BincodeEventSerializer::new();
let event = make_book_change();
let bytes = serializer.serialize_book_change(&event);
assert!(bytes.is_ok());
let bytes = bytes.unwrap_or_default();
let decoded = serializer.deserialize_book_change(&bytes);
assert!(decoded.is_ok());
let decoded = decoded.unwrap_or_else(|_| make_book_change());
assert_eq!(decoded, event);
}
#[test]
fn test_bincode_content_type() {
let serializer = BincodeEventSerializer::new();
assert_eq!(serializer.content_type(), "application/x-bincode");
}
#[test]
fn test_bincode_deserialize_trade_error() {
let serializer = BincodeEventSerializer::new();
let result = serializer.deserialize_trade(b"\x00\x01");
assert!(result.is_err());
}
#[test]
fn test_bincode_deserialize_book_change_error() {
let serializer = BincodeEventSerializer::new();
let result = serializer.deserialize_book_change(b"\x00\x01");
assert!(result.is_err());
}
#[test]
fn test_bincode_smaller_than_json_book_change() {
let event = make_book_change();
let bincode_ser = BincodeEventSerializer::new();
let json_ser = JsonEventSerializer::new();
let bin_bytes = bincode_ser
.serialize_book_change(&event)
.unwrap_or_default();
let json_bytes = json_ser.serialize_book_change(&event).unwrap_or_default();
assert!(
bin_bytes.len() < json_bytes.len(),
"bincode ({}) should be smaller than json ({})",
bin_bytes.len(),
json_bytes.len()
);
}
}
}