1use rust_decimal::Decimal;
9use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
10use serde_json::Value;
11
12pub use crate::shared::symbol::{InvalidSymbol, Symbol};
13
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(rename_all = "camelCase")]
17#[non_exhaustive]
18pub struct ServerTime {
19 pub server_time: u64,
21}
22
23#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
25#[serde(rename_all = "camelCase")]
26#[non_exhaustive]
27pub struct ExchangeInfo {
28 pub timezone: String,
30 pub server_time: u64,
32 pub rate_limits: Vec<RateLimit>,
34 pub exchange_filters: Vec<Filter>,
36 pub symbols: Vec<SymbolInfo>,
38 #[serde(default, skip_serializing_if = "Option::is_none")]
41 pub sors: Option<Vec<Sor>>,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
46#[serde(rename_all = "camelCase")]
47#[non_exhaustive]
48pub struct RateLimit {
49 pub rate_limit_type: String,
51 pub interval: String,
53 pub interval_num: u32,
55 pub limit: u32,
57}
58
59#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
61#[serde(rename_all = "camelCase")]
62#[non_exhaustive]
63pub struct Sor {
64 pub base_asset: String,
66 pub symbols: Vec<String>,
68}
69
70#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
72#[serde(rename_all = "camelCase")]
73#[non_exhaustive]
74pub struct SymbolInfo {
75 pub symbol: String,
77 pub status: SymbolStatus,
79 pub base_asset: String,
81 pub base_asset_precision: u32,
83 pub quote_asset: String,
85 pub quote_precision: u32,
87 pub quote_asset_precision: u32,
89 pub base_commission_precision: u32,
91 pub quote_commission_precision: u32,
93 pub order_types: Vec<String>,
95 pub iceberg_allowed: bool,
97 pub oco_allowed: bool,
99 pub oto_allowed: bool,
101 #[serde(default, skip_serializing_if = "Option::is_none")]
104 pub opo_allowed: Option<bool>,
105 pub quote_order_qty_market_allowed: bool,
107 pub allow_trailing_stop: bool,
109 pub cancel_replace_allowed: bool,
111 #[serde(default, skip_serializing_if = "Option::is_none")]
114 pub amend_allowed: Option<bool>,
115 #[serde(default, skip_serializing_if = "Option::is_none")]
118 pub peg_instructions_allowed: Option<bool>,
119 pub is_spot_trading_allowed: bool,
121 pub is_margin_trading_allowed: bool,
123 pub filters: Vec<Filter>,
125 pub permissions: Vec<String>,
127 pub permission_sets: Vec<Vec<String>>,
129 pub default_self_trade_prevention_mode: String,
131 pub allowed_self_trade_prevention_modes: Vec<String>,
133}
134
135polyoxide_venue::open_enum! {
136 pub enum SymbolStatus {
139 Trading => "TRADING",
140 Break => "BREAK",
141 Halt => "HALT",
142 PreTrading => "PRE_TRADING",
143 PostTrading => "POST_TRADING",
144 EndOfDay => "END_OF_DAY",
145 AuctionMatch => "AUCTION_MATCH",
146 }
147}
148
149macro_rules! filters {
154 ($(
155 $(#[$vmeta:meta])*
156 $variant:ident = $tag:literal {
157 $( $field:ident : $ty:ty $(=> $with:literal)? ),* $(,)?
158 }
159 )+) => {
160 #[derive(Debug, Clone, PartialEq)]
168 #[non_exhaustive]
169 pub enum Filter {
170 $(
171 $(#[$vmeta])*
172 #[non_exhaustive]
173 $variant { $( $field: $ty ),* },
174 )+
175 Other {
177 filter_type: String,
179 raw: Value,
181 },
182 }
183
184 #[derive(Serialize, Deserialize)]
187 #[serde(tag = "filterType")]
188 enum FilterWire {
189 $(
190 #[serde(rename = $tag, rename_all = "camelCase")]
191 $variant { $( $(#[serde(with = $with)])? $field: $ty ),* },
192 )+
193 }
194
195 impl FilterWire {
196 const KNOWN: &'static [&'static str] = &[$($tag),+];
198 }
199
200 impl From<FilterWire> for Filter {
201 fn from(wire: FilterWire) -> Self {
202 match wire {
203 $( FilterWire::$variant { $($field),* } => Self::$variant { $($field),* }, )+
204 }
205 }
206 }
207
208 impl<'de> Deserialize<'de> for Filter {
209 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
210 let raw = Value::deserialize(deserializer)?;
211 let filter_type = raw
212 .get("filterType")
213 .and_then(Value::as_str)
214 .ok_or_else(|| <D::Error as de::Error>::custom("a filter without a string filterType"))?
215 .to_owned();
216 if !FilterWire::KNOWN.contains(&filter_type.as_str()) {
217 return Ok(Self::Other { filter_type, raw });
218 }
219 let wire: FilterWire =
220 serde_json::from_value(raw).map_err(<D::Error as de::Error>::custom)?;
221 Ok(wire.into())
222 }
223 }
224
225 impl Serialize for Filter {
226 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
227 let wire = match self {
228 $( Self::$variant { $($field),* } => FilterWire::$variant { $($field: *$field),* }, )+
229 Self::Other { raw, .. } => return raw.serialize(serializer),
230 };
231 wire.serialize(serializer)
232 }
233 }
234 };
235}
236
237filters! {
238 PriceFilter = "PRICE_FILTER" {
240 min_price: Decimal => "rust_decimal::serde::str",
241 max_price: Decimal => "rust_decimal::serde::str",
242 tick_size: Decimal => "rust_decimal::serde::str",
243 }
244 PercentPriceBySide = "PERCENT_PRICE_BY_SIDE" {
246 bid_multiplier_up: Decimal => "rust_decimal::serde::str",
247 bid_multiplier_down: Decimal => "rust_decimal::serde::str",
248 ask_multiplier_up: Decimal => "rust_decimal::serde::str",
249 ask_multiplier_down: Decimal => "rust_decimal::serde::str",
250 avg_price_mins: u32,
251 }
252 LotSize = "LOT_SIZE" {
254 min_qty: Decimal => "rust_decimal::serde::str",
255 max_qty: Decimal => "rust_decimal::serde::str",
256 step_size: Decimal => "rust_decimal::serde::str",
257 }
258 Notional = "NOTIONAL" {
260 min_notional: Decimal => "rust_decimal::serde::str",
261 apply_min_to_market: bool,
262 max_notional: Decimal => "rust_decimal::serde::str",
263 apply_max_to_market: bool,
264 avg_price_mins: u32,
265 }
266 IcebergParts = "ICEBERG_PARTS" {
268 limit: u32,
269 }
270 MarketLotSize = "MARKET_LOT_SIZE" {
272 min_qty: Decimal => "rust_decimal::serde::str",
273 max_qty: Decimal => "rust_decimal::serde::str",
274 step_size: Decimal => "rust_decimal::serde::str",
275 }
276 MaxNumOrders = "MAX_NUM_ORDERS" {
278 max_num_orders: u32,
279 }
280 MaxNumAlgoOrders = "MAX_NUM_ALGO_ORDERS" {
282 max_num_algo_orders: u32,
283 }
284 MaxPosition = "MAX_POSITION" {
286 max_position: Decimal => "rust_decimal::serde::str",
287 }
288 TrailingDelta = "TRAILING_DELTA" {
290 min_trailing_above_delta: u64,
291 max_trailing_above_delta: u64,
292 min_trailing_below_delta: u64,
293 max_trailing_below_delta: u64,
294 }
295 MaxNumOrderAmends = "MAX_NUM_ORDER_AMENDS" {
297 max_num_order_amends: u32,
298 }
299 MaxNumOrderLists = "MAX_NUM_ORDER_LISTS" {
301 max_num_order_lists: u32,
302 }
303}
304
305pub use rows::{
306 AggTrade, AvgPrice, BookTicker, Depth, Interval, Kline, Level, PriceTicker, Ticker24h, Trade,
307};
308
309mod rows {
314 use std::fmt;
315
316 use polyoxide_venue::positional::{drain, element, DecimalStr};
317 use rust_decimal::Decimal;
318 use serde::{
319 de::{SeqAccess, Visitor},
320 ser::SerializeSeq,
321 Deserialize, Deserializer, Serialize, Serializer,
322 };
323
324 polyoxide_venue::wire_enum! {
325 pub enum Interval {
328 Second1 => "1s",
330 Minute1 => "1m",
332 Minute3 => "3m",
334 Minute5 => "5m",
336 Minute15 => "15m",
338 Minute30 => "30m",
340 Hour1 => "1h",
342 Hour2 => "2h",
344 Hour4 => "4h",
346 Hour6 => "6h",
348 Hour8 => "8h",
350 Hour12 => "12h",
352 Day1 => "1d",
354 Day3 => "3d",
356 Week1 => "1w",
358 Month1 => "1M",
360 }
361 }
362
363 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
365 #[serde(rename_all = "camelCase")]
366 #[non_exhaustive]
367 pub struct Depth {
368 pub last_update_id: u64,
370 pub bids: Vec<Level>,
372 pub asks: Vec<Level>,
374 }
375
376 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
378 #[non_exhaustive]
379 pub struct Level {
380 pub price: Decimal,
382 pub quantity: Decimal,
384 }
385
386 impl Level {
387 pub fn new(price: Decimal, quantity: Decimal) -> Self {
389 Self { price, quantity }
390 }
391 }
392
393 impl<'de> Deserialize<'de> for Level {
394 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
395 struct LevelVisitor;
396
397 impl<'de> Visitor<'de> for LevelVisitor {
398 type Value = Level;
399
400 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
401 f.write_str("a [price, quantity] array")
402 }
403
404 fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Level, A::Error> {
405 const WHAT: &str = "a [price, quantity] array";
406 let level = Level {
407 price: element::<DecimalStr, _>(&mut seq, 0, WHAT)?.0,
408 quantity: element::<DecimalStr, _>(&mut seq, 1, WHAT)?.0,
409 };
410 drain(&mut seq)?;
411 Ok(level)
412 }
413 }
414
415 deserializer.deserialize_seq(LevelVisitor)
416 }
417 }
418
419 impl Serialize for Level {
420 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
421 let mut seq = serializer.serialize_seq(Some(2))?;
422 seq.serialize_element(&self.price.to_string())?;
423 seq.serialize_element(&self.quantity.to_string())?;
424 seq.end()
425 }
426 }
427
428 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
430 #[serde(rename_all = "camelCase")]
431 #[non_exhaustive]
432 pub struct Trade {
433 pub id: u64,
435 #[serde(with = "rust_decimal::serde::str")]
437 pub price: Decimal,
438 #[serde(with = "rust_decimal::serde::str")]
440 pub qty: Decimal,
441 #[serde(with = "rust_decimal::serde::str")]
443 pub quote_qty: Decimal,
444 pub time: u64,
446 pub is_buyer_maker: bool,
448 pub is_best_match: bool,
450 }
451
452 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
455 #[non_exhaustive]
456 pub struct AggTrade {
457 #[serde(rename = "a")]
459 pub id: u64,
460 #[serde(rename = "p", with = "rust_decimal::serde::str")]
462 pub price: Decimal,
463 #[serde(rename = "q", with = "rust_decimal::serde::str")]
465 pub quantity: Decimal,
466 #[serde(rename = "f")]
468 pub first_trade_id: u64,
469 #[serde(rename = "l")]
471 pub last_trade_id: u64,
472 #[serde(rename = "T")]
474 pub time: u64,
475 #[serde(rename = "m")]
477 pub is_buyer_maker: bool,
478 #[serde(rename = "M")]
480 pub is_best_match: bool,
481 }
482
483 #[derive(Debug, Clone, PartialEq, Eq)]
488 #[non_exhaustive]
489 pub struct Kline {
490 pub open_time: u64,
492 pub open: Decimal,
494 pub high: Decimal,
496 pub low: Decimal,
498 pub close: Decimal,
500 pub volume: Decimal,
502 pub close_time: u64,
504 pub quote_volume: Decimal,
506 pub trade_count: u64,
508 pub taker_buy_base_volume: Decimal,
510 pub taker_buy_quote_volume: Decimal,
512 }
513
514 impl<'de> Deserialize<'de> for Kline {
515 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
516 struct KlineVisitor;
517
518 impl<'de> Visitor<'de> for KlineVisitor {
519 type Value = Kline;
520
521 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
522 f.write_str("a kline array of at least 11 elements")
523 }
524
525 fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Kline, A::Error> {
526 const WHAT: &str = "a kline array of at least 11 elements";
527 let kline = Kline {
528 open_time: element(&mut seq, 0, WHAT)?,
529 open: element::<DecimalStr, _>(&mut seq, 1, WHAT)?.0,
530 high: element::<DecimalStr, _>(&mut seq, 2, WHAT)?.0,
531 low: element::<DecimalStr, _>(&mut seq, 3, WHAT)?.0,
532 close: element::<DecimalStr, _>(&mut seq, 4, WHAT)?.0,
533 volume: element::<DecimalStr, _>(&mut seq, 5, WHAT)?.0,
534 close_time: element(&mut seq, 6, WHAT)?,
535 quote_volume: element::<DecimalStr, _>(&mut seq, 7, WHAT)?.0,
536 trade_count: element(&mut seq, 8, WHAT)?,
537 taker_buy_base_volume: element::<DecimalStr, _>(&mut seq, 9, WHAT)?.0,
538 taker_buy_quote_volume: element::<DecimalStr, _>(&mut seq, 10, WHAT)?.0,
539 };
540 drain(&mut seq)?;
541 Ok(kline)
542 }
543 }
544
545 deserializer.deserialize_seq(KlineVisitor)
546 }
547 }
548
549 impl Serialize for Kline {
550 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
551 let mut seq = serializer.serialize_seq(Some(11))?;
552 seq.serialize_element(&self.open_time)?;
553 for value in [&self.open, &self.high, &self.low, &self.close, &self.volume] {
554 seq.serialize_element(&value.to_string())?;
555 }
556 seq.serialize_element(&self.close_time)?;
557 seq.serialize_element(&self.quote_volume.to_string())?;
558 seq.serialize_element(&self.trade_count)?;
559 seq.serialize_element(&self.taker_buy_base_volume.to_string())?;
560 seq.serialize_element(&self.taker_buy_quote_volume.to_string())?;
561 seq.end()
562 }
563 }
564
565 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
567 #[serde(rename_all = "camelCase")]
568 #[non_exhaustive]
569 pub struct AvgPrice {
570 pub mins: u32,
572 #[serde(with = "rust_decimal::serde::str")]
574 pub price: Decimal,
575 pub close_time: u64,
577 }
578
579 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
581 #[serde(rename_all = "camelCase")]
582 #[non_exhaustive]
583 pub struct Ticker24h {
584 pub symbol: String,
586 #[serde(with = "rust_decimal::serde::str")]
588 pub price_change: Decimal,
589 #[serde(with = "rust_decimal::serde::str")]
591 pub price_change_percent: Decimal,
592 #[serde(with = "rust_decimal::serde::str")]
594 pub weighted_avg_price: Decimal,
595 #[serde(with = "rust_decimal::serde::str")]
597 pub prev_close_price: Decimal,
598 #[serde(with = "rust_decimal::serde::str")]
600 pub last_price: Decimal,
601 #[serde(with = "rust_decimal::serde::str")]
603 pub last_qty: Decimal,
604 #[serde(with = "rust_decimal::serde::str")]
606 pub bid_price: Decimal,
607 #[serde(with = "rust_decimal::serde::str")]
609 pub bid_qty: Decimal,
610 #[serde(with = "rust_decimal::serde::str")]
612 pub ask_price: Decimal,
613 #[serde(with = "rust_decimal::serde::str")]
615 pub ask_qty: Decimal,
616 #[serde(with = "rust_decimal::serde::str")]
618 pub open_price: Decimal,
619 #[serde(with = "rust_decimal::serde::str")]
621 pub high_price: Decimal,
622 #[serde(with = "rust_decimal::serde::str")]
624 pub low_price: Decimal,
625 #[serde(with = "rust_decimal::serde::str")]
627 pub volume: Decimal,
628 #[serde(with = "rust_decimal::serde::str")]
630 pub quote_volume: Decimal,
631 pub open_time: u64,
633 pub close_time: u64,
635 pub first_id: i64,
637 pub last_id: i64,
639 pub count: u64,
641 }
642
643 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
645 #[non_exhaustive]
646 pub struct PriceTicker {
647 pub symbol: String,
649 #[serde(with = "rust_decimal::serde::str")]
651 pub price: Decimal,
652 }
653
654 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
657 #[serde(rename_all = "camelCase")]
658 #[non_exhaustive]
659 pub struct BookTicker {
660 pub symbol: String,
662 #[serde(with = "rust_decimal::serde::str")]
664 pub bid_price: Decimal,
665 #[serde(with = "rust_decimal::serde::str")]
667 pub bid_qty: Decimal,
668 #[serde(with = "rust_decimal::serde::str")]
670 pub ask_price: Decimal,
671 #[serde(with = "rust_decimal::serde::str")]
673 pub ask_qty: Decimal,
674 }
675}
676
677#[cfg(test)]
678mod tests {
679 use polyoxide_test_support::{agreement, fixtures};
680
681 use super::*;
682
683 fn text(name: &str) -> String {
684 fixtures!("spot/rest").text(name)
685 }
686
687 #[test]
688 fn the_exchange_info_capture_agrees_with_its_types() {
689 let text = text("exchange_info");
690 let diff = agreement::compare::<ExchangeInfo>("exchange_info", &text);
691 assert!(
692 diff.unmodelled.is_empty(),
693 "the server sent {:?}, which the types do not model",
694 diff.unmodelled
695 );
696 assert!(
697 diff.invented.is_empty(),
698 "the types emit {:?}, which the server did not send",
699 diff.invented
700 );
701 }
702
703 #[test]
704 fn every_symbol_and_filter_serializes_back_to_what_the_server_sent() {
705 let text = text("exchange_info");
706 let raw: Value = serde_json::from_str(&text).unwrap();
707 let info: ExchangeInfo = serde_json::from_str(&text).unwrap();
708 assert_eq!(
709 serde_json::to_value(&info.rate_limits).unwrap(),
710 raw["rateLimits"]
711 );
712 assert_eq!(
713 serde_json::to_value(&info.exchange_filters).unwrap(),
714 raw["exchangeFilters"]
715 );
716 let wire = raw["symbols"].as_array().unwrap();
717 assert_eq!(info.symbols.len(), wire.len());
718 for (symbol, sent) in info.symbols.iter().zip(wire) {
719 assert_eq!(
720 serde_json::to_value(symbol).unwrap(),
721 *sent,
722 "{}",
723 symbol.symbol
724 );
725 }
726 }
727
728 #[test]
729 fn the_capture_holds_the_cases_the_types_exist_for() {
730 let info: ExchangeInfo = serde_json::from_str(&text("exchange_info")).unwrap();
731 assert!(info.symbols.len() >= 2, "BTCUSDT and a non-trading symbol");
732 assert!(
733 info.symbols
734 .iter()
735 .any(|s| s.symbol == "BTCUSDT" && s.status == SymbolStatus::Trading),
736 "BTCUSDT, trading"
737 );
738 assert!(
739 info.symbols
740 .iter()
741 .any(|s| s.status != SymbolStatus::Trading),
742 "a symbol that is not trading"
743 );
744 let unnamed: Vec<_> = info
747 .symbols
748 .iter()
749 .filter(|s| matches!(s.status, SymbolStatus::Other(_)))
750 .map(|s| (&s.symbol, &s.status))
751 .collect();
752 assert!(
753 unnamed.is_empty(),
754 "statuses the enum does not name: {unnamed:?}"
755 );
756 let unnamed: Vec<&Filter> = info
759 .symbols
760 .iter()
761 .flat_map(|s| &s.filters)
762 .chain(&info.exchange_filters)
763 .filter(|f| matches!(f, Filter::Other { .. }))
764 .collect();
765 assert!(
766 unnamed.is_empty(),
767 "filter types Filter does not name, add them to `filters!`: {unnamed:?}"
768 );
769 }
770
771 #[test]
772 fn the_captured_btcusdt_filters_decode_to_their_values() {
773 let text = text("exchange_info");
774 let raw: Value = serde_json::from_str(&text).unwrap();
775 let info: ExchangeInfo = serde_json::from_str(&text).unwrap();
776 let btc = info.symbols.iter().find(|s| s.symbol == "BTCUSDT").unwrap();
777 let wire = raw["symbols"]
778 .as_array()
779 .unwrap()
780 .iter()
781 .find(|s| s["symbol"] == "BTCUSDT")
782 .unwrap();
783 let sent = |filter_type: &str, key: &str| -> String {
784 wire["filters"]
785 .as_array()
786 .unwrap()
787 .iter()
788 .find(|f| f["filterType"] == filter_type)
789 .unwrap_or_else(|| panic!("no {filter_type}"))[key]
790 .as_str()
791 .unwrap()
792 .to_owned()
793 };
794 let price = btc
795 .filters
796 .iter()
797 .find_map(|f| match f {
798 Filter::PriceFilter { tick_size, .. } => Some(*tick_size),
799 _ => None,
800 })
801 .expect("a PRICE_FILTER");
802 assert_eq!(price.to_string(), sent("PRICE_FILTER", "tickSize"));
803 let lot = btc
804 .filters
805 .iter()
806 .find_map(|f| match f {
807 Filter::LotSize { step_size, .. } => Some(*step_size),
808 _ => None,
809 })
810 .expect("a LOT_SIZE");
811 assert_eq!(lot.to_string(), sent("LOT_SIZE", "stepSize"));
812 let market = btc
813 .filters
814 .iter()
815 .find_map(|f| match f {
816 Filter::MarketLotSize { step_size, .. } => Some(*step_size),
817 _ => None,
818 })
819 .expect("a MARKET_LOT_SIZE");
820 assert_eq!(market.to_string(), sent("MARKET_LOT_SIZE", "stepSize"));
821 let notional = btc
822 .filters
823 .iter()
824 .find_map(|f| match f {
825 Filter::Notional { min_notional, .. } => Some(*min_notional),
826 _ => None,
827 })
828 .expect("a NOTIONAL");
829 assert_eq!(notional.to_string(), sent("NOTIONAL", "minNotional"));
830 }
831
832 #[test]
833 fn the_time_capture_agrees_with_its_type() {
834 let text = text("time");
835 let diff = agreement::compare::<ServerTime>("time", &text);
836 assert!(diff.unmodelled.is_empty(), "{:?}", diff.unmodelled);
837 assert!(diff.invented.is_empty(), "{:?}", diff.invented);
838 let time: ServerTime = serde_json::from_str(&text).unwrap();
839 assert!(time.server_time > 1_790_000_000_000);
840 }
841
842 #[test]
843 fn an_unknown_filter_type_is_kept_whole() {
844 let raw = serde_json::json!({"filterType": "A_FILTER_FROM_THE_FUTURE", "limit": 7});
845 let filter: Filter = serde_json::from_value(raw.clone()).unwrap();
846 assert!(matches!(
847 &filter,
848 Filter::Other { filter_type, raw: kept }
849 if filter_type == "A_FILTER_FROM_THE_FUTURE" && *kept == raw
850 ));
851 assert_eq!(serde_json::to_value(&filter).unwrap(), raw);
852 }
853
854 #[test]
855 fn a_known_filter_that_loses_a_field_fails() {
856 let raw = serde_json::json!({"filterType": "PRICE_FILTER", "minPrice": "0.01000000"});
857 assert!(serde_json::from_value::<Filter>(raw).is_err());
858 }
859
860 #[test]
861 fn a_filter_without_a_type_fails() {
862 let raw = serde_json::json!({"minPrice": "0.01000000"});
863 let err = serde_json::from_value::<Filter>(raw).unwrap_err();
864 assert!(err.to_string().contains("filterType"), "{err}");
865 }
866
867 #[test]
868 fn a_status_keeps_an_unknown_spelling() {
869 assert_eq!(SymbolStatus::from_wire("TRADING"), SymbolStatus::Trading);
870 assert_eq!(
871 SymbolStatus::from_wire("PRE_TRADING"),
872 SymbolStatus::PreTrading
873 );
874 let unknown = SymbolStatus::from_wire("A_NEW_STATE");
875 assert_eq!(unknown, SymbolStatus::Other("A_NEW_STATE".into()));
876 assert_eq!(unknown.as_str(), "A_NEW_STATE");
877 }
878
879 #[test]
880 fn the_status_spellings_are_binance_s() {
881 let table = [
882 (SymbolStatus::Trading, "TRADING"),
883 (SymbolStatus::Break, "BREAK"),
884 (SymbolStatus::Halt, "HALT"),
885 (SymbolStatus::PreTrading, "PRE_TRADING"),
886 (SymbolStatus::PostTrading, "POST_TRADING"),
887 (SymbolStatus::EndOfDay, "END_OF_DAY"),
888 (SymbolStatus::AuctionMatch, "AUCTION_MATCH"),
889 ];
890 for (status, wire) in table {
891 assert_eq!(status.as_str(), wire);
892 }
893 }
894}