1use std::fmt;
9
10use rust_decimal::Decimal;
11use serde::{Deserialize, Serialize, Serializer};
12use serde_json::Value;
13
14use crate::spot::{
15 types::{Interval, Level},
16 ws::{error::SpotWsError, stream::StreamName},
17};
18
19#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
21#[non_exhaustive]
22pub struct AggTradeEvent {
23 #[serde(rename = "e")]
25 pub event_type: String,
26 #[serde(rename = "E")]
28 pub event_time: u64,
29 #[serde(rename = "s")]
31 pub symbol: String,
32 #[serde(rename = "a")]
34 pub id: u64,
35 #[serde(rename = "p", with = "rust_decimal::serde::str")]
37 pub price: Decimal,
38 #[serde(rename = "q", with = "rust_decimal::serde::str")]
40 pub quantity: Decimal,
41 #[serde(rename = "f")]
43 pub first_trade_id: u64,
44 #[serde(rename = "l")]
46 pub last_trade_id: u64,
47 #[serde(rename = "T")]
49 pub trade_time: u64,
50 #[serde(rename = "m")]
52 pub is_buyer_maker: bool,
53}
54
55#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
57#[non_exhaustive]
58pub struct TradeEvent {
59 #[serde(rename = "e")]
61 pub event_type: String,
62 #[serde(rename = "E")]
64 pub event_time: u64,
65 #[serde(rename = "s")]
67 pub symbol: String,
68 #[serde(rename = "t")]
70 pub id: u64,
71 #[serde(rename = "p", with = "rust_decimal::serde::str")]
73 pub price: Decimal,
74 #[serde(rename = "q", with = "rust_decimal::serde::str")]
76 pub quantity: Decimal,
77 #[serde(rename = "T")]
79 pub trade_time: u64,
80 #[serde(rename = "m")]
82 pub is_buyer_maker: bool,
83}
84
85#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
87#[non_exhaustive]
88pub struct KlineEvent {
89 #[serde(rename = "e")]
91 pub event_type: String,
92 #[serde(rename = "E")]
94 pub event_time: u64,
95 #[serde(rename = "s")]
97 pub symbol: String,
98 #[serde(rename = "k")]
100 pub kline: KlineBar,
101}
102
103#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
106#[non_exhaustive]
107pub struct KlineBar {
108 #[serde(rename = "t")]
110 pub open_time: u64,
111 #[serde(rename = "T")]
113 pub close_time: u64,
114 #[serde(rename = "s")]
116 pub symbol: String,
117 #[serde(rename = "i")]
119 pub interval: Interval,
120 #[serde(rename = "f")]
122 pub first_trade_id: i64,
123 #[serde(rename = "L")]
125 pub last_trade_id: i64,
126 #[serde(rename = "o", with = "rust_decimal::serde::str")]
128 pub open: Decimal,
129 #[serde(rename = "c", with = "rust_decimal::serde::str")]
131 pub close: Decimal,
132 #[serde(rename = "h", with = "rust_decimal::serde::str")]
134 pub high: Decimal,
135 #[serde(rename = "l", with = "rust_decimal::serde::str")]
137 pub low: Decimal,
138 #[serde(rename = "v", with = "rust_decimal::serde::str")]
140 pub volume: Decimal,
141 #[serde(rename = "n")]
143 pub trade_count: u64,
144 #[serde(rename = "x")]
146 pub is_closed: bool,
147 #[serde(rename = "q", with = "rust_decimal::serde::str")]
149 pub quote_volume: Decimal,
150 #[serde(rename = "V", with = "rust_decimal::serde::str")]
152 pub taker_buy_base_volume: Decimal,
153 #[serde(rename = "Q", with = "rust_decimal::serde::str")]
155 pub taker_buy_quote_volume: Decimal,
156}
157
158#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
161#[non_exhaustive]
162pub struct MiniTickerEvent {
163 #[serde(rename = "e")]
165 pub event_type: String,
166 #[serde(rename = "E")]
168 pub event_time: u64,
169 #[serde(rename = "s")]
171 pub symbol: String,
172 #[serde(rename = "c", with = "rust_decimal::serde::str")]
174 pub close_price: Decimal,
175 #[serde(rename = "o", with = "rust_decimal::serde::str")]
177 pub open_price: Decimal,
178 #[serde(rename = "h", with = "rust_decimal::serde::str")]
180 pub high_price: Decimal,
181 #[serde(rename = "l", with = "rust_decimal::serde::str")]
183 pub low_price: Decimal,
184 #[serde(rename = "v", with = "rust_decimal::serde::str")]
186 pub volume: Decimal,
187 #[serde(rename = "q", with = "rust_decimal::serde::str")]
189 pub quote_volume: Decimal,
190}
191
192#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
194#[non_exhaustive]
195pub struct TickerEvent {
196 #[serde(rename = "e")]
198 pub event_type: String,
199 #[serde(rename = "E")]
201 pub event_time: u64,
202 #[serde(rename = "s")]
204 pub symbol: String,
205 #[serde(rename = "p", with = "rust_decimal::serde::str")]
207 pub price_change: Decimal,
208 #[serde(rename = "P", with = "rust_decimal::serde::str")]
210 pub price_change_percent: Decimal,
211 #[serde(rename = "w", with = "rust_decimal::serde::str")]
213 pub weighted_avg_price: Decimal,
214 #[serde(rename = "x", with = "rust_decimal::serde::str")]
216 pub prev_close_price: Decimal,
217 #[serde(rename = "c", with = "rust_decimal::serde::str")]
219 pub last_price: Decimal,
220 #[serde(rename = "Q", with = "rust_decimal::serde::str")]
222 pub last_quantity: Decimal,
223 #[serde(rename = "b", with = "rust_decimal::serde::str")]
225 pub bid_price: Decimal,
226 #[serde(rename = "B", with = "rust_decimal::serde::str")]
228 pub bid_quantity: Decimal,
229 #[serde(rename = "a", with = "rust_decimal::serde::str")]
231 pub ask_price: Decimal,
232 #[serde(rename = "A", with = "rust_decimal::serde::str")]
234 pub ask_quantity: Decimal,
235 #[serde(rename = "o", with = "rust_decimal::serde::str")]
237 pub open_price: Decimal,
238 #[serde(rename = "h", with = "rust_decimal::serde::str")]
240 pub high_price: Decimal,
241 #[serde(rename = "l", with = "rust_decimal::serde::str")]
243 pub low_price: Decimal,
244 #[serde(rename = "v", with = "rust_decimal::serde::str")]
246 pub volume: Decimal,
247 #[serde(rename = "q", with = "rust_decimal::serde::str")]
249 pub quote_volume: Decimal,
250 #[serde(rename = "O")]
252 pub open_time: u64,
253 #[serde(rename = "C")]
255 pub close_time: u64,
256 #[serde(rename = "F")]
258 pub first_trade_id: i64,
259 #[serde(rename = "L")]
261 pub last_trade_id: i64,
262 #[serde(rename = "n")]
264 pub trade_count: u64,
265}
266
267#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
270#[non_exhaustive]
271pub struct BookTickerEvent {
272 #[serde(rename = "u")]
274 pub update_id: u64,
275 #[serde(rename = "s")]
277 pub symbol: String,
278 #[serde(rename = "b", with = "rust_decimal::serde::str")]
280 pub bid_price: Decimal,
281 #[serde(rename = "B", with = "rust_decimal::serde::str")]
283 pub bid_quantity: Decimal,
284 #[serde(rename = "a", with = "rust_decimal::serde::str")]
286 pub ask_price: Decimal,
287 #[serde(rename = "A", with = "rust_decimal::serde::str")]
289 pub ask_quantity: Decimal,
290}
291
292#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
298#[non_exhaustive]
299pub struct PartialDepthEvent {
300 #[serde(skip)]
302 pub symbol: String,
303 #[serde(rename = "lastUpdateId")]
305 pub last_update_id: u64,
306 pub bids: Vec<Level>,
308 pub asks: Vec<Level>,
310}
311
312#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
314#[non_exhaustive]
315pub struct AvgPriceEvent {
316 #[serde(rename = "e")]
318 pub event_type: String,
319 #[serde(rename = "E")]
321 pub event_time: u64,
322 #[serde(rename = "s")]
324 pub symbol: String,
325 #[serde(rename = "i")]
327 pub interval: String,
328 #[serde(rename = "w", with = "rust_decimal::serde::str")]
330 pub price: Decimal,
331 #[serde(rename = "T")]
333 pub last_trade_time: u64,
334}
335
336#[derive(Debug, Clone, PartialEq, Eq)]
338#[non_exhaustive]
339pub enum Payload {
340 AggTrade(AggTradeEvent),
342 Trade(TradeEvent),
344 Kline(KlineEvent),
346 MiniTicker(MiniTickerEvent),
348 MiniTickers(Vec<MiniTickerEvent>),
351 Ticker(TickerEvent),
353 BookTicker(BookTickerEvent),
355 PartialDepth(PartialDepthEvent),
357 AvgPrice(AvgPriceEvent),
359 Unknown {
363 event_type: String,
365 raw: String,
367 },
368}
369
370impl Serialize for Payload {
371 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
372 match self {
373 Self::AggTrade(event) => event.serialize(serializer),
374 Self::Trade(event) => event.serialize(serializer),
375 Self::Kline(event) => event.serialize(serializer),
376 Self::MiniTicker(event) => event.serialize(serializer),
377 Self::MiniTickers(rows) => rows.serialize(serializer),
378 Self::Ticker(event) => event.serialize(serializer),
379 Self::BookTicker(event) => event.serialize(serializer),
380 Self::PartialDepth(event) => event.serialize(serializer),
381 Self::AvgPrice(event) => event.serialize(serializer),
382 Self::Unknown { raw, .. } => serde_json::from_str::<Value>(raw)
383 .map_err(serde::ser::Error::custom)?
384 .serialize(serializer),
385 }
386 }
387}
388
389#[derive(Debug, Clone, PartialEq, Eq)]
394#[non_exhaustive]
395pub struct Update {
396 pub stream: StreamName,
398 pub payload: Payload,
400}
401
402impl Serialize for Update {
403 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
404 use serde::ser::SerializeMap;
405 let mut map = serializer.serialize_map(Some(2))?;
406 map.serialize_entry("stream", &self.stream.to_string())?;
407 map.serialize_entry("data", &self.payload)?;
408 map.end()
409 }
410}
411
412#[derive(Deserialize)]
413struct Envelope {
414 stream: String,
415 data: Value,
416}
417
418impl Update {
419 pub fn from_json(text: &str) -> Result<Self, SpotWsError> {
425 let envelope: Envelope =
426 serde_json::from_str(text).map_err(|err| frame_error("", text, err))?;
427 let stream: StreamName = envelope
428 .stream
429 .parse()
430 .map_err(|err| frame_error(&envelope.stream, text, err))?;
431 let payload = decode(&stream, envelope.data)
432 .map_err(|err| frame_error(&envelope.stream, text, err))?;
433 Ok(Self { stream, payload })
434 }
435}
436
437fn frame_error(stream: &str, raw: &str, reason: impl fmt::Display) -> SpotWsError {
438 SpotWsError::Frame {
439 stream: stream.to_owned(),
440 raw: raw.to_owned(),
441 reason: reason.to_string(),
442 }
443}
444
445fn decode(stream: &StreamName, data: Value) -> Result<Payload, serde_json::Error> {
446 let expected = match stream {
450 StreamName::AllMiniTickers => {
451 return serde_json::from_value(data).map(Payload::MiniTickers)
452 }
453 StreamName::BookTicker(_) => return serde_json::from_value(data).map(Payload::BookTicker),
454 StreamName::PartialDepth(symbol, ..) => {
455 let mut event: PartialDepthEvent = serde_json::from_value(data)?;
456 event.symbol = symbol.to_string();
457 return Ok(Payload::PartialDepth(event));
458 }
459 StreamName::AggTrade(_) => "aggTrade",
460 StreamName::Trade(_) => "trade",
461 StreamName::Kline(..) => "kline",
462 StreamName::MiniTicker(_) => "24hrMiniTicker",
463 StreamName::Ticker(_) => "24hrTicker",
464 StreamName::AvgPrice(_) => "avgPrice",
465 };
466 let event_type = data.get("e").and_then(Value::as_str).unwrap_or_default();
467 if event_type != expected {
468 return Ok(Payload::Unknown {
469 event_type: event_type.to_owned(),
470 raw: data.to_string(),
471 });
472 }
473 Ok(match stream {
474 StreamName::AggTrade(_) => Payload::AggTrade(serde_json::from_value(data)?),
475 StreamName::Trade(_) => Payload::Trade(serde_json::from_value(data)?),
476 StreamName::Kline(..) => Payload::Kline(serde_json::from_value(data)?),
477 StreamName::MiniTicker(_) => Payload::MiniTicker(serde_json::from_value(data)?),
478 StreamName::Ticker(_) => Payload::Ticker(serde_json::from_value(data)?),
479 StreamName::AvgPrice(_) => Payload::AvgPrice(serde_json::from_value(data)?),
480 StreamName::AllMiniTickers | StreamName::BookTicker(_) | StreamName::PartialDepth(..) => {
481 unreachable!("returned above")
482 }
483 })
484}
485
486#[cfg(test)]
487mod tests {
488 use super::*;
489 use crate::spot::ws::fixtures;
490 use serde_json::json;
491
492 fn wire(frame: &str) -> Value {
493 serde_json::from_str(frame).unwrap()
494 }
495
496 fn data(frame: &str) -> Value {
497 wire(frame)["data"].clone()
498 }
499
500 fn dec(row: &Value, key: &str) -> Decimal {
501 row[key]
502 .as_str()
503 .unwrap_or_else(|| panic!("{key} is not a string"))
504 .parse()
505 .unwrap()
506 }
507
508 fn int(row: &Value, key: &str) -> u64 {
509 row[key]
510 .as_u64()
511 .unwrap_or_else(|| panic!("{key} is not a u64"))
512 }
513
514 fn signed(row: &Value, key: &str) -> i64 {
515 row[key]
516 .as_i64()
517 .unwrap_or_else(|| panic!("{key} is not an i64"))
518 }
519
520 fn text(row: &Value, key: &str) -> String {
521 row[key]
522 .as_str()
523 .unwrap_or_else(|| panic!("{key} is not a string"))
524 .to_owned()
525 }
526
527 fn payload(frame: &str) -> Payload {
528 Update::from_json(frame).unwrap().payload
529 }
530
531 fn on_stream(frame: &str, stream: &str) -> String {
533 let mut frame = wire(frame);
534 frame["stream"] = json!(stream);
535 frame.to_string()
536 }
537
538 #[test]
539 fn every_captured_frame_decodes_as_its_stream() {
540 for (name, frame) in fixtures::ALL {
541 let update = Update::from_json(frame).unwrap_or_else(|e| panic!("{name}: {e}"));
542 assert!(
543 !matches!(update.payload, Payload::Unknown { .. }),
544 "{name} decoded as Unknown"
545 );
546 }
547 }
548
549 #[test]
550 fn each_frame_lands_in_the_payload_its_stream_names() {
551 assert!(matches!(payload(fixtures::AGG_TRADE), Payload::AggTrade(_)));
552 assert!(matches!(payload(fixtures::TRADE), Payload::Trade(_)));
553 assert!(matches!(payload(fixtures::KLINE), Payload::Kline(_)));
554 assert!(matches!(
555 payload(fixtures::MINI_TICKER),
556 Payload::MiniTicker(_)
557 ));
558 assert!(matches!(
559 payload(fixtures::ALL_MINI_TICKERS),
560 Payload::MiniTickers(_)
561 ));
562 assert!(matches!(payload(fixtures::TICKER), Payload::Ticker(_)));
563 assert!(matches!(
564 payload(fixtures::BOOK_TICKER),
565 Payload::BookTicker(_)
566 ));
567 assert!(matches!(
568 payload(fixtures::PARTIAL_DEPTH),
569 Payload::PartialDepth(_)
570 ));
571 assert!(matches!(
572 payload(fixtures::PARTIAL_DEPTH_100MS),
573 Payload::PartialDepth(_)
574 ));
575 assert!(matches!(payload(fixtures::AVG_PRICE), Payload::AvgPrice(_)));
576 assert!(matches!(
577 payload(fixtures::NON_ASCII_MINI_TICKER),
578 Payload::MiniTicker(_)
579 ));
580 }
581
582 #[test]
588 fn each_field_holds_the_value_of_its_own_key() {
589 fn mini(e: &MiniTickerEvent, row: &Value) {
590 assert_eq!(
591 [
592 e.close_price,
593 e.open_price,
594 e.high_price,
595 e.low_price,
596 e.volume,
597 e.quote_volume,
598 ],
599 ["c", "o", "h", "l", "v", "q"].map(|k| dec(row, k)),
600 "{}",
601 e.symbol
602 );
603 assert_eq!(e.event_time, int(row, "E"), "{}", e.symbol);
604 assert_eq!(e.symbol, text(row, "s"));
605 assert_eq!(e.event_type, text(row, "e"));
606 }
607
608 let Payload::MiniTickers(rows) = payload(fixtures::ALL_MINI_TICKERS) else {
609 panic!("not MiniTickers");
610 };
611 assert_eq!(rows.len(), 2);
612 for (e, row) in rows
613 .iter()
614 .zip(data(fixtures::ALL_MINI_TICKERS).as_array().unwrap())
615 {
616 mini(e, row);
617 }
618 for frame in [fixtures::MINI_TICKER, fixtures::NON_ASCII_MINI_TICKER] {
619 let Payload::MiniTicker(e) = payload(frame) else {
620 panic!("not MiniTicker");
621 };
622 mini(&e, &data(frame));
623 }
624
625 let row = data(fixtures::TICKER);
626 let Payload::Ticker(e) = payload(fixtures::TICKER) else {
627 panic!("not Ticker");
628 };
629 assert_eq!(
630 [
631 e.price_change,
632 e.price_change_percent,
633 e.weighted_avg_price,
634 e.prev_close_price,
635 e.last_price,
636 e.last_quantity,
637 e.bid_price,
638 e.bid_quantity,
639 e.ask_price,
640 e.ask_quantity,
641 e.open_price,
642 e.high_price,
643 e.low_price,
644 e.volume,
645 e.quote_volume,
646 ],
647 ["p", "P", "w", "x", "c", "Q", "b", "B", "a", "A", "o", "h", "l", "v", "q"]
648 .map(|k| dec(&row, k))
649 );
650 assert_eq!(e.symbol, text(&row, "s"));
651 assert_eq!(e.event_type, text(&row, "e"));
652 assert_eq!(
653 [e.event_time, e.open_time, e.close_time, e.trade_count],
654 ["E", "O", "C", "n"].map(|k| int(&row, k))
655 );
656 assert_eq!(
657 [e.first_trade_id, e.last_trade_id],
658 ["F", "L"].map(|k| signed(&row, k))
659 );
660
661 let row = data(fixtures::AGG_TRADE);
662 let Payload::AggTrade(e) = payload(fixtures::AGG_TRADE) else {
663 panic!("not AggTrade");
664 };
665 assert_eq!([e.price, e.quantity], ["p", "q"].map(|k| dec(&row, k)));
666 assert_eq!(
667 [
668 e.event_time,
669 e.id,
670 e.first_trade_id,
671 e.last_trade_id,
672 e.trade_time
673 ],
674 ["E", "a", "f", "l", "T"].map(|k| int(&row, k))
675 );
676 assert_eq!(e.is_buyer_maker, row["m"].as_bool().unwrap());
677 assert_eq!(e.symbol, text(&row, "s"));
678 assert_eq!(e.event_type, text(&row, "e"));
679
680 let row = data(fixtures::TRADE);
681 let Payload::Trade(e) = payload(fixtures::TRADE) else {
682 panic!("not Trade");
683 };
684 assert_eq!([e.price, e.quantity], ["p", "q"].map(|k| dec(&row, k)));
685 assert_eq!(
686 [e.event_time, e.id, e.trade_time],
687 ["E", "t", "T"].map(|k| int(&row, k))
688 );
689 assert_eq!(e.is_buyer_maker, row["m"].as_bool().unwrap());
690 assert_eq!(e.symbol, text(&row, "s"));
691 assert_eq!(e.event_type, text(&row, "e"));
692
693 let event = data(fixtures::KLINE);
694 let row = event["k"].clone();
695 let Payload::Kline(e) = payload(fixtures::KLINE) else {
696 panic!("not Kline");
697 };
698 let k = &e.kline;
699 assert_eq!(
700 [
701 k.open,
702 k.close,
703 k.high,
704 k.low,
705 k.volume,
706 k.quote_volume,
707 k.taker_buy_base_volume,
708 k.taker_buy_quote_volume,
709 ],
710 ["o", "c", "h", "l", "v", "q", "V", "Q"].map(|key| dec(&row, key))
711 );
712 assert_eq!(
713 [k.open_time, k.close_time, k.trade_count],
714 ["t", "T", "n"].map(|key| int(&row, key))
715 );
716 assert_eq!(
717 [k.first_trade_id, k.last_trade_id],
718 ["f", "L"].map(|key| signed(&row, key))
719 );
720 assert_eq!(k.interval.as_str(), text(&row, "i"));
721 assert_eq!(k.is_closed, row["x"].as_bool().unwrap());
722 assert_eq!(k.symbol, text(&row, "s"));
723 assert_eq!(e.symbol, text(&event, "s"));
724 assert_eq!(e.event_type, text(&event, "e"));
725
726 let row = data(fixtures::BOOK_TICKER);
727 let Payload::BookTicker(e) = payload(fixtures::BOOK_TICKER) else {
728 panic!("not BookTicker");
729 };
730 assert_eq!(
731 [e.bid_price, e.bid_quantity, e.ask_price, e.ask_quantity],
732 ["b", "B", "a", "A"].map(|k| dec(&row, k))
733 );
734 assert_eq!(e.update_id, int(&row, "u"));
735 assert_eq!(e.symbol, text(&row, "s"));
736
737 let row = data(fixtures::AVG_PRICE);
738 let Payload::AvgPrice(e) = payload(fixtures::AVG_PRICE) else {
739 panic!("not AvgPrice");
740 };
741 assert_eq!(e.price, dec(&row, "w"));
742 assert_eq!(
743 [e.event_time, e.last_trade_time],
744 ["E", "T"].map(|k| int(&row, k))
745 );
746 assert_eq!(e.interval, text(&row, "i"));
747 assert_eq!(e.symbol, text(&row, "s"));
748 assert_eq!(e.event_type, text(&row, "e"));
749 }
750
751 #[test]
752 fn a_partial_depth_frame_takes_its_symbol_from_the_envelope() {
753 for (frame, bids) in [
755 (fixtures::PARTIAL_DEPTH, 5),
756 (fixtures::PARTIAL_DEPTH_100MS, 3),
757 ] {
758 let row = data(frame);
759 assert_eq!(row.as_object().unwrap().len(), 3);
760 let update = Update::from_json(frame).unwrap();
761 let Payload::PartialDepth(e) = update.payload else {
762 panic!("not PartialDepth");
763 };
764 assert_eq!(e.symbol, "BTCUSDT");
765 assert_eq!(e.last_update_id, int(&row, "lastUpdateId"));
766 assert_eq!(e.bids.len(), bids);
767 assert_eq!(e.asks.len(), bids);
768 for (side, key) in [(&e.bids, "bids"), (&e.asks, "asks")] {
769 let sent: Vec<(Decimal, Decimal)> = row[key]
770 .as_array()
771 .unwrap()
772 .iter()
773 .map(|level| {
774 (
775 level[0].as_str().unwrap().parse().unwrap(),
776 level[1].as_str().unwrap().parse().unwrap(),
777 )
778 })
779 .collect();
780 let decoded: Vec<(Decimal, Decimal)> =
781 side.iter().map(|l| (l.price, l.quantity)).collect();
782 assert_eq!(decoded, sent, "{key}");
783 }
784 }
785
786 let ethereum = on_stream(fixtures::PARTIAL_DEPTH, "ethusdt@depth5");
789 let Payload::PartialDepth(e) = Update::from_json(ðereum).unwrap().payload else {
790 panic!("not PartialDepth");
791 };
792 assert_eq!(e.symbol, "ETHUSDT");
793 let chinese = on_stream(fixtures::PARTIAL_DEPTH, "币安人生usdt@depth5");
794 let Payload::PartialDepth(e) = Update::from_json(&chinese).unwrap().payload else {
795 panic!("not PartialDepth");
796 };
797 assert_eq!(e.symbol, "币安人生USDT");
798 }
799
800 #[test]
801 fn a_book_ticker_frame_has_no_event_type_and_decodes() {
802 assert!(data(fixtures::BOOK_TICKER).get("e").is_none());
803 assert!(matches!(
804 payload(fixtures::BOOK_TICKER),
805 Payload::BookTicker(_)
806 ));
807 }
808
809 #[test]
810 fn the_non_ascii_symbol_keeps_its_characters_in_the_name_and_the_payload() {
811 let update = Update::from_json(fixtures::NON_ASCII_MINI_TICKER).unwrap();
812 let StreamName::MiniTicker(symbol) = &update.stream else {
813 panic!("not MiniTicker: {:?}", update.stream);
814 };
815 assert!(!symbol.as_str().is_ascii());
816 let Payload::MiniTicker(e) = &update.payload else {
817 panic!("not MiniTicker");
818 };
819 assert_eq!(
820 e.symbol.to_ascii_lowercase(),
821 symbol.as_str().to_ascii_lowercase()
822 );
823 assert_eq!(
824 wire(fixtures::NON_ASCII_MINI_TICKER)["stream"],
825 update.stream.to_string()
826 );
827 }
828
829 #[test]
830 fn an_object_whose_event_type_is_not_its_streams_is_kept_whole() {
831 let crossed = on_stream(fixtures::AGG_TRADE, "btcusdt@trade");
834 let update = Update::from_json(&crossed).unwrap();
835 assert_eq!(update.stream.to_string(), "btcusdt@trade");
836 let Payload::Unknown { event_type, raw } = &update.payload else {
837 panic!("not Unknown: {:?}", update.payload);
838 };
839 assert_eq!(event_type, "aggTrade");
840 assert_eq!(
841 serde_json::from_str::<Value>(raw).unwrap(),
842 data(fixtures::AGG_TRADE)
843 );
844
845 for (e, expected) in [(json!("aggTradeV2"), "aggTradeV2"), (Value::Null, "")] {
847 let mut frame = wire(fixtures::AGG_TRADE);
848 match e {
849 Value::Null => {
850 frame["data"].as_object_mut().unwrap().remove("e");
851 }
852 e => frame["data"]["e"] = e,
853 }
854 let update = Update::from_json(&frame.to_string()).unwrap();
855 assert!(
856 matches!(&update.payload, Payload::Unknown { event_type, .. } if event_type == expected),
857 "{:?}",
858 update.payload
859 );
860 assert_eq!(serde_json::to_value(&update).unwrap(), frame);
862 }
863 }
864
865 #[test]
866 fn a_broken_frame_is_a_frame_error_that_names_its_stream() {
867 let mut broken = wire(fixtures::AGG_TRADE);
868 broken["data"]["p"] = json!("x");
869 let err = Update::from_json(&broken.to_string()).unwrap_err();
870 assert!(
871 matches!(&err, SpotWsError::Frame { stream, .. } if stream == "btcusdt@aggTrade"),
872 "{err:?}"
873 );
874
875 let mut object = wire(fixtures::ALL_MINI_TICKERS);
878 object["data"] = json!({});
879 assert!(matches!(
880 Update::from_json(&object.to_string()),
881 Err(SpotWsError::Frame { .. })
882 ));
883 let mut thin = wire(fixtures::BOOK_TICKER);
884 thin["data"].as_object_mut().unwrap().remove("u");
885 assert!(matches!(
886 Update::from_json(&thin.to_string()),
887 Err(SpotWsError::Frame { .. })
888 ));
889 let mut empty = wire(fixtures::PARTIAL_DEPTH);
890 empty["data"] = json!({});
891 assert!(matches!(
892 Update::from_json(&empty.to_string()),
893 Err(SpotWsError::Frame { .. })
894 ));
895 }
896
897 #[test]
898 fn a_stream_this_crate_does_not_build_is_a_frame_error() {
899 for stream in [
900 "btcusdt@forceOrder",
901 "BTCUSDT@trade",
902 "!ticker@arr",
903 "btcusdt@depth5@250ms",
904 ] {
905 let err = Update::from_json(&on_stream(fixtures::TRADE, stream)).unwrap_err();
906 assert!(
907 matches!(&err, SpotWsError::Frame { stream: s, .. } if s == stream),
908 "{stream}: {err:?}"
909 );
910 }
911 assert!(matches!(
912 Update::from_json("not json"),
913 Err(SpotWsError::Frame { stream, .. }) if stream.is_empty()
914 ));
915 assert!(matches!(
916 Update::from_json(r#"{"result":null,"id":1}"#),
917 Err(SpotWsError::Frame { .. })
918 ));
919 }
920
921 #[test]
922 fn an_update_serialises_back_to_its_envelope() {
923 for frame in [
926 fixtures::BOOK_TICKER,
927 fixtures::PARTIAL_DEPTH,
928 fixtures::PARTIAL_DEPTH_100MS,
929 fixtures::MINI_TICKER,
930 fixtures::ALL_MINI_TICKERS,
931 fixtures::TICKER,
932 fixtures::AVG_PRICE,
933 ] {
934 let update = Update::from_json(frame).unwrap();
935 assert_eq!(serde_json::to_value(&update).unwrap(), wire(frame));
936 }
937 }
938}