From 703bd9f509537cddf1d81d7c1f3cd7b081819b73 Mon Sep 17 00:00:00 2001 From: floor-licker Date: Thu, 29 Jan 2026 18:33:27 -0500 Subject: [PATCH] feat(book): align /book response fields --- src/client.rs | 16 +++++++++++-- src/decode.rs | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/types.rs | 15 ++++++++++-- 3 files changed, 92 insertions(+), 4 deletions(-) diff --git a/src/client.rs b/src/client.rs index 227f5c1..8e41290 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1914,7 +1914,11 @@ mod tests { ], "asks": [ {"price": "0.76", "size": "50.0"} - ] + ], + "min_order_size": "1", + "neg_risk": false, + "tick_size": "0.01", + "last_trade_price": "0.755" }"#; let mock = server @@ -1935,6 +1939,10 @@ mod tests { assert_eq!(book.market, "0x123"); assert_eq!(book.bids.len(), 1); assert_eq!(book.asks.len(), 1); + assert_eq!(book.min_order_size, Decimal::from_str("1").unwrap()); + assert!(!book.neg_risk); + assert_eq!(book.tick_size, Decimal::from_str("0.01").unwrap()); + assert_eq!(book.last_trade_price, Some(Decimal::from_str("0.755").unwrap())); } #[tokio::test(flavor = "multi_thread")] @@ -2349,7 +2357,11 @@ mod tests { "hash": "test-hash", "timestamp": "1234567890", "bids": [{"price": "0.75", "size": "100.0"}], - "asks": [{"price": "0.76", "size": "50.0"}] + "asks": [{"price": "0.76", "size": "50.0"}], + "min_order_size": "1", + "neg_risk": false, + "tick_size": "0.01", + "last_trade_price": null } ]"#; diff --git a/src/decode.rs b/src/decode.rs index c7c8070..aabbc73 100644 --- a/src/decode.rs +++ b/src/decode.rs @@ -108,6 +108,71 @@ pub mod deserializers { None => Ok(None), } } + + /// Deserialize a vec that may be `null` (treat `null` as empty vec). + pub fn vec_from_null<'de, D, T>(deserializer: D) -> std::result::Result, D::Error> + where + D: Deserializer<'de>, + T: serde::Deserialize<'de>, + { + Ok(Option::>::deserialize(deserializer)?.unwrap_or_default()) + } + + /// Deserialize an optional Decimal from string/number/null. + /// + /// - `null` => `None` + /// - `""` => `None` + /// - invalid values => error + pub fn optional_decimal_from_string<'de, D>( + deserializer: D, + ) -> std::result::Result, D::Error> + where + D: Deserializer<'de>, + { + let value = serde_json::Value::deserialize(deserializer)?; + match value { + serde_json::Value::Null => Ok(None), + serde_json::Value::String(s) => { + let s = s.trim(); + if s.is_empty() { + Ok(None) + } else { + s.parse::() + .map(Some) + .map_err(serde::de::Error::custom) + } + } + serde_json::Value::Number(n) => Decimal::from_str(&n.to_string()) + .map(Some) + .map_err(serde::de::Error::custom), + other => Err(serde::de::Error::custom(format!( + "Expected decimal as string/number/null, got {other}" + ))), + } + } + + /// Like `optional_decimal_from_string`, but returns `None` on parse errors. + pub fn optional_decimal_from_string_default_on_error<'de, D>( + deserializer: D, + ) -> std::result::Result, D::Error> + where + D: Deserializer<'de>, + { + let value = serde_json::Value::deserialize(deserializer)?; + match value { + serde_json::Value::Null => Ok(None), + serde_json::Value::String(s) => { + let s = s.trim(); + if s.is_empty() { + Ok(None) + } else { + Ok(s.parse::().ok()) + } + } + serde_json::Value::Number(n) => Ok(Decimal::from_str(&n.to_string()).ok()), + _ => Ok(None), + } + } } /// Raw API response types for efficient parsing diff --git a/src/types.rs b/src/types.rs index 79d3fa9..ca6041c 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1024,14 +1024,25 @@ pub struct BookParams { pub struct OrderBookSummary { pub market: String, pub asset_id: String, - pub hash: String, + #[serde(default)] + pub hash: Option, #[serde(deserialize_with = "crate::decode::deserializers::number_from_string")] pub timestamp: u64, + #[serde(default, deserialize_with = "crate::decode::deserializers::vec_from_null")] pub bids: Vec, + #[serde(default, deserialize_with = "crate::decode::deserializers::vec_from_null")] pub asks: Vec, + pub min_order_size: Decimal, + pub neg_risk: bool, + pub tick_size: Decimal, + #[serde( + default, + deserialize_with = "crate::decode::deserializers::optional_decimal_from_string_default_on_error" + )] + pub last_trade_price: Option, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct OrderSummary { #[serde(with = "rust_decimal::serde::str")] pub price: Decimal,