feat(book): align /book response fields

This commit is contained in:
floor-licker
2026-01-29 18:33:27 -05:00
parent e28e1afa0b
commit 703bd9f509
3 changed files with 92 additions and 4 deletions
+14 -2
View File
@@ -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
}
]"#;
+65
View File
@@ -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<Vec<T>, D::Error>
where
D: Deserializer<'de>,
T: serde::Deserialize<'de>,
{
Ok(Option::<Vec<T>>::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<Option<Decimal>, 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::<Decimal>()
.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<Option<Decimal>, 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::<Decimal>().ok())
}
}
serde_json::Value::Number(n) => Ok(Decimal::from_str(&n.to_string()).ok()),
_ => Ok(None),
}
}
}
/// Raw API response types for efficient parsing
+13 -2
View File
@@ -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<String>,
#[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<OrderSummary>,
#[serde(default, deserialize_with = "crate::decode::deserializers::vec_from_null")]
pub asks: Vec<OrderSummary>,
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<Decimal>,
}
#[derive(Debug, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrderSummary {
#[serde(with = "rust_decimal::serde::str")]
pub price: Decimal,