1pub mod bar;
19pub mod bet;
20pub mod black_scholes;
21pub mod close;
22pub mod custom;
23pub mod delta;
24pub mod deltas;
25pub mod depth;
26pub mod forward;
27pub mod funding;
28pub mod greeks;
29pub mod option_chain;
30pub mod order;
31pub mod prices;
32pub mod quote;
33pub mod registry;
34pub mod status;
35pub mod trade;
36
37#[cfg(any(test, feature = "stubs"))]
38pub mod stubs;
39
40use std::{
41 fmt::{Debug, Display},
42 hash::{Hash, Hasher},
43 str::FromStr,
44};
45
46use nautilus_core::{Params, UnixNanos};
47use serde::{
48 Deserialize, Serialize,
49 de::{self, IgnoredAny, MapAccess, SeqAccess, Visitor},
50};
51use serde_json::Value as JsonValue;
52
53#[cfg(feature = "defi")]
54use crate::defi::DefiData;
55#[rustfmt::skip] pub use bar::{Bar, BarSpecification, BarType};
58pub use black_scholes::Greeks;
59pub use close::InstrumentClose;
60#[cfg(feature = "python")]
61pub use custom::PythonCustomDataWrapper;
62pub use custom::{
63 CustomData, CustomDataTrait, ensure_custom_data_json_registered, register_custom_data_json,
64};
65#[cfg(feature = "python")]
66pub use custom::{
67 get_python_data_class, reconstruct_python_custom_data, register_python_data_class,
68};
69pub use delta::OrderBookDelta;
70pub use deltas::{OrderBookDeltas, OrderBookDeltas_API};
71pub use depth::{DEPTH10_LEN, OrderBookDepth10};
72pub use forward::ForwardPrice;
73pub use funding::FundingRateUpdate;
74pub use greeks::{
75 BlackScholesGreeksResult, GreeksData, HasGreeks, OptionGreekValues, PortfolioGreeks,
76 YieldCurveData, black_scholes_greeks, imply_vol_and_greeks, refine_vol_and_greeks,
77};
78pub use option_chain::{OptionChainSlice, OptionGreeks, OptionStrikeData, StrikeRange};
79pub use order::{BookOrder, NULL_ORDER};
80pub use prices::{IndexPriceUpdate, MarkPriceUpdate};
81pub use quote::QuoteTick;
82#[cfg(feature = "arrow")]
83pub use registry::{
84 ArrowDecoder, ArrowEncoder, decode_custom_from_arrow, encode_custom_to_arrow,
85 ensure_arrow_registered, get_arrow_schema, register_arrow,
86};
87#[cfg(feature = "python")]
88pub use registry::{
89 PyExtractor, ensure_py_extractor_registered, ensure_rust_extractor_factory_registered,
90 ensure_rust_extractor_registered, get_rust_extractor, register_py_extractor,
91 register_rust_extractor, register_rust_extractor_factory, try_extract_from_py,
92};
93pub use registry::{
94 deserialize_custom_from_json, ensure_json_deserializer_registered, register_json_deserializer,
95};
96pub use status::InstrumentStatus;
97pub use trade::TradeTick;
98
99use crate::identifiers::{InstrumentId, Venue};
100#[derive(Debug)]
105pub enum Data {
106 Delta(OrderBookDelta),
107 Deltas(OrderBookDeltas_API),
108 Depth10(Box<OrderBookDepth10>), Quote(QuoteTick),
110 Trade(TradeTick),
111 Bar(Bar),
112 MarkPriceUpdate(MarkPriceUpdate), IndexPriceUpdate(IndexPriceUpdate), FundingRateUpdate(FundingRateUpdate),
115 OptionGreeks(OptionGreeks),
116 InstrumentStatus(InstrumentStatus),
117 InstrumentClose(InstrumentClose),
118 Custom(CustomData),
119 #[cfg(feature = "defi")]
120 Defi(Box<DefiData>), }
122
123#[cfg(feature = "ffi")]
128#[repr(C)]
129#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
130#[allow(non_camel_case_types)]
131pub enum DataFFI {
132 Delta(OrderBookDelta),
133 Deltas(OrderBookDeltas_API),
134 Depth10(Box<OrderBookDepth10>),
135 Quote(QuoteTick),
136 Trade(TradeTick),
137 Bar(Bar),
138 MarkPriceUpdate(MarkPriceUpdate),
139 IndexPriceUpdate(IndexPriceUpdate),
140 InstrumentClose(InstrumentClose),
141}
142
143#[cfg(feature = "ffi")]
144impl TryFrom<Data> for DataFFI {
145 type Error = anyhow::Error;
146
147 fn try_from(value: Data) -> Result<Self, Self::Error> {
148 match value {
149 Data::Delta(x) => Ok(Self::Delta(x)),
150 Data::Deltas(x) => Ok(Self::Deltas(x)),
151 Data::Depth10(x) => Ok(Self::Depth10(x)),
152 Data::Quote(x) => Ok(Self::Quote(x)),
153 Data::Trade(x) => Ok(Self::Trade(x)),
154 Data::Bar(x) => Ok(Self::Bar(x)),
155 Data::MarkPriceUpdate(x) => Ok(Self::MarkPriceUpdate(x)),
156 Data::IndexPriceUpdate(x) => Ok(Self::IndexPriceUpdate(x)),
157 Data::FundingRateUpdate(_) => {
158 anyhow::bail!("Cannot convert Data::FundingRateUpdate to DataFFI")
159 }
160 Data::OptionGreeks(_) => {
161 anyhow::bail!("Cannot convert Data::OptionGreeks to DataFFI")
162 }
163 Data::InstrumentStatus(_) => {
164 anyhow::bail!("Cannot convert Data::InstrumentStatus to DataFFI")
165 }
166 Data::InstrumentClose(x) => Ok(Self::InstrumentClose(x)),
167 Data::Custom(_) => anyhow::bail!("Cannot convert Data::Custom to DataFFI"),
168 #[cfg(feature = "defi")]
169 Data::Defi(_) => anyhow::bail!("Cannot convert Data::Defi to DataFFI"),
170 }
171 }
172}
173
174#[cfg(feature = "ffi")]
175impl From<DataFFI> for Data {
176 fn from(value: DataFFI) -> Self {
177 match value {
178 DataFFI::Delta(x) => Self::Delta(x),
179 DataFFI::Deltas(x) => Self::Deltas(x),
180 DataFFI::Depth10(x) => Self::Depth10(x),
181 DataFFI::Quote(x) => Self::Quote(x),
182 DataFFI::Trade(x) => Self::Trade(x),
183 DataFFI::Bar(x) => Self::Bar(x),
184 DataFFI::MarkPriceUpdate(x) => Self::MarkPriceUpdate(x),
185 DataFFI::IndexPriceUpdate(x) => Self::IndexPriceUpdate(x),
186 DataFFI::InstrumentClose(x) => Self::InstrumentClose(x),
187 }
188 }
189}
190
191impl<'de> Deserialize<'de> for Data {
192 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
193 where
194 D: serde::Deserializer<'de>,
195 {
196 use serde::de::Error;
197 let value = serde_json::Value::deserialize(deserializer)?;
198 let type_name = value
199 .get("type")
200 .and_then(|v| v.as_str())
201 .ok_or_else(|| D::Error::custom("Missing 'type' field in Data"))?
202 .to_string();
203
204 match type_name.as_str() {
205 "OrderBookDelta" => Ok(Self::Delta(
206 serde_json::from_value(value).map_err(D::Error::custom)?,
207 )),
208 "OrderBookDeltas" => Ok(Self::Deltas(
209 serde_json::from_value(value).map_err(D::Error::custom)?,
210 )),
211 "OrderBookDepth10" => Ok(Self::Depth10(
212 serde_json::from_value(value).map_err(D::Error::custom)?,
213 )),
214 "QuoteTick" => Ok(Self::Quote(
215 serde_json::from_value(value).map_err(D::Error::custom)?,
216 )),
217 "TradeTick" => Ok(Self::Trade(
218 serde_json::from_value(value).map_err(D::Error::custom)?,
219 )),
220 "Bar" => Ok(Self::Bar(
221 serde_json::from_value(value).map_err(D::Error::custom)?,
222 )),
223 "MarkPriceUpdate" => Ok(Self::MarkPriceUpdate(
224 serde_json::from_value(value).map_err(D::Error::custom)?,
225 )),
226 "IndexPriceUpdate" => Ok(Self::IndexPriceUpdate(
227 serde_json::from_value(value).map_err(D::Error::custom)?,
228 )),
229 "FundingRateUpdate" => Ok(Self::FundingRateUpdate(
230 serde_json::from_value(value).map_err(D::Error::custom)?,
231 )),
232 "OptionGreeks" => Ok(Self::OptionGreeks(
233 serde_json::from_value(value).map_err(D::Error::custom)?,
234 )),
235 "InstrumentStatus" => Ok(Self::InstrumentStatus(
236 serde_json::from_value(value).map_err(D::Error::custom)?,
237 )),
238 "InstrumentClose" => Ok(Self::InstrumentClose(
239 serde_json::from_value(value).map_err(D::Error::custom)?,
240 )),
241 _ => {
242 if let Some(data) =
243 deserialize_custom_from_json(&type_name, &value).map_err(D::Error::custom)?
244 {
245 Ok(data)
246 } else {
247 Err(D::Error::custom(format!("Unknown Data type: {type_name}")))
248 }
249 }
250 }
251 }
252}
253
254impl Clone for Data {
255 fn clone(&self) -> Self {
256 match self {
257 Self::Delta(x) => Self::Delta(*x),
258 Self::Deltas(x) => Self::Deltas(x.clone()),
259 Self::Depth10(x) => Self::Depth10(x.clone()),
260 Self::Quote(x) => Self::Quote(*x),
261 Self::Trade(x) => Self::Trade(*x),
262 Self::Bar(x) => Self::Bar(*x),
263 Self::MarkPriceUpdate(x) => Self::MarkPriceUpdate(*x),
264 Self::IndexPriceUpdate(x) => Self::IndexPriceUpdate(*x),
265 Self::FundingRateUpdate(x) => Self::FundingRateUpdate(*x),
266 Self::OptionGreeks(x) => Self::OptionGreeks(*x),
267 Self::InstrumentStatus(x) => Self::InstrumentStatus(*x),
268 Self::InstrumentClose(x) => Self::InstrumentClose(*x),
269 Self::Custom(x) => Self::Custom(x.clone()),
270 #[cfg(feature = "defi")]
271 Self::Defi(x) => Self::Defi(x.clone()),
272 }
273 }
274}
275
276impl PartialEq for Data {
277 fn eq(&self, other: &Self) -> bool {
278 match (self, other) {
279 (Self::Delta(a), Self::Delta(b)) => a == b,
280 (Self::Deltas(a), Self::Deltas(b)) => a == b,
281 (Self::Depth10(a), Self::Depth10(b)) => a == b,
282 (Self::Quote(a), Self::Quote(b)) => a == b,
283 (Self::Trade(a), Self::Trade(b)) => a == b,
284 (Self::Bar(a), Self::Bar(b)) => a == b,
285 (Self::MarkPriceUpdate(a), Self::MarkPriceUpdate(b)) => a == b,
286 (Self::IndexPriceUpdate(a), Self::IndexPriceUpdate(b)) => a == b,
287 (Self::FundingRateUpdate(a), Self::FundingRateUpdate(b)) => a == b,
288 (Self::OptionGreeks(a), Self::OptionGreeks(b)) => a == b,
289 (Self::InstrumentStatus(a), Self::InstrumentStatus(b)) => a == b,
290 (Self::InstrumentClose(a), Self::InstrumentClose(b)) => a == b,
291 (Self::Custom(a), Self::Custom(b)) => a == b,
292 #[cfg(feature = "defi")]
293 (Self::Defi(a), Self::Defi(b)) => a == b,
294 _ => false,
295 }
296 }
297}
298
299impl Serialize for Data {
300 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
301 where
302 S: serde::Serializer,
303 {
304 match self {
305 Self::Delta(x) => x.serialize(serializer),
306 Self::Deltas(x) => x.serialize(serializer),
307 Self::Depth10(x) => x.serialize(serializer),
308 Self::Quote(x) => x.serialize(serializer),
309 Self::Trade(x) => x.serialize(serializer),
310 Self::Bar(x) => x.serialize(serializer),
311 Self::MarkPriceUpdate(x) => x.serialize(serializer),
312 Self::IndexPriceUpdate(x) => x.serialize(serializer),
313 Self::FundingRateUpdate(x) => x.serialize(serializer),
314 Self::OptionGreeks(x) => x.serialize(serializer),
315 Self::InstrumentStatus(x) => x.serialize(serializer),
316 Self::InstrumentClose(x) => x.serialize(serializer),
317 Self::Custom(x) => x.serialize(serializer),
318 #[cfg(feature = "defi")]
319 Self::Defi(_) => Err(serde::ser::Error::custom(
320 "Data::Defi serialization is not supported",
321 )),
322 }
323 }
324}
325
326macro_rules! impl_try_from_data {
327 ($variant:ident, $type:ty) => {
328 impl TryFrom<Data> for $type {
329 type Error = ();
330
331 fn try_from(value: Data) -> Result<Self, Self::Error> {
332 match value {
333 Data::$variant(x) => Ok(x),
334 _ => Err(()),
335 }
336 }
337 }
338 };
339}
340
341impl TryFrom<Data> for OrderBookDepth10 {
342 type Error = ();
343
344 fn try_from(value: Data) -> Result<Self, Self::Error> {
345 match value {
346 Data::Depth10(x) => Ok(*x),
347 _ => Err(()),
348 }
349 }
350}
351
352impl_try_from_data!(Quote, QuoteTick);
353impl_try_from_data!(Delta, OrderBookDelta);
354impl_try_from_data!(Deltas, OrderBookDeltas_API);
355impl_try_from_data!(Trade, TradeTick);
356impl_try_from_data!(Bar, Bar);
357impl_try_from_data!(MarkPriceUpdate, MarkPriceUpdate);
358impl_try_from_data!(IndexPriceUpdate, IndexPriceUpdate);
359impl_try_from_data!(FundingRateUpdate, FundingRateUpdate);
360impl_try_from_data!(OptionGreeks, OptionGreeks);
361impl_try_from_data!(InstrumentStatus, InstrumentStatus);
362impl_try_from_data!(InstrumentClose, InstrumentClose);
363
364#[must_use]
369pub fn to_variant<T: TryFrom<Data>>(data: Vec<Data>) -> Vec<T> {
370 data.into_iter()
371 .filter_map(|d| T::try_from(d).ok())
372 .collect()
373}
374
375impl Data {
376 #[must_use]
378 pub fn instrument_id(&self) -> InstrumentId {
379 match self {
380 Self::Delta(delta) => delta.instrument_id,
381 Self::Deltas(deltas) => deltas.instrument_id,
382 Self::Depth10(depth) => depth.instrument_id,
383 Self::Quote(quote) => quote.instrument_id,
384 Self::Trade(trade) => trade.instrument_id,
385 Self::Bar(bar) => bar.bar_type.instrument_id(),
386 Self::MarkPriceUpdate(mark_price) => mark_price.instrument_id,
387 Self::IndexPriceUpdate(index_price) => index_price.instrument_id,
388 Self::FundingRateUpdate(funding_rate) => funding_rate.instrument_id,
389 Self::OptionGreeks(greeks) => greeks.instrument_id,
390 Self::InstrumentStatus(status) => status.instrument_id,
391 Self::InstrumentClose(close) => close.instrument_id,
392 Self::Custom(custom) => custom
393 .data_type
394 .identifier()
395 .and_then(|s| InstrumentId::from_str(s).ok())
396 .or_else(|| {
397 custom
398 .data_type
399 .metadata()
400 .and_then(|m| m.get_str("instrument_id"))
401 .and_then(|s| InstrumentId::from_str(s).ok())
402 })
403 .unwrap_or_else(|| InstrumentId::from("NULL.NULL")),
404 #[cfg(feature = "defi")]
405 Self::Defi(defi) => defi.instrument_id(),
406 }
407 }
408
409 #[must_use]
411 pub fn is_order_book_data(&self) -> bool {
412 matches!(self, Self::Delta(_) | Self::Deltas(_) | Self::Depth10(_))
413 }
414}
415
416pub trait HasTsInit {
422 fn ts_init(&self) -> UnixNanos;
424}
425
426pub trait CatalogPathPrefix {
428 fn path_prefix() -> &'static str;
430}
431
432#[macro_export]
442macro_rules! impl_catalog_path_prefix {
443 ($type:ty, $path:expr) => {
444 impl $crate::data::CatalogPathPrefix for $type {
445 fn path_prefix() -> &'static str {
446 $path
447 }
448 }
449 };
450}
451
452impl_catalog_path_prefix!(QuoteTick, "quotes");
454impl_catalog_path_prefix!(TradeTick, "trades");
455impl_catalog_path_prefix!(OrderBookDelta, "order_book_deltas");
456impl_catalog_path_prefix!(OrderBookDepth10, "order_book_depths");
457impl_catalog_path_prefix!(Bar, "bars");
458impl_catalog_path_prefix!(IndexPriceUpdate, "index_prices");
459impl_catalog_path_prefix!(MarkPriceUpdate, "mark_prices");
460impl_catalog_path_prefix!(FundingRateUpdate, "funding_rate_update");
461impl_catalog_path_prefix!(OptionGreeks, "option_greeks");
462impl_catalog_path_prefix!(InstrumentStatus, "instrument_status");
463impl_catalog_path_prefix!(InstrumentClose, "instrument_closes");
464
465use crate::instruments::InstrumentAny;
466impl_catalog_path_prefix!(InstrumentAny, "instruments");
467
468impl HasTsInit for Data {
469 fn ts_init(&self) -> UnixNanos {
470 match self {
471 Self::Delta(d) => d.ts_init,
472 Self::Deltas(d) => d.ts_init,
473 Self::Depth10(d) => d.ts_init,
474 Self::Quote(q) => q.ts_init,
475 Self::Trade(t) => t.ts_init,
476 Self::Bar(b) => b.ts_init,
477 Self::MarkPriceUpdate(p) => p.ts_init,
478 Self::IndexPriceUpdate(p) => p.ts_init,
479 Self::FundingRateUpdate(f) => f.ts_init,
480 Self::OptionGreeks(g) => g.ts_init,
481 Self::InstrumentStatus(s) => s.ts_init,
482 Self::InstrumentClose(c) => c.ts_init,
483 Self::Custom(c) => c.data.ts_init(),
484 #[cfg(feature = "defi")]
485 Self::Defi(d) => d.ts_init(),
486 }
487 }
488}
489
490pub fn is_monotonically_increasing_by_init<T: HasTsInit>(data: &[T]) -> bool {
494 data.array_windows()
495 .all(|[a, b]| a.ts_init() <= b.ts_init())
496}
497
498impl From<OrderBookDelta> for Data {
499 fn from(value: OrderBookDelta) -> Self {
500 Self::Delta(value)
501 }
502}
503
504impl From<OrderBookDeltas_API> for Data {
505 fn from(value: OrderBookDeltas_API) -> Self {
506 Self::Deltas(value)
507 }
508}
509
510impl From<OrderBookDepth10> for Data {
511 fn from(value: OrderBookDepth10) -> Self {
512 Self::Depth10(Box::new(value))
513 }
514}
515
516impl From<QuoteTick> for Data {
517 fn from(value: QuoteTick) -> Self {
518 Self::Quote(value)
519 }
520}
521
522impl From<TradeTick> for Data {
523 fn from(value: TradeTick) -> Self {
524 Self::Trade(value)
525 }
526}
527
528impl From<Bar> for Data {
529 fn from(value: Bar) -> Self {
530 Self::Bar(value)
531 }
532}
533
534impl From<MarkPriceUpdate> for Data {
535 fn from(value: MarkPriceUpdate) -> Self {
536 Self::MarkPriceUpdate(value)
537 }
538}
539
540impl From<IndexPriceUpdate> for Data {
541 fn from(value: IndexPriceUpdate) -> Self {
542 Self::IndexPriceUpdate(value)
543 }
544}
545
546impl From<FundingRateUpdate> for Data {
547 fn from(value: FundingRateUpdate) -> Self {
548 Self::FundingRateUpdate(value)
549 }
550}
551
552impl From<OptionGreeks> for Data {
553 fn from(value: OptionGreeks) -> Self {
554 Self::OptionGreeks(value)
555 }
556}
557
558impl From<InstrumentStatus> for Data {
559 fn from(value: InstrumentStatus) -> Self {
560 Self::InstrumentStatus(value)
561 }
562}
563
564impl From<InstrumentClose> for Data {
565 fn from(value: InstrumentClose) -> Self {
566 Self::InstrumentClose(value)
567 }
568}
569
570#[cfg(feature = "defi")]
571impl From<DefiData> for Data {
572 fn from(value: DefiData) -> Self {
573 Self::Defi(Box::new(value))
574 }
575}
576
577fn value_to_topic_string(v: &JsonValue) -> String {
579 if let Some(s) = v.as_str() {
580 return s.to_string();
581 }
582
583 if let Some(n) = v.as_u64() {
584 return n.to_string();
585 }
586
587 if let Some(n) = v.as_i64() {
588 return n.to_string();
589 }
590
591 if let Some(b) = v.as_bool() {
592 return b.to_string();
593 }
594
595 if let Some(f) = v.as_f64() {
596 return f.to_string();
597 }
598
599 if v.is_null() {
600 return "null".to_string();
601 }
602 serde_json::to_string(v).unwrap_or_default()
603}
604
605fn params_to_topic_suffix(params: &Params) -> String {
607 let mut entries = params.iter().collect::<Vec<_>>();
608 entries.sort_by_key(|(key, _)| *key);
609
610 entries
611 .into_iter()
612 .map(|(k, v)| format!("{k}={}", value_to_topic_string(v)))
613 .collect::<Vec<_>>()
614 .join(".")
615}
616
617#[derive(Clone, Serialize)]
619#[cfg_attr(
620 feature = "python",
621 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model", from_py_object)
622)]
623#[cfg_attr(
624 feature = "python",
625 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
626)]
627pub struct DataType {
628 type_name: String,
629 metadata: Option<Params>,
630 topic: String,
631 hash: u64,
632 identifier: Option<String>,
633}
634
635impl<'de> Deserialize<'de> for DataType {
636 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
637 where
638 D: serde::Deserializer<'de>,
639 {
640 const FIELDS: &[&str] = &["type_name", "metadata", "topic", "hash", "identifier"];
641
642 #[derive(Deserialize)]
643 #[serde(field_identifier, rename_all = "snake_case")]
644 enum Field {
645 TypeName,
646 Metadata,
647 Topic,
648 Hash,
649 Identifier,
650 #[serde(other)]
651 Other,
652 }
653
654 fn finish(
655 type_name: &str,
656 metadata: Option<Params>,
657 topic: Option<String>,
658 identifier: Option<String>,
659 ) -> DataType {
660 let mut data_type = DataType::new(type_name, metadata, identifier);
661
662 if let Some(topic) = topic {
663 let mut hasher = std::collections::hash_map::DefaultHasher::new();
664 topic.hash(&mut hasher);
665 data_type.topic = topic;
666 data_type.hash = hasher.finish();
667 }
668
669 data_type
670 }
671
672 struct DataTypeVisitor;
673
674 impl<'de> Visitor<'de> for DataTypeVisitor {
675 type Value = DataType;
676
677 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
678 formatter.write_str("struct DataType")
679 }
680
681 fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
682 where
683 A: SeqAccess<'de>,
684 {
685 let type_name: String = seq
686 .next_element()?
687 .ok_or_else(|| de::Error::invalid_length(0, &self))?;
688 let metadata = seq
692 .next_element()?
693 .ok_or_else(|| de::Error::invalid_length(1, &self))?;
694 let topic = seq
695 .next_element()?
696 .ok_or_else(|| de::Error::invalid_length(2, &self))?;
697 let _hash: u64 = seq
698 .next_element()?
699 .ok_or_else(|| de::Error::invalid_length(3, &self))?;
700 let identifier = seq
701 .next_element()?
702 .ok_or_else(|| de::Error::invalid_length(4, &self))?;
703
704 Ok(finish(&type_name, metadata, Some(topic), identifier))
705 }
706
707 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
708 where
709 A: MapAccess<'de>,
710 {
711 let mut type_name: Option<String> = None;
712 let mut metadata = None;
713 let mut topic = None;
714 let mut hash_seen = false;
715 let mut identifier = None;
716
717 while let Some(key) = map.next_key()? {
718 match key {
719 Field::TypeName => {
720 if type_name.is_some() {
721 return Err(de::Error::duplicate_field("type_name"));
722 }
723 type_name = Some(map.next_value()?);
724 }
725 Field::Metadata => {
726 if metadata.is_some() {
727 return Err(de::Error::duplicate_field("metadata"));
728 }
729 metadata = Some(map.next_value()?);
730 }
731 Field::Topic => {
732 if topic.is_some() {
733 return Err(de::Error::duplicate_field("topic"));
734 }
735 topic = Some(map.next_value()?);
736 }
737 Field::Hash => {
738 if hash_seen {
739 return Err(de::Error::duplicate_field("hash"));
740 }
741 hash_seen = true;
742 let _: Option<u64> = map.next_value()?;
743 }
744 Field::Identifier => {
745 if identifier.is_some() {
746 return Err(de::Error::duplicate_field("identifier"));
747 }
748 identifier = Some(map.next_value()?);
749 }
750 Field::Other => {
751 let _: IgnoredAny = map.next_value()?;
752 }
753 }
754 }
755
756 let type_name = type_name.ok_or_else(|| de::Error::missing_field("type_name"))?;
757 Ok(finish(
758 &type_name,
759 metadata.unwrap_or(None),
760 topic.unwrap_or(None),
761 identifier.unwrap_or(None),
762 ))
763 }
764 }
765
766 deserializer.deserialize_struct("DataType", FIELDS, DataTypeVisitor)
767 }
768}
769
770impl DataType {
771 #[must_use]
773 pub fn new(type_name: &str, metadata: Option<Params>, identifier: Option<String>) -> Self {
774 let topic = if let Some(ref meta) = metadata {
776 if meta.is_empty() {
777 type_name.to_string()
778 } else {
779 format!("{type_name}.{}", params_to_topic_suffix(meta))
780 }
781 } else {
782 type_name.to_string()
783 };
784
785 let mut hasher = std::collections::hash_map::DefaultHasher::new();
787 topic.hash(&mut hasher);
788
789 Self {
790 type_name: type_name.to_owned(),
791 metadata,
792 topic,
793 hash: hasher.finish(),
794 identifier,
795 }
796 }
797
798 #[must_use]
802 pub fn from_parts(type_name: &str, topic: &str, metadata: Option<Params>) -> Self {
803 let mut hasher = std::collections::hash_map::DefaultHasher::new();
804 topic.hash(&mut hasher);
805 Self {
806 type_name: type_name.to_owned(),
807 metadata,
808 topic: topic.to_owned(),
809 hash: hasher.finish(),
810 identifier: None,
811 }
812 }
813
814 pub fn to_persistence_json(&self) -> Result<String, serde_json::Error> {
820 let mut map = serde_json::Map::new();
821 map.insert(
822 "type_name".to_string(),
823 serde_json::Value::String(self.type_name.clone()),
824 );
825 map.insert(
826 "metadata".to_string(),
827 self.metadata.as_ref().map_or(serde_json::Value::Null, |m| {
828 serde_json::to_value(m).unwrap_or(serde_json::Value::Null)
829 }),
830 );
831
832 if let Some(ref id) = self.identifier {
833 map.insert(
834 "identifier".to_string(),
835 serde_json::Value::String(id.clone()),
836 );
837 }
838 serde_json::to_string(&serde_json::Value::Object(map))
839 }
840
841 pub fn from_persistence_json(s: &str) -> Result<Self, anyhow::Error> {
848 let value: serde_json::Value =
849 serde_json::from_str(s).map_err(|e| anyhow::anyhow!("Invalid data_type JSON: {e}"))?;
850 let obj = value
851 .as_object()
852 .ok_or_else(|| anyhow::anyhow!("data_type must be a JSON object"))?;
853 let type_name = obj
854 .get("type_name")
855 .and_then(|v| v.as_str())
856 .ok_or_else(|| anyhow::anyhow!("data_type must have type_name"))?
857 .to_string();
858 let metadata = obj.get("metadata").and_then(|m| {
859 if m.is_null() {
860 None
861 } else {
862 let p: Params = serde_json::from_value(m.clone()).ok()?;
863 if p.is_empty() { None } else { Some(p) }
864 }
865 });
866 let identifier = obj
867 .get("identifier")
868 .and_then(|v| v.as_str())
869 .map(String::from);
870 Ok(Self::new(&type_name, metadata, identifier))
871 }
872
873 #[must_use]
875 pub fn type_name(&self) -> &str {
876 self.type_name.as_str()
877 }
878
879 #[must_use]
881 pub fn metadata(&self) -> Option<&Params> {
882 self.metadata.as_ref()
883 }
884
885 #[must_use]
887 pub fn metadata_str(&self) -> String {
888 self.metadata.as_ref().map_or_else(
889 || "null".to_string(),
890 |metadata| {
891 let mut entries = metadata.iter().collect::<Vec<_>>();
892 entries.sort_by_key(|(key, _)| *key);
893
894 let mut metadata_map = serde_json::Map::new();
895 for (key, value) in entries {
896 metadata_map.insert(key.clone(), value.clone());
897 }
898
899 serde_json::to_string(&metadata_map).unwrap_or_default()
900 },
901 )
902 }
903
904 #[must_use]
906 pub fn metadata_string_map(&self) -> Option<std::collections::HashMap<String, String>> {
907 self.metadata.as_ref().map(|p| {
908 p.iter()
909 .map(|(k, v)| (k.clone(), value_to_topic_string(v)))
910 .collect()
911 })
912 }
913
914 #[must_use]
916 pub fn precomputed_hash(&self) -> u64 {
917 self.hash
918 }
919
920 #[must_use]
922 pub fn topic(&self) -> &str {
923 self.topic.as_str()
924 }
925
926 #[must_use]
928 pub fn identifier(&self) -> Option<&str> {
929 self.identifier.as_deref()
930 }
931
932 #[must_use]
939 pub fn instrument_id(&self) -> Option<InstrumentId> {
940 let metadata = self.metadata.as_ref()?;
941 let instrument_id = metadata.get_str("instrument_id")?;
942 Some(
943 InstrumentId::from_str(instrument_id)
944 .expect("Invalid `InstrumentId` for 'instrument_id'"),
945 )
946 }
947
948 #[must_use]
955 pub fn venue(&self) -> Option<Venue> {
956 let metadata = self.metadata.as_ref()?;
957 let venue_str = metadata.get_str("venue")?;
958 Some(Venue::from(venue_str))
959 }
960
961 #[must_use]
968 pub fn start(&self) -> Option<UnixNanos> {
969 let metadata = self.metadata.as_ref()?;
970 let start_str = metadata.get_str("start")?;
971 Some(UnixNanos::from_str(start_str).expect("Invalid `UnixNanos` for 'start'"))
972 }
973
974 #[must_use]
981 pub fn end(&self) -> Option<UnixNanos> {
982 let metadata = self.metadata.as_ref()?;
983 let end_str = metadata.get_str("end")?;
984 Some(UnixNanos::from_str(end_str).expect("Invalid `UnixNanos` for 'end'"))
985 }
986
987 #[must_use]
994 pub fn limit(&self) -> Option<usize> {
995 let metadata = self.metadata.as_ref()?;
996 metadata.get_usize("limit").or_else(|| {
997 metadata
998 .get_str("limit")
999 .map(|s| s.parse::<usize>().expect("Invalid `usize` for 'limit'"))
1000 })
1001 }
1002}
1003
1004impl PartialEq for DataType {
1005 fn eq(&self, other: &Self) -> bool {
1006 self.topic == other.topic
1007 }
1008}
1009
1010impl Eq for DataType {}
1011
1012impl PartialOrd for DataType {
1013 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1014 Some(self.cmp(other))
1015 }
1016}
1017
1018impl Ord for DataType {
1019 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1020 self.topic.cmp(&other.topic)
1021 }
1022}
1023
1024impl Hash for DataType {
1025 fn hash<H: Hasher>(&self, state: &mut H) {
1026 self.hash.hash(state);
1027 }
1028}
1029
1030impl Display for DataType {
1031 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1032 write!(f, "{}", self.topic)
1033 }
1034}
1035
1036impl Debug for DataType {
1037 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1038 write!(
1039 f,
1040 "DataType(type_name={}, metadata={:?}, identifier={:?})",
1041 self.type_name, self.metadata, self.identifier
1042 )
1043 }
1044}
1045
1046#[cfg(test)]
1047mod tests {
1048 use std::hash::DefaultHasher;
1049
1050 use rstest::*;
1051 use serde_json::json;
1052
1053 use super::*;
1054
1055 fn params_from_json(value: serde_json::Value) -> Params {
1056 serde_json::from_value(value).expect("valid Params JSON")
1057 }
1058
1059 fn hash_data_type(data_type: &DataType) -> u64 {
1060 let mut hasher = DefaultHasher::new();
1061 data_type.hash(&mut hasher);
1062 hasher.finish()
1063 }
1064
1065 #[cfg(feature = "ffi")]
1066 #[rstest]
1067 fn test_funding_rate_update_does_not_convert_to_data_ffi() {
1068 let funding_rate = FundingRateUpdate::new(
1069 InstrumentId::from("BTCUSDT-PERP.BINANCE"),
1070 "0.0001".parse().unwrap(),
1071 Some(480),
1072 Some(UnixNanos::from(1_000_000_000)),
1073 UnixNanos::from(1),
1074 UnixNanos::from(2),
1075 );
1076
1077 let err = DataFFI::try_from(Data::FundingRateUpdate(funding_rate)).unwrap_err();
1078
1079 assert_eq!(
1080 err.to_string(),
1081 "Cannot convert Data::FundingRateUpdate to DataFFI"
1082 );
1083 }
1084
1085 #[rstest]
1086 fn test_data_type_creation_with_metadata() {
1087 let metadata = Some(params_from_json(
1088 json!({"key1": "value1", "key2": "value2"}),
1089 ));
1090 let data_type = DataType::new("ExampleType", metadata.clone(), None);
1091
1092 assert_eq!(data_type.type_name(), "ExampleType");
1093 assert_eq!(data_type.topic(), "ExampleType.key1=value1.key2=value2");
1094 assert_eq!(data_type.metadata(), metadata.as_ref());
1095 }
1096
1097 #[rstest]
1098 fn test_data_type_topic_identity_uses_canonical_metadata_order() {
1099 let mut metadata1 = Params::new();
1100 metadata1.insert("b".to_string(), json!(2));
1101 metadata1.insert("a".to_string(), json!(1));
1102 let mut metadata2 = Params::new();
1103 metadata2.insert("a".to_string(), json!(1));
1104 metadata2.insert("b".to_string(), json!(2));
1105
1106 let data_type1 = DataType::new("ExampleType", Some(metadata1), None);
1107 let data_type2 = DataType::new("ExampleType", Some(metadata2), None);
1108 let mut hasher1 = DefaultHasher::new();
1109 data_type1.hash(&mut hasher1);
1110 let hash1 = hasher1.finish();
1111 let mut hasher2 = DefaultHasher::new();
1112 data_type2.hash(&mut hasher2);
1113 let hash2 = hasher2.finish();
1114
1115 assert_eq!(data_type1.topic(), "ExampleType.a=1.b=2");
1116 assert_eq!(data_type1.topic(), data_type2.topic());
1117 assert_eq!(data_type1, data_type2);
1118 assert_eq!(hash1, hash2);
1119 assert_eq!(format!("{data_type1}"), format!("{data_type2}"));
1120 assert_eq!(data_type1.metadata_str(), r#"{"a":1,"b":2}"#);
1121 assert_eq!(data_type1.metadata_str(), data_type2.metadata_str());
1122 }
1123
1124 #[rstest]
1125 fn test_data_type_creation_without_metadata() {
1126 let data_type = DataType::new("ExampleType", None, None);
1127
1128 assert_eq!(data_type.type_name(), "ExampleType");
1129 assert_eq!(data_type.topic(), "ExampleType");
1130 assert_eq!(data_type.metadata(), None);
1131 }
1132
1133 #[rstest]
1134 fn test_data_type_equality() {
1135 let metadata1 = Some(params_from_json(json!({"key1": "value1"})));
1136 let metadata2 = Some(params_from_json(json!({"key1": "value1"})));
1137
1138 let data_type1 = DataType::new("ExampleType", metadata1, None);
1139 let data_type2 = DataType::new("ExampleType", metadata2, None);
1140
1141 assert_eq!(data_type1, data_type2);
1142 }
1143
1144 #[rstest]
1145 fn test_data_type_inequality() {
1146 let metadata1 = Some(params_from_json(json!({"key1": "value1"})));
1147 let metadata2 = Some(params_from_json(json!({"key2": "value2"})));
1148
1149 let data_type1 = DataType::new("ExampleType", metadata1, None);
1150 let data_type2 = DataType::new("ExampleType", metadata2, None);
1151
1152 assert_ne!(data_type1, data_type2);
1153 }
1154
1155 #[rstest]
1156 fn test_data_type_ordering() {
1157 let metadata1 = Some(params_from_json(json!({"key1": "value1"})));
1158 let metadata2 = Some(params_from_json(json!({"key2": "value2"})));
1159
1160 let data_type1 = DataType::new("ExampleTypeA", metadata1, None);
1161 let data_type2 = DataType::new("ExampleTypeB", metadata2, None);
1162
1163 assert!(data_type1 < data_type2);
1164 }
1165
1166 #[rstest]
1167 fn test_data_type_hash() {
1168 let metadata = Some(params_from_json(json!({"key1": "value1"})));
1169
1170 let data_type1 = DataType::new("ExampleType", metadata.clone(), None);
1171 let data_type2 = DataType::new("ExampleType", metadata, None);
1172
1173 let mut hasher1 = DefaultHasher::new();
1174 data_type1.hash(&mut hasher1);
1175 let hash1 = hasher1.finish();
1176
1177 let mut hasher2 = DefaultHasher::new();
1178 data_type2.hash(&mut hasher2);
1179 let hash2 = hasher2.finish();
1180
1181 assert_eq!(hash1, hash2);
1182 }
1183
1184 #[rstest]
1185 fn test_data_type_deserialization_recomputes_hash_from_topic() {
1186 let expected = DataType::from_parts(
1187 "ExampleType",
1188 "custom.topic",
1189 Some(params_from_json(json!({"key": "value"}))),
1190 );
1191 let payload = json!({
1192 "type_name": expected.type_name(),
1193 "metadata": expected.metadata(),
1194 "topic": expected.topic(),
1195 "hash": expected.precomputed_hash() ^ u64::MAX,
1196 "identifier": "catalog/path",
1197 });
1198
1199 let deserialized: DataType = serde_json::from_value(payload).unwrap();
1200
1201 assert_eq!(deserialized.topic(), expected.topic());
1202 assert_eq!(deserialized.precomputed_hash(), expected.precomputed_hash());
1203 }
1204
1205 #[rstest]
1206 fn test_data_type_deserialization_without_cache_fields_uses_constructor() {
1207 let payload = json!({
1208 "type_name": "ExampleType",
1209 "metadata": {"z": 9, "a": 1},
1210 "identifier": "catalog/path",
1211 });
1212 let expected = DataType::new(
1213 "ExampleType",
1214 Some(params_from_json(json!({"z": 9, "a": 1}))),
1215 Some("catalog/path".to_string()),
1216 );
1217
1218 let deserialized: DataType = serde_json::from_value(payload).unwrap();
1219
1220 assert_eq!(deserialized.topic(), expected.topic());
1221 assert_eq!(deserialized.precomputed_hash(), expected.precomputed_hash());
1222 }
1223
1224 #[rstest]
1225 fn test_data_type_deserialization_preserves_topic_without_hash() {
1226 let expected = DataType::from_parts("ExampleType", "custom.topic", None);
1227 let payload = json!({
1228 "type_name": "ExampleType",
1229 "metadata": null,
1230 "topic": "custom.topic",
1231 });
1232
1233 let deserialized: DataType = serde_json::from_value(payload).unwrap();
1234
1235 assert_eq!(deserialized.topic(), "custom.topic");
1236 assert_eq!(deserialized.precomputed_hash(), expected.precomputed_hash());
1237 }
1238
1239 #[rstest]
1240 fn test_data_type_deserialization_ignores_hash_without_topic() {
1241 let expected = DataType::new("ExampleType", None, None);
1242 let payload = json!({
1243 "type_name": "ExampleType",
1244 "metadata": null,
1245 "hash": expected.precomputed_hash() ^ u64::MAX,
1246 });
1247
1248 let deserialized: DataType = serde_json::from_value(payload).unwrap();
1249
1250 assert_eq!(deserialized.topic(), expected.topic());
1251 assert_eq!(deserialized.precomputed_hash(), expected.precomputed_hash());
1252 }
1253
1254 #[rstest]
1255 fn test_data_type_deserialization_rejects_duplicate_map_key() {
1256 let payload = r#"{"type_name":"ExampleType","topic":"first","topic":"second"}"#;
1257
1258 let error = serde_json::from_str::<DataType>(payload).unwrap_err();
1259
1260 assert!(error.to_string().contains("duplicate field `topic`"));
1261 }
1262
1263 #[rstest]
1264 #[case(
1265 r#"{"type_name":"ExampleType","topic":null,"topic":"second"}"#,
1266 "duplicate field `topic`"
1267 )]
1268 #[case(
1269 r#"{"type_name":"ExampleType","hash":null,"hash":7}"#,
1270 "duplicate field `hash`"
1271 )]
1272 #[case(
1273 r#"{"type_name":"ExampleType","metadata":null,"metadata":{"a":1}}"#,
1274 "duplicate field `metadata`"
1275 )]
1276 #[case(
1277 r#"{"type_name":"ExampleType","identifier":null,"identifier":"second"}"#,
1278 "duplicate field `identifier`"
1279 )]
1280 fn test_data_type_deserialization_rejects_duplicate_map_key_after_null(
1281 #[case] payload: &str,
1282 #[case] expected: &str,
1283 ) {
1284 let error = serde_json::from_str::<DataType>(payload).unwrap_err();
1287
1288 assert!(error.to_string().contains(expected));
1289 }
1290
1291 #[rstest]
1292 fn test_data_type_serde_roundtrip_preserves_fields_and_repairs_hash() {
1293 let expected = DataType::from_parts(
1294 "ExampleType",
1295 "custom.topic",
1296 Some(params_from_json(json!({"key": "value"}))),
1297 );
1298 let payload = json!({
1299 "type_name": expected.type_name(),
1300 "metadata": expected.metadata(),
1301 "topic": expected.topic(),
1302 "hash": expected.precomputed_hash() ^ u64::MAX,
1303 "identifier": "catalog/path",
1304 });
1305 let deserialized: DataType = serde_json::from_value(payload).unwrap();
1306
1307 let json = serde_json::to_string(&deserialized).unwrap();
1308 let roundtripped: DataType = serde_json::from_str(&json).unwrap();
1309
1310 assert_eq!(roundtripped.type_name(), "ExampleType");
1311 assert_eq!(roundtripped.metadata(), expected.metadata());
1312 assert_eq!(roundtripped.identifier(), Some("catalog/path"));
1313 assert_eq!(roundtripped.topic(), "custom.topic");
1314 assert_eq!(roundtripped.precomputed_hash(), expected.precomputed_hash());
1315 }
1316
1317 #[rstest]
1318 fn test_data_type_serialized_cache_fields_remain_wire_compatible() {
1319 #[derive(Deserialize)]
1320 struct LegacyDataType {
1321 type_name: String,
1322 metadata: Option<Params>,
1323 topic: String,
1324 hash: u64,
1325 identifier: Option<String>,
1326 }
1327
1328 let expected = DataType::new(
1329 "ExampleType",
1330 Some(params_from_json(json!({"key": "value"}))),
1331 Some("catalog/path".to_string()),
1332 );
1333 let mut payload = serde_json::to_value(&expected).unwrap();
1334 payload["hash"] = json!(expected.precomputed_hash() ^ u64::MAX);
1335 let repaired: DataType = serde_json::from_value(payload).unwrap();
1336
1337 let serialized = serde_json::to_value(&repaired).unwrap();
1338 assert!(serialized.get("topic").is_some());
1339 assert!(serialized.get("hash").is_some());
1340
1341 let legacy: LegacyDataType = serde_json::from_value(serialized).unwrap();
1342 assert_eq!(legacy.type_name, expected.type_name());
1343 assert_eq!(legacy.metadata.as_ref(), expected.metadata());
1344 assert_eq!(legacy.topic, expected.topic());
1345 assert_eq!(legacy.hash, expected.precomputed_hash());
1346 assert_eq!(legacy.identifier.as_deref(), expected.identifier());
1347 }
1348
1349 #[rstest]
1350 fn test_data_type_display() {
1351 let metadata = Some(params_from_json(json!({"key1": "value1"})));
1352 let data_type = DataType::new("ExampleType", metadata, None);
1353
1354 assert_eq!(format!("{data_type}"), "ExampleType.key1=value1");
1355 }
1356
1357 #[rstest]
1358 fn test_data_type_debug() {
1359 let metadata = Some(params_from_json(json!({"key1": "value1"})));
1360 let data_type = DataType::new("ExampleType", metadata.clone(), None);
1361
1362 assert_eq!(
1363 format!("{data_type:?}"),
1364 format!("DataType(type_name=ExampleType, metadata={metadata:?}, identifier=None)")
1365 );
1366 }
1367
1368 #[rstest]
1369 fn test_parse_instrument_id_from_metadata() {
1370 let instrument_id_str = "MSFT.XNAS";
1371 let metadata = Some(params_from_json(
1372 json!({"instrument_id": instrument_id_str}),
1373 ));
1374 let data_type = DataType::new("InstrumentAny", metadata, None);
1375
1376 assert_eq!(
1377 data_type.instrument_id().unwrap(),
1378 InstrumentId::from_str(instrument_id_str).unwrap()
1379 );
1380 }
1381
1382 #[rstest]
1383 fn test_parse_venue_from_metadata() {
1384 let venue_str = "BINANCE";
1385 let metadata = Some(params_from_json(json!({"venue": venue_str})));
1386 let data_type = DataType::new(stringify!(InstrumentAny), metadata, None);
1387
1388 assert_eq!(data_type.venue().unwrap(), Venue::new(venue_str));
1389 }
1390
1391 #[rstest]
1392 fn test_parse_start_from_metadata() {
1393 let start_ns = 1_600_054_595_844_758_000;
1394 let metadata = Some(params_from_json(json!({"start": start_ns.to_string()})));
1395 let data_type = DataType::new(stringify!(TradeTick), metadata, None);
1396
1397 assert_eq!(data_type.start().unwrap(), UnixNanos::from(start_ns),);
1398 }
1399
1400 #[rstest]
1401 fn test_parse_end_from_metadata() {
1402 let end_ns = 1_720_954_595_844_758_000;
1403 let metadata = Some(params_from_json(json!({"end": end_ns.to_string()})));
1404 let data_type = DataType::new(stringify!(TradeTick), metadata, None);
1405
1406 assert_eq!(data_type.end().unwrap(), UnixNanos::from(end_ns),);
1407 }
1408
1409 #[rstest]
1410 fn test_parse_limit_from_metadata() {
1411 let limit = 1000;
1412 let metadata = Some(params_from_json(json!({"limit": limit})));
1413 let data_type = DataType::new(stringify!(TradeTick), metadata, None);
1414
1415 assert_eq!(data_type.limit().unwrap(), limit);
1416 }
1417
1418 #[rstest]
1419 fn test_data_type_metadata_accessors_return_none_without_metadata() {
1420 let data_type = DataType::new(stringify!(TradeTick), None, None);
1421
1422 assert_eq!(data_type.instrument_id(), None);
1423 assert_eq!(data_type.venue(), None);
1424 assert_eq!(data_type.start(), None);
1425 assert_eq!(data_type.end(), None);
1426 }
1427
1428 #[rstest]
1429 fn test_data_type_persistence_json_with_identifier() {
1430 let data_type = DataType::new("MyCustomType", None, Some("venue//symbol".to_string()));
1431 let json = data_type.to_persistence_json().unwrap();
1432 assert!(!json.contains("topic"));
1433 assert!(json.contains("\"identifier\":\"venue//symbol\""));
1434 let restored = DataType::from_persistence_json(&json).unwrap();
1435 assert_eq!(restored.type_name(), "MyCustomType");
1436 assert_eq!(restored.identifier(), Some("venue//symbol"));
1437 assert_eq!(restored.topic(), "MyCustomType");
1438 }
1439
1440 #[rstest]
1441 fn test_data_type_from_persistence_json_rebuilds_canonical_topic() {
1442 let json = r#"{
1443 "type_name": "ExampleType",
1444 "topic": "ExampleType.z=9.a=1",
1445 "metadata": {"z": 9, "a": 1}
1446 }"#;
1447
1448 let restored = DataType::from_persistence_json(json).unwrap();
1449
1450 assert_eq!(restored.topic(), "ExampleType.a=1.z=9");
1451 }
1452
1453 #[rstest]
1454 fn test_data_type_persistence_result_hashes_like_equal_deserialized_value() {
1455 let persistence_json = r#"{
1456 "type_name": "ExampleType",
1457 "topic": "ignored.legacy.topic",
1458 "metadata": {"z": 9, "a": 1},
1459 "identifier": "catalog/path"
1460 }"#;
1461 let persisted = DataType::from_persistence_json(persistence_json).unwrap();
1462 let payload = json!({
1463 "type_name": persisted.type_name(),
1464 "metadata": persisted.metadata(),
1465 "topic": persisted.topic(),
1466 "hash": persisted.precomputed_hash() ^ u64::MAX,
1467 "identifier": persisted.identifier(),
1468 });
1469 let deserialized: DataType = serde_json::from_value(payload).unwrap();
1470
1471 assert_eq!(persisted.topic(), "ExampleType.a=1.z=9");
1472 assert_eq!(persisted.identifier(), Some("catalog/path"));
1473 assert_eq!(deserialized, persisted);
1474 assert_eq!(hash_data_type(&deserialized), hash_data_type(&persisted));
1475 }
1476
1477 #[rstest]
1478 fn test_data_type_identifier_getter() {
1479 let data_type = DataType::new("T", None, Some("id".to_string()));
1480 assert_eq!(data_type.identifier(), Some("id"));
1481 let data_type_no_id = DataType::new("T", None, None);
1482 assert_eq!(data_type_no_id.identifier(), None);
1483 }
1484}