1use std::fmt::Display;
17
18use bytes::Bytes;
19use serde::{Deserialize, Serialize, de::Error as _};
20use ustr::Ustr;
21
22use super::switchboard::CLOSE_TOPIC;
23use crate::enums::SerializationEncoding;
24
25#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
27pub enum BusPayloadCategory {
28 MarketData,
29 BuiltIn,
30 Other,
31}
32
33#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
42pub enum BusPayloadType {
43 Custom(Ustr),
45 Instrument,
46 OrderBookDeltas,
47 OrderBookDepth,
48 QuoteTick,
49 TradeTick,
50 Bar,
51 MarkPriceUpdate,
52 IndexPriceUpdate,
53 FundingRateUpdate,
54 OptionGreeks,
55 AccountState,
56 OrderEvent,
57 PositionEvent,
58 PortfolioSnapshot,
59 SubscribeCommand,
60 UnsubscribeCommand,
61 TradingCommand,
62 GenerateExecutionMassStatus,
63 OrderStatusReport,
64 FillReport,
65 PositionStatusReport,
66 ExecutionMassStatus,
67 #[cfg(feature = "defi")]
68 Block,
69 #[cfg(feature = "defi")]
70 Pool,
71 #[cfg(feature = "defi")]
72 PoolLiquidityUpdate,
73 #[cfg(feature = "defi")]
74 PoolFeeCollect,
75 #[cfg(feature = "defi")]
76 PoolFlash,
77}
78
79impl BusPayloadType {
80 pub(crate) const PUBLISHED_TYPES: &'static [Self] = &[
81 Self::Instrument,
82 Self::OrderBookDeltas,
83 Self::OrderBookDepth,
84 Self::QuoteTick,
85 Self::TradeTick,
86 Self::Bar,
87 Self::MarkPriceUpdate,
88 Self::IndexPriceUpdate,
89 Self::FundingRateUpdate,
90 Self::OptionGreeks,
91 Self::AccountState,
92 Self::OrderEvent,
93 Self::PositionEvent,
94 Self::PortfolioSnapshot,
95 Self::SubscribeCommand,
96 Self::UnsubscribeCommand,
97 Self::TradingCommand,
98 Self::GenerateExecutionMassStatus,
99 Self::OrderStatusReport,
100 Self::FillReport,
101 Self::PositionStatusReport,
102 Self::ExecutionMassStatus,
103 #[cfg(feature = "defi")]
104 Self::Block,
105 #[cfg(feature = "defi")]
106 Self::Pool,
107 #[cfg(feature = "defi")]
108 Self::PoolLiquidityUpdate,
109 #[cfg(feature = "defi")]
110 Self::PoolFeeCollect,
111 #[cfg(feature = "defi")]
112 Self::PoolFlash,
113 ];
114
115 #[must_use]
120 pub fn as_str(&self) -> &str {
121 match self {
122 Self::Custom(type_name) => type_name.as_str(),
123 Self::Instrument => "InstrumentAny",
124 Self::OrderBookDeltas => "OrderBookDeltas",
125 Self::OrderBookDepth => "OrderBookDepth",
126 Self::QuoteTick => "QuoteTick",
127 Self::TradeTick => "TradeTick",
128 Self::Bar => "Bar",
129 Self::MarkPriceUpdate => "MarkPriceUpdate",
130 Self::IndexPriceUpdate => "IndexPriceUpdate",
131 Self::FundingRateUpdate => "FundingRateUpdate",
132 Self::OptionGreeks => "OptionGreeks",
133 Self::AccountState => "AccountState",
134 Self::OrderEvent => "OrderEventAny",
135 Self::PositionEvent => "PositionEvent",
136 Self::PortfolioSnapshot => "PortfolioSnapshot",
137 Self::SubscribeCommand => "SubscribeCommand",
138 Self::UnsubscribeCommand => "UnsubscribeCommand",
139 Self::TradingCommand => "TradingCommand",
140 Self::GenerateExecutionMassStatus => "GenerateExecutionMassStatus",
141 Self::OrderStatusReport => "OrderStatusReport",
142 Self::FillReport => "FillReport",
143 Self::PositionStatusReport => "PositionStatusReport",
144 Self::ExecutionMassStatus => "ExecutionMassStatus",
145 #[cfg(feature = "defi")]
146 Self::Block => "Block",
147 #[cfg(feature = "defi")]
148 Self::Pool => "Pool",
149 #[cfg(feature = "defi")]
150 Self::PoolLiquidityUpdate => "PoolLiquidityUpdate",
151 #[cfg(feature = "defi")]
152 Self::PoolFeeCollect => "PoolFeeCollect",
153 #[cfg(feature = "defi")]
154 Self::PoolFlash => "PoolFlash",
155 }
156 }
157
158 #[must_use]
164 pub fn from_name(name: &str) -> Self {
165 match name {
166 "InstrumentAny" => Self::Instrument,
167 "OrderBookDeltas" => Self::OrderBookDeltas,
168 "OrderBookDepth" => Self::OrderBookDepth,
169 "QuoteTick" => Self::QuoteTick,
170 "TradeTick" => Self::TradeTick,
171 "Bar" => Self::Bar,
172 "MarkPriceUpdate" => Self::MarkPriceUpdate,
173 "IndexPriceUpdate" => Self::IndexPriceUpdate,
174 "FundingRateUpdate" => Self::FundingRateUpdate,
175 "OptionGreeks" => Self::OptionGreeks,
176 "AccountState" => Self::AccountState,
177 "OrderEventAny" => Self::OrderEvent,
178 "PositionEvent" => Self::PositionEvent,
179 "PortfolioSnapshot" => Self::PortfolioSnapshot,
180 #[cfg(feature = "defi")]
181 "Block" => Self::Block,
182 #[cfg(feature = "defi")]
183 "Pool" => Self::Pool,
184 #[cfg(feature = "defi")]
185 "PoolLiquidityUpdate" => Self::PoolLiquidityUpdate,
186 #[cfg(feature = "defi")]
187 "PoolFeeCollect" => Self::PoolFeeCollect,
188 #[cfg(feature = "defi")]
189 "PoolFlash" => Self::PoolFlash,
190 other => Self::Custom(Ustr::from(other)),
191 }
192 }
193
194 #[must_use]
198 pub fn from_typed_name(name: &str) -> Option<Self> {
199 match name {
200 "SubscribeCommand" => Some(Self::SubscribeCommand),
201 "UnsubscribeCommand" => Some(Self::UnsubscribeCommand),
202 "TradingCommand" => Some(Self::TradingCommand),
203 "GenerateExecutionMassStatus" => Some(Self::GenerateExecutionMassStatus),
204 "OrderStatusReport" => Some(Self::OrderStatusReport),
205 "FillReport" => Some(Self::FillReport),
206 "PositionStatusReport" => Some(Self::PositionStatusReport),
207 "ExecutionMassStatus" => Some(Self::ExecutionMassStatus),
208 _ => None,
209 }
210 }
211
212 #[must_use]
214 pub const fn is_typed_message(self) -> bool {
215 matches!(
216 self,
217 Self::SubscribeCommand
218 | Self::UnsubscribeCommand
219 | Self::TradingCommand
220 | Self::GenerateExecutionMassStatus
221 | Self::OrderStatusReport
222 | Self::FillReport
223 | Self::PositionStatusReport
224 | Self::ExecutionMassStatus
225 )
226 }
227
228 #[must_use]
230 pub fn category(&self) -> BusPayloadCategory {
231 if self.has_bus_binary_schema() {
232 BusPayloadCategory::MarketData
233 } else {
234 match self {
235 Self::AccountState
236 | Self::OrderEvent
237 | Self::PositionEvent
238 | Self::PortfolioSnapshot => BusPayloadCategory::BuiltIn,
239 _ => BusPayloadCategory::Other,
240 }
241 }
242 }
243
244 #[must_use]
246 pub fn supports(&self, encoding: SerializationEncoding) -> bool {
247 match encoding {
248 SerializationEncoding::Json | SerializationEncoding::MsgPack => true,
249 SerializationEncoding::Sbe => cfg!(feature = "sbe") && self.has_bus_binary_schema(),
250 SerializationEncoding::Capnp => cfg!(feature = "capnp") && self.has_bus_binary_schema(),
251 }
252 }
253
254 fn has_bus_binary_schema(&self) -> bool {
255 matches!(
256 self,
257 Self::OrderBookDeltas
258 | Self::OrderBookDepth
259 | Self::QuoteTick
260 | Self::TradeTick
261 | Self::Bar
262 | Self::MarkPriceUpdate
263 | Self::IndexPriceUpdate
264 | Self::FundingRateUpdate
265 | Self::OptionGreeks
266 )
267 }
268}
269
270impl Display for BusPayloadType {
271 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
272 f.write_str(self.as_str())
273 }
274}
275
276impl Serialize for BusPayloadType {
277 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
278 if self.is_typed_message() {
279 TypedPayloadTypeRef {
280 typed: self.as_str(),
281 }
282 .serialize(serializer)
283 } else {
284 serializer.serialize_str(self.as_str())
285 }
286 }
287}
288
289impl<'de> Deserialize<'de> for BusPayloadType {
290 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
291 match BusPayloadTypeRepr::deserialize(deserializer)? {
292 BusPayloadTypeRepr::Name(name) => Ok(Self::from_name(&name)),
293 BusPayloadTypeRepr::Typed(value) => Self::from_typed_name(&value.typed)
294 .ok_or_else(|| D::Error::custom(format!("unknown typed payload: {}", value.typed))),
295 }
296 }
297}
298
299#[derive(Serialize)]
300struct TypedPayloadTypeRef<'a> {
301 typed: &'a str,
302}
303
304#[derive(Deserialize)]
305#[serde(deny_unknown_fields)]
306struct TypedPayloadType {
307 typed: String,
308}
309
310#[derive(Deserialize)]
311#[serde(untagged)]
312enum BusPayloadTypeRepr {
313 Name(String),
314 Typed(TypedPayloadType),
315}
316
317#[derive(Clone, Debug, Serialize, Deserialize)]
322#[cfg_attr(
323 feature = "python",
324 pyo3::pyclass(module = "nautilus_trader.common", from_py_object)
325)]
326#[cfg_attr(
327 feature = "python",
328 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.common")
329)]
330pub struct BusMessage {
331 pub topic: Ustr,
333 pub payload_type: BusPayloadType,
336 pub payload: Bytes,
338 pub encoding: SerializationEncoding,
341}
342
343impl BusMessage {
344 pub fn new(
346 topic: Ustr,
347 payload_type: BusPayloadType,
348 payload: Bytes,
349 encoding: SerializationEncoding,
350 ) -> Self {
351 debug_assert!(!topic.is_empty());
352 Self {
353 topic,
354 payload_type,
355 payload,
356 encoding,
357 }
358 }
359
360 pub fn with_str_topic<T: AsRef<str>>(
365 topic: T,
366 payload_type: BusPayloadType,
367 payload: Bytes,
368 encoding: SerializationEncoding,
369 ) -> Self {
370 Self::new(Ustr::from(topic.as_ref()), payload_type, payload, encoding)
371 }
372
373 pub fn new_close() -> Self {
375 Self::with_str_topic(
376 CLOSE_TOPIC,
377 BusPayloadType::Custom(Ustr::default()),
378 Bytes::new(),
379 SerializationEncoding::default(),
380 )
381 }
382}
383
384impl Display for BusMessage {
385 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
386 write!(
387 f,
388 "[{}] {} {} {}",
389 self.topic,
390 self.payload_type.as_str(),
391 String::from_utf8_lossy(&self.payload),
392 self.encoding
393 )
394 }
395}
396
397#[cfg(test)]
398mod tests {
399 use bytes::Bytes;
400 use rstest::rstest;
401
402 use super::*;
403
404 #[rstest]
405 #[case("test/topic", "payload data")]
406 #[case("events/trading", "Another payload")]
407 fn test_with_str_topic_str(#[case] topic: &str, #[case] payload_str: &str) {
408 let payload = Bytes::from(payload_str.to_string());
409
410 let message = BusMessage::with_str_topic(
411 topic,
412 BusPayloadType::QuoteTick,
413 payload.clone(),
414 SerializationEncoding::Json,
415 );
416
417 assert_eq!(message.topic, topic);
418 assert_eq!(message.payload_type, BusPayloadType::QuoteTick);
419 assert_eq!(message.encoding, SerializationEncoding::Json);
420 assert_eq!(message.payload, payload);
421 }
422
423 #[rstest]
424 fn test_with_str_topic_string() {
425 let topic_string = String::from("orders/new");
426 let payload = Bytes::from("order payload data");
427
428 let message = BusMessage::with_str_topic(
429 topic_string.clone(),
430 BusPayloadType::OrderEvent,
431 payload.clone(),
432 SerializationEncoding::MsgPack,
433 );
434
435 assert_eq!(message.topic, topic_string);
436 assert_eq!(message.payload_type, BusPayloadType::OrderEvent);
437 assert_eq!(message.encoding, SerializationEncoding::MsgPack);
438 assert_eq!(message.payload, payload);
439 }
440
441 #[rstest]
442 fn test_new_close() {
443 let message = BusMessage::new_close();
444
445 assert_eq!(message.topic, "CLOSE");
446 assert!(message.payload.is_empty());
447 }
448
449 #[rstest]
450 #[case(BusPayloadType::QuoteTick, BusPayloadCategory::MarketData)]
451 #[case(BusPayloadType::OrderBookDeltas, BusPayloadCategory::MarketData)]
452 #[case(BusPayloadType::AccountState, BusPayloadCategory::BuiltIn)]
453 #[case(BusPayloadType::OrderEvent, BusPayloadCategory::BuiltIn)]
454 #[case(BusPayloadType::Instrument, BusPayloadCategory::Other)]
455 #[case(BusPayloadType::OptionGreeks, BusPayloadCategory::MarketData)]
456 #[case(
457 BusPayloadType::Custom(Ustr::from("CustomPayload")),
458 BusPayloadCategory::Other
459 )]
460 fn bus_payload_type_category(
461 #[case] payload_type: BusPayloadType,
462 #[case] expected: BusPayloadCategory,
463 ) {
464 assert_eq!(payload_type.category(), expected);
465 }
466
467 #[rstest]
468 #[case(BusPayloadType::QuoteTick, SerializationEncoding::Json, true)]
469 #[case(BusPayloadType::QuoteTick, SerializationEncoding::MsgPack, true)]
470 #[cfg_attr(
471 feature = "sbe",
472 case(BusPayloadType::QuoteTick, SerializationEncoding::Sbe, true)
473 )]
474 #[cfg_attr(
475 not(feature = "sbe"),
476 case(BusPayloadType::QuoteTick, SerializationEncoding::Sbe, false)
477 )]
478 #[cfg_attr(
479 feature = "capnp",
480 case(BusPayloadType::QuoteTick, SerializationEncoding::Capnp, true)
481 )]
482 #[cfg_attr(
483 not(feature = "capnp"),
484 case(BusPayloadType::QuoteTick, SerializationEncoding::Capnp, false)
485 )]
486 #[case(BusPayloadType::AccountState, SerializationEncoding::Json, true)]
487 #[case(BusPayloadType::AccountState, SerializationEncoding::Capnp, false)]
488 #[case(BusPayloadType::Instrument, SerializationEncoding::Sbe, false)]
489 #[cfg_attr(
490 feature = "sbe",
491 case(BusPayloadType::OptionGreeks, SerializationEncoding::Sbe, true)
492 )]
493 #[cfg_attr(
494 not(feature = "sbe"),
495 case(BusPayloadType::OptionGreeks, SerializationEncoding::Sbe, false)
496 )]
497 #[cfg_attr(
498 feature = "capnp",
499 case(BusPayloadType::OptionGreeks, SerializationEncoding::Capnp, true)
500 )]
501 #[cfg_attr(
502 not(feature = "capnp"),
503 case(BusPayloadType::OptionGreeks, SerializationEncoding::Capnp, false)
504 )]
505 #[case(
506 BusPayloadType::Custom(Ustr::from("CustomPayload")),
507 SerializationEncoding::MsgPack,
508 true
509 )]
510 #[case(
511 BusPayloadType::Custom(Ustr::from("CustomPayload")),
512 SerializationEncoding::Sbe,
513 false
514 )]
515 fn bus_payload_type_supports_encoding(
516 #[case] payload_type: BusPayloadType,
517 #[case] encoding: SerializationEncoding,
518 #[case] expected: bool,
519 ) {
520 assert_eq!(payload_type.supports(encoding), expected);
521 }
522
523 #[rstest]
524 fn bus_payload_type_accepts_canonical_depth_name() {
525 assert_eq!(
526 BusPayloadType::from_name("OrderBookDepth"),
527 BusPayloadType::OrderBookDepth
528 );
529 assert_eq!(
530 BusPayloadType::from_name("OrderBookDepth10"),
531 BusPayloadType::Custom(Ustr::from("OrderBookDepth10")),
532 );
533 }
534
535 #[rstest]
536 fn typed_payload_types_use_json_msgpack_policy() {
537 let payload_types = [
538 (BusPayloadType::SubscribeCommand, "SubscribeCommand"),
539 (BusPayloadType::UnsubscribeCommand, "UnsubscribeCommand"),
540 (BusPayloadType::TradingCommand, "TradingCommand"),
541 (
542 BusPayloadType::GenerateExecutionMassStatus,
543 "GenerateExecutionMassStatus",
544 ),
545 (BusPayloadType::OrderStatusReport, "OrderStatusReport"),
546 (BusPayloadType::FillReport, "FillReport"),
547 (BusPayloadType::PositionStatusReport, "PositionStatusReport"),
548 (BusPayloadType::ExecutionMassStatus, "ExecutionMassStatus"),
549 ];
550
551 for (payload_type, name) in payload_types {
552 let custom_payload_type = BusPayloadType::Custom(Ustr::from(name));
553 let typed_json =
554 serde_json::to_value(payload_type).expect("typed payload type must serialize");
555 let custom_json = serde_json::to_value(custom_payload_type)
556 .expect("custom payload type must serialize");
557
558 assert_eq!(payload_type.as_str(), name);
559 assert_eq!(BusPayloadType::from_name(name), custom_payload_type);
560 assert_eq!(BusPayloadType::from_typed_name(name), Some(payload_type));
561 assert_eq!(typed_json, serde_json::json!({ "typed": name }));
562 assert_eq!(custom_json, serde_json::json!(name));
563 assert_eq!(
564 serde_json::from_value::<BusPayloadType>(typed_json)
565 .expect("typed payload type must deserialize"),
566 payload_type
567 );
568 assert_eq!(
569 serde_json::from_value::<BusPayloadType>(custom_json)
570 .expect("custom payload type must deserialize"),
571 custom_payload_type
572 );
573 assert!(payload_type.is_typed_message());
574 assert!(!custom_payload_type.is_typed_message());
575 assert_eq!(payload_type.category(), BusPayloadCategory::Other);
576 assert!(payload_type.supports(SerializationEncoding::Json));
577 assert!(payload_type.supports(SerializationEncoding::MsgPack));
578 assert!(!payload_type.supports(SerializationEncoding::Sbe));
579 assert!(!payload_type.supports(SerializationEncoding::Capnp));
580 }
581 }
582}