1use crate::{
2 exchange::Connector, instrument::InstrumentData, subscription::candle::CandleInterval,
3};
4use derive_more::Display;
5use fnv::FnvHashMap;
6use rustrade_instrument::{
7 Keyed,
8 asset::name::AssetNameInternal,
9 exchange::ExchangeId,
10 instrument::market_data::{MarketDataInstrument, kind::MarketDataInstrumentKind},
11};
12use rustrade_integration::{
13 Validator, error::SocketError, protocol::websocket::WsMessage, subscription::SubscriptionId,
14};
15use serde::{Deserialize, Serialize};
16use smol_str::{ToSmolStr, format_smolstr};
17use std::{borrow::Borrow, fmt::Debug, hash::Hash};
18
19pub mod book;
21
22pub mod candle;
24
25pub mod greeks;
27
28pub mod liquidation;
30
31pub mod quote;
33
34pub mod trade;
36
37pub trait SubscriptionKind
39where
40 Self: Debug + Clone,
41{
42 type Event: Debug;
43 fn as_str(&self) -> &'static str;
44}
45
46#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize)]
49pub struct Subscription<Exchange = ExchangeId, Inst = MarketDataInstrument, Kind = SubKind> {
50 pub exchange: Exchange,
51 #[serde(flatten)]
52 pub instrument: Inst,
53 #[serde(alias = "type")]
54 pub kind: Kind,
55}
56
57pub fn display_subscriptions_without_exchange<Exchange, Instrument, Kind>(
58 subscriptions: &[Subscription<Exchange, Instrument, Kind>],
59) -> String
60where
61 Instrument: std::fmt::Display,
62 Kind: std::fmt::Display,
63{
64 subscriptions
65 .iter()
66 .map(
67 |Subscription {
68 exchange: _,
69 instrument,
70 kind,
71 }| { format_smolstr!("({instrument}, {kind})") },
72 )
73 .collect::<Vec<_>>()
74 .join(",")
75}
76
77impl<Exchange, Instrument, Kind> std::fmt::Display for Subscription<Exchange, Instrument, Kind>
78where
79 Exchange: std::fmt::Display,
80 Instrument: std::fmt::Display,
81 Kind: std::fmt::Display,
82{
83 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84 write!(f, "({}|{}|{})", self.exchange, self.kind, self.instrument)
85 }
86}
87
88#[derive(
89 Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Display, Deserialize, Serialize,
90)]
91pub enum SubKind {
92 PublicTrades,
93 OrderBooksL1,
94 OrderBooksL2,
95 OrderBooksL3,
96 Liquidations,
97 #[display("candles")]
105 Candles {
106 interval: CandleInterval,
107 },
108 Quotes,
111}
112
113impl<Exchange, S, Kind> From<(Exchange, S, S, MarketDataInstrumentKind, Kind)>
114 for Subscription<Exchange, MarketDataInstrument, Kind>
115where
116 S: Into<AssetNameInternal>,
117{
118 fn from(
119 (exchange, base, quote, instrument_kind, kind): (
120 Exchange,
121 S,
122 S,
123 MarketDataInstrumentKind,
124 Kind,
125 ),
126 ) -> Self {
127 Self::new(exchange, (base, quote, instrument_kind), kind)
128 }
129}
130
131impl<InstrumentKey, Exchange, S, Kind>
132 From<(
133 InstrumentKey,
134 Exchange,
135 S,
136 S,
137 MarketDataInstrumentKind,
138 Kind,
139 )> for Subscription<Exchange, Keyed<InstrumentKey, MarketDataInstrument>, Kind>
140where
141 S: Into<AssetNameInternal>,
142{
143 fn from(
144 (instrument_id, exchange, base, quote, instrument_kind, kind): (
145 InstrumentKey,
146 Exchange,
147 S,
148 S,
149 MarketDataInstrumentKind,
150 Kind,
151 ),
152 ) -> Self {
153 let instrument = Keyed::new(instrument_id, (base, quote, instrument_kind).into());
154
155 Self::new(exchange, instrument, kind)
156 }
157}
158
159impl<Exchange, I, Instrument, Kind> From<(Exchange, I, Kind)>
160 for Subscription<Exchange, Instrument, Kind>
161where
162 I: Into<Instrument>,
163{
164 fn from((exchange, instrument, kind): (Exchange, I, Kind)) -> Self {
165 Self::new(exchange, instrument, kind)
166 }
167}
168
169impl<Instrument, Exchange, Kind> Subscription<Exchange, Instrument, Kind> {
170 pub fn new<I>(exchange: Exchange, instrument: I, kind: Kind) -> Self
172 where
173 I: Into<Instrument>,
174 {
175 Self {
176 exchange,
177 instrument: instrument.into(),
178 kind,
179 }
180 }
181}
182
183impl<Exchange, Instrument, Kind> Validator for Subscription<Exchange, Instrument, Kind>
184where
185 Exchange: Connector,
186 Instrument: InstrumentData,
187{
188 type Error = SocketError;
189
190 fn validate(self) -> Result<Self, Self::Error>
191 where
192 Self: Sized,
193 {
194 if exchange_supports_instrument_kind(Exchange::ID, self.instrument.kind()) {
196 Ok(self)
197 } else {
198 Err(SocketError::Unsupported {
199 entity: Exchange::ID.to_string(),
200 item: self.instrument.kind().to_string(),
201 })
202 }
203 }
204}
205
206#[allow(clippy::match_like_matches_macro)]
213pub fn exchange_supports_instrument_kind(
214 exchange: ExchangeId,
215 instrument_kind: &MarketDataInstrumentKind,
216) -> bool {
217 use rustrade_instrument::{
218 exchange::ExchangeId::*, instrument::market_data::kind::MarketDataInstrumentKind::*,
219 };
220
221 match (exchange, instrument_kind) {
222 (
224 BinanceFuturesUsd | Bitmex | BybitPerpetualsUsd | GateioPerpetualsUsd
225 | GateioPerpetualsBtc,
226 Spot,
227 ) => false,
228 (LseFx | LseCrypto | LseEquities, Spot) => true,
229 (LseCfd | LseFutures, Spot) => false,
230 (_, Spot) => true,
236
237 (GateioFuturesUsd | GateioFuturesBtc | Okx, Future { .. }) => true,
239 (_, Future { .. }) => false,
240
241 (
243 BinanceFuturesUsd | Bitmex | Okx | BybitPerpetualsUsd | GateioPerpetualsUsd
244 | GateioPerpetualsBtc | HyperliquidPerp,
245 Perpetual,
246 ) => true,
247 (_, Perpetual) => false,
248
249 (GateioOptions | Okx, Option { .. }) => true,
251 (_, Option { .. }) => false,
252
253 (LseCfd | LseFutures, Cfd) => true,
255 (_, Cfd) => false,
256 }
257}
258
259impl<Instrument> Validator for Subscription<ExchangeId, Instrument, SubKind>
260where
261 Instrument: InstrumentData,
262{
263 type Error = SocketError;
264
265 fn validate(self) -> Result<Self, Self::Error>
266 where
267 Self: Sized,
268 {
269 if exchange_supports_instrument_kind_sub_kind(
271 &self.exchange,
272 self.instrument.kind(),
273 self.kind,
274 ) {
275 Ok(self)
276 } else {
277 Err(SocketError::Unsupported {
278 entity: self.exchange.to_string(),
279 item: format!("({}, {})", self.instrument.kind(), self.kind),
280 })
281 }
282 }
283}
284
285pub fn exchange_supports_instrument_kind_sub_kind(
292 exchange_id: &ExchangeId,
293 instrument_kind: &MarketDataInstrumentKind,
294 sub_kind: SubKind,
295) -> bool {
296 use ExchangeId::*;
297 use MarketDataInstrumentKind::*;
298 use SubKind::*;
299
300 match (exchange_id, instrument_kind, sub_kind) {
301 (BinanceSpot, Spot, PublicTrades | OrderBooksL1 | OrderBooksL2) => true,
302 (BinanceSpot, Spot, Candles { interval }) => {
303 crate::exchange::binance::supports_candle_interval(interval)
304 }
305 (
306 BinanceFuturesUsd,
307 Perpetual,
308 PublicTrades | OrderBooksL1 | OrderBooksL2 | Liquidations,
309 ) => true,
310 (BinanceFuturesUsd, Perpetual, Candles { interval }) => {
311 crate::exchange::binance::supports_candle_interval(interval)
312 }
313 (Bitfinex, Spot, PublicTrades) => true,
314 (Bitmex, Perpetual, PublicTrades) => true,
315 (BybitSpot, Spot, PublicTrades | OrderBooksL1 | OrderBooksL2) => true,
316 (BybitPerpetualsUsd, Perpetual, PublicTrades | OrderBooksL1 | OrderBooksL2) => true,
317 (Coinbase, Spot, PublicTrades) => true,
318 (GateioSpot, Spot, PublicTrades) => true,
319 (GateioFuturesUsd, Future { .. }, PublicTrades) => true,
320 (GateioFuturesBtc, Future { .. }, PublicTrades) => true,
321 (GateioPerpetualsUsd, Perpetual, PublicTrades) => true,
322 (GateioPerpetualsBtc, Perpetual, PublicTrades) => true,
323 (GateioOptions, Option { .. }, PublicTrades) => true,
324 (Kraken, Spot, PublicTrades | OrderBooksL1) => true,
325 (Okx, Spot | Future { .. } | Perpetual | Option { .. }, PublicTrades) => true,
326 (HyperliquidPerp, Perpetual, PublicTrades | OrderBooksL2) => true,
327
328 (LseFx | LseCrypto | LseEquities, Spot, PublicTrades | OrderBooksL1) => true,
350 (LseCfd | LseFutures, Cfd, PublicTrades | OrderBooksL1) => true,
351 (_, _, _) => false,
356 }
357}
358
359#[derive(Clone, Eq, PartialEq, Debug)]
362pub struct SubscriptionMeta<InstrumentKey> {
363 pub instrument_map: Map<InstrumentKey>,
366 pub ws_subscriptions: Vec<WsMessage>,
368}
369
370#[derive(Clone, Eq, PartialEq, Debug, Deserialize, Serialize)]
375pub struct Map<T>(pub FnvHashMap<SubscriptionId, T>);
376
377impl<T> FromIterator<(SubscriptionId, T)> for Map<T> {
378 fn from_iter<Iter>(iter: Iter) -> Self
379 where
380 Iter: IntoIterator<Item = (SubscriptionId, T)>,
381 {
382 Self(iter.into_iter().collect::<FnvHashMap<SubscriptionId, T>>())
383 }
384}
385
386impl<T> Map<T> {
387 pub fn find<SubId>(&self, id: &SubId) -> Result<&T, SocketError>
389 where
390 SubscriptionId: Borrow<SubId>,
391 SubId: AsRef<str> + Hash + Eq + ?Sized,
392 {
393 self.0
394 .get(id)
395 .ok_or_else(|| SocketError::Unidentifiable(SubscriptionId(id.as_ref().to_smolstr())))
396 }
397
398 pub fn find_mut<SubId>(&mut self, id: &SubId) -> Result<&mut T, SocketError>
400 where
401 SubscriptionId: Borrow<SubId>,
402 SubId: AsRef<str> + Hash + Eq + ?Sized,
403 {
404 self.0
405 .get_mut(id)
406 .ok_or_else(|| SocketError::Unidentifiable(SubscriptionId(id.as_ref().to_smolstr())))
407 }
408}
409
410#[cfg(test)]
411#[allow(clippy::unwrap_used)] mod tests {
413 use super::*;
414
415 mod supports {
416 use super::*;
417 use rustrade_instrument::exchange::ExchangeId;
418
419 const LSE: [(ExchangeId, MarketDataInstrumentKind); 5] = [
422 (ExchangeId::LseFx, MarketDataInstrumentKind::Spot),
423 (ExchangeId::LseCrypto, MarketDataInstrumentKind::Spot),
424 (ExchangeId::LseEquities, MarketDataInstrumentKind::Spot),
425 (ExchangeId::LseFutures, MarketDataInstrumentKind::Cfd),
426 (ExchangeId::LseCfd, MarketDataInstrumentKind::Cfd),
427 ];
428
429 #[test]
430 fn test_lse_supports_exactly_its_own_kind() {
431 for (exchange, supported) in LSE {
435 assert!(
436 exchange_supports_instrument_kind(exchange, &supported),
437 "{exchange:?} should support {supported:?}"
438 );
439
440 for denied in [
441 MarketDataInstrumentKind::Spot,
442 MarketDataInstrumentKind::Cfd,
443 MarketDataInstrumentKind::Perpetual,
444 ] {
445 if denied == supported {
446 continue;
447 }
448 assert!(
449 !exchange_supports_instrument_kind(exchange, &denied),
450 "{exchange:?} should not support {denied:?}"
451 );
452 }
453 }
454 }
455
456 #[test]
457 fn test_cfd_denied_for_non_cfd_exchanges() {
458 for exchange in [
459 ExchangeId::BinanceSpot,
460 ExchangeId::Coinbase,
461 ExchangeId::Okx,
462 ExchangeId::GateioOptions,
463 ExchangeId::Mock,
464 ] {
465 assert!(
466 !exchange_supports_instrument_kind(exchange, &MarketDataInstrumentKind::Cfd),
467 "{exchange:?} should not support Cfd"
468 );
469 }
470 }
471
472 #[test]
473 fn test_lse_supports_both_tick_derived_sub_kinds_for_its_own_kind_only() {
474 for (exchange, supported) in LSE {
478 for sub_kind in [SubKind::PublicTrades, SubKind::OrderBooksL1] {
479 assert!(
480 exchange_supports_instrument_kind_sub_kind(&exchange, &supported, sub_kind),
481 "{exchange:?} should support ({supported:?}, {sub_kind})"
482 );
483
484 for denied in [
485 MarketDataInstrumentKind::Spot,
486 MarketDataInstrumentKind::Cfd,
487 MarketDataInstrumentKind::Perpetual,
488 ] {
489 if denied == supported {
490 continue;
491 }
492 assert!(
493 !exchange_supports_instrument_kind_sub_kind(
494 &exchange, &denied, sub_kind
495 ),
496 "{exchange:?} should not support ({denied:?}, {sub_kind})"
497 );
498 }
499 }
500 }
501 }
502
503 #[test]
504 fn test_lse_serves_no_candle_stream_at_any_interval() {
505 for (exchange, kind) in LSE {
509 for interval in CandleInterval::ALL {
510 assert!(
511 !exchange_supports_instrument_kind_sub_kind(
512 &exchange,
513 &kind,
514 SubKind::Candles { interval },
515 ),
516 "{exchange:?} serves no candle stream, including {interval:?}"
517 );
518 }
519 }
520 }
521
522 #[test]
523 fn test_lse_denies_sub_kinds_its_tick_cannot_serve() {
524 for (exchange, kind) in LSE {
528 for sub_kind in [
529 SubKind::OrderBooksL2,
530 SubKind::OrderBooksL3,
531 SubKind::Liquidations,
532 SubKind::Quotes,
533 ] {
534 assert!(
535 !exchange_supports_instrument_kind_sub_kind(&exchange, &kind, sub_kind),
536 "{exchange:?} should not support ({kind:?}, {sub_kind})"
537 );
538 }
539 }
540 }
541 }
542
543 mod subscription {
544 use super::*;
545 use crate::{
546 exchange::{coinbase::Coinbase, okx::Okx},
547 subscription::trade::PublicTrades,
548 };
549 use rustrade_instrument::instrument::market_data::MarketDataInstrument;
550
551 mod de {
552 use super::*;
553 use crate::{
554 exchange::{
555 binance::{futures::BinanceFuturesUsd, spot::BinanceSpot},
556 gateio::perpetual::GateioPerpetualsUsd,
557 okx::Okx,
558 },
559 subscription::{book::OrderBooksL2, trade::PublicTrades},
560 };
561 use rustrade_instrument::instrument::market_data::MarketDataInstrument;
562
563 #[test]
564 fn test_subscription_okx_spot_public_trades() {
565 let input = r#"
566 {
567 "exchange": "okx",
568 "base": "btc",
569 "quote": "usdt",
570 "instrument_kind": "spot",
571 "kind": "public_trades"
572 }
573 "#;
574
575 serde_json::from_str::<Subscription<Okx, MarketDataInstrument, PublicTrades>>(
576 input,
577 )
578 .unwrap();
579 }
580
581 #[test]
582 fn test_subscription_binance_spot_public_trades() {
583 let input = r#"
584 {
585 "exchange": "binance_spot",
586 "base": "btc",
587 "quote": "usdt",
588 "instrument_kind": "spot",
589 "kind": "public_trades"
590 }
591 "#;
592
593 serde_json::from_str::<Subscription<BinanceSpot, MarketDataInstrument, PublicTrades>>(input)
594 .unwrap();
595 }
596
597 #[test]
598 fn test_subscription_binance_futures_usd_order_books_l2() {
599 let input = r#"
600 {
601 "exchange": "binance_futures_usd",
602 "base": "btc",
603 "quote": "usdt",
604 "instrument_kind": "perpetual",
605 "kind": "order_books_l2"
606 }
607 "#;
608
609 serde_json::from_str::<
610 Subscription<BinanceFuturesUsd, MarketDataInstrument, OrderBooksL2>,
611 >(input)
612 .unwrap();
613 }
614
615 #[test]
616 fn subscription_gateio_futures_usd_public_trades() {
617 let input = r#"
618 {
619 "exchange": "gateio_perpetuals_usd",
620 "base": "btc",
621 "quote": "usdt",
622 "instrument_kind": "perpetual",
623 "kind": "public_trades"
624 }
625 "#;
626
627 serde_json::from_str::<
628 Subscription<GateioPerpetualsUsd, MarketDataInstrument, PublicTrades>,
629 >(input)
630 .unwrap();
631 }
632 }
633
634 #[test]
635 fn test_validate_bitfinex_public_trades() {
636 struct TestCase {
637 input: Subscription<Coinbase, MarketDataInstrument, PublicTrades>,
638 expected:
639 Result<Subscription<Coinbase, MarketDataInstrument, PublicTrades>, SocketError>,
640 }
641
642 let tests = vec![
643 TestCase {
644 input: Subscription::from((
646 Coinbase,
647 "base",
648 "quote",
649 MarketDataInstrumentKind::Spot,
650 PublicTrades,
651 )),
652 expected: Ok(Subscription::from((
653 Coinbase,
654 "base",
655 "quote",
656 MarketDataInstrumentKind::Spot,
657 PublicTrades,
658 ))),
659 },
660 TestCase {
661 input: Subscription::from((
663 Coinbase,
664 "base",
665 "quote",
666 MarketDataInstrumentKind::Perpetual,
667 PublicTrades,
668 )),
669 expected: Err(SocketError::Unsupported {
670 entity: "".to_string(),
671 item: "".to_string(),
672 }),
673 },
674 ];
675
676 for (index, test) in tests.into_iter().enumerate() {
677 let actual = test.input.validate();
678 match (actual, test.expected) {
679 (Ok(actual), Ok(expected)) => {
680 assert_eq!(actual, expected, "TC{} failed", index)
681 }
682 (Err(_), Err(_)) => {
683 }
685 (actual, expected) => {
686 panic!(
688 "TC{index} failed because actual != expected. \nActual: {actual:?}\nExpected: {expected:?}\n"
689 );
690 }
691 }
692 }
693 }
694
695 #[test]
696 fn test_validate_okx_public_trades() {
697 struct TestCase {
698 input: Subscription<Okx, MarketDataInstrument, PublicTrades>,
699 expected:
700 Result<Subscription<Okx, MarketDataInstrument, PublicTrades>, SocketError>,
701 }
702
703 let tests = vec![
704 TestCase {
705 input: Subscription::from((
707 Okx,
708 "base",
709 "quote",
710 MarketDataInstrumentKind::Spot,
711 PublicTrades,
712 )),
713 expected: Ok(Subscription::from((
714 Okx,
715 "base",
716 "quote",
717 MarketDataInstrumentKind::Spot,
718 PublicTrades,
719 ))),
720 },
721 TestCase {
722 input: Subscription::from((
724 Okx,
725 "base",
726 "quote",
727 MarketDataInstrumentKind::Perpetual,
728 PublicTrades,
729 )),
730 expected: Ok(Subscription::from((
731 Okx,
732 "base",
733 "quote",
734 MarketDataInstrumentKind::Perpetual,
735 PublicTrades,
736 ))),
737 },
738 ];
739
740 for (index, test) in tests.into_iter().enumerate() {
741 let actual = test.input.validate();
742 match (actual, test.expected) {
743 (Ok(actual), Ok(expected)) => {
744 assert_eq!(actual, expected, "TC{} failed", index)
745 }
746 (Err(_), Err(_)) => {
747 }
749 (actual, expected) => {
750 panic!(
752 "TC{index} failed because actual != expected. \nActual: {actual:?}\nExpected: {expected:?}\n"
753 );
754 }
755 }
756 }
757 }
758 }
759
760 mod sub_kind {
761 use super::*;
762
763 #[test]
764 fn candles_variant_serde_round_trips() {
765 let kind = SubKind::Candles {
766 interval: CandleInterval::Min1,
767 };
768
769 let json = serde_json::to_string(&kind).unwrap();
770 let back = serde_json::from_str::<SubKind>(&json).unwrap();
771
772 assert_eq!(kind, back, "SubKind::Candles must serde round-trip");
773 }
774
775 #[test]
776 fn candles_variant_serde_shape_is_externally_tagged() {
777 let json = serde_json::to_string(&SubKind::Candles {
782 interval: CandleInterval::Min1,
783 })
784 .unwrap();
785 assert_eq!(json, r#"{"Candles":{"interval":"1m"}}"#);
786 }
787
788 #[test]
789 fn candles_display_tag_is_interval_independent() {
790 assert_eq!(
792 SubKind::Candles {
793 interval: CandleInterval::Sec1
794 }
795 .to_string(),
796 "candles"
797 );
798 assert_eq!(
799 SubKind::Candles {
800 interval: CandleInterval::Month1
801 }
802 .to_string(),
803 "candles"
804 );
805 }
806
807 #[test]
808 fn candles_supported_per_interval_on_both_binance_venues() {
809 for interval in CandleInterval::ALL {
813 let expected = crate::exchange::binance::supports_candle_interval(interval);
814 assert_eq!(
815 exchange_supports_instrument_kind_sub_kind(
816 &ExchangeId::BinanceSpot,
817 &MarketDataInstrumentKind::Spot,
818 SubKind::Candles { interval },
819 ),
820 expected,
821 "BinanceSpot/Spot Candles {interval:?}"
822 );
823 assert_eq!(
824 exchange_supports_instrument_kind_sub_kind(
825 &ExchangeId::BinanceFuturesUsd,
826 &MarketDataInstrumentKind::Perpetual,
827 SubKind::Candles { interval },
828 ),
829 expected,
830 "BinanceFuturesUsd/Perpetual Candles {interval:?}"
831 );
832 }
833 }
834
835 #[test]
836 fn candles_rejected_for_unsupported_venue_kind_pairing() {
837 assert!(
839 !exchange_supports_instrument_kind_sub_kind(
840 &ExchangeId::BinanceSpot,
841 &MarketDataInstrumentKind::Perpetual,
842 SubKind::Candles {
843 interval: CandleInterval::Min1,
844 },
845 ),
846 "BinanceSpot does not serve Perpetual candles"
847 );
848 }
849 }
850
851 mod instrument_map {
852 use super::*;
853 use rustrade_instrument::instrument::market_data::MarketDataInstrument;
854
855 #[test]
856 fn test_find_instrument() {
857 let ids = Map(FnvHashMap::from_iter([(
859 SubscriptionId::from("present"),
860 MarketDataInstrument::from(("base", "quote", MarketDataInstrumentKind::Spot)),
861 )]));
862
863 struct TestCase {
864 input: SubscriptionId,
865 expected: Result<MarketDataInstrument, SocketError>,
866 }
867
868 let cases = vec![
869 TestCase {
870 input: SubscriptionId::from("present"),
872 expected: Ok(MarketDataInstrument::from((
873 "base",
874 "quote",
875 MarketDataInstrumentKind::Spot,
876 ))),
877 },
878 TestCase {
879 input: SubscriptionId::from("not present"),
881 expected: Err(SocketError::Unidentifiable(SubscriptionId::from(
882 "not present",
883 ))),
884 },
885 ];
886
887 for (index, test) in cases.into_iter().enumerate() {
888 let actual = ids.find(&test.input);
889 match (actual, test.expected) {
890 (Ok(actual), Ok(expected)) => {
891 assert_eq!(*actual, expected, "TC{} failed", index)
892 }
893 (Err(_), Err(_)) => {
894 }
896 (actual, expected) => {
897 panic!(
899 "TC{index} failed because actual != expected. \nActual: {actual:?}\nExpected: {expected:?}\n"
900 );
901 }
902 }
903 }
904 }
905 }
906}