Skip to main content

nautilus_model/data/
mod.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Data types for the trading domain model.
17
18pub 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// Re-exports
56#[rustfmt::skip]  // Keep these grouped
57pub 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;
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/// A built-in Nautilus data type.
101///
102/// Not recommended for storing large amounts of data, as the largest variant is significantly
103/// larger (10x) than the smallest.
104#[derive(Debug)]
105pub enum Data {
106    Delta(OrderBookDelta),
107    Deltas(Box<OrderBookDeltas>),
108    Depth10(Box<OrderBookDepth10>), // This variant is significantly larger
109    Quote(QuoteTick),
110    Trade(TradeTick),
111    Bar(Bar),
112    MarkPrice(MarkPriceUpdate),
113    IndexPrice(IndexPriceUpdate),
114    FundingRate(FundingRateUpdate),
115    OptionGreeks(OptionGreeks),
116    InstrumentStatus(InstrumentStatus),
117    InstrumentClose(InstrumentClose),
118    Custom(CustomData),
119    #[cfg(feature = "defi")]
120    Defi(Box<DefiData>), // This variant is significantly larger
121}
122
123impl<'de> Deserialize<'de> for Data {
124    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
125    where
126        D: serde::Deserializer<'de>,
127    {
128        use serde::de::Error;
129        let value = serde_json::Value::deserialize(deserializer)?;
130        let type_name = value
131            .get("type")
132            .and_then(|v| v.as_str())
133            .ok_or_else(|| D::Error::custom("Missing 'type' field in Data"))?
134            .to_string();
135
136        match type_name.as_str() {
137            "OrderBookDelta" => Ok(Self::Delta(
138                serde_json::from_value(value).map_err(D::Error::custom)?,
139            )),
140            "OrderBookDeltas" => Ok(Self::Deltas(
141                serde_json::from_value(value).map_err(D::Error::custom)?,
142            )),
143            "OrderBookDepth10" => Ok(Self::Depth10(
144                serde_json::from_value(value).map_err(D::Error::custom)?,
145            )),
146            "QuoteTick" => Ok(Self::Quote(
147                serde_json::from_value(value).map_err(D::Error::custom)?,
148            )),
149            "TradeTick" => Ok(Self::Trade(
150                serde_json::from_value(value).map_err(D::Error::custom)?,
151            )),
152            "Bar" => Ok(Self::Bar(
153                serde_json::from_value(value).map_err(D::Error::custom)?,
154            )),
155            "MarkPriceUpdate" => Ok(Self::MarkPrice(
156                serde_json::from_value(value).map_err(D::Error::custom)?,
157            )),
158            "IndexPriceUpdate" => Ok(Self::IndexPrice(
159                serde_json::from_value(value).map_err(D::Error::custom)?,
160            )),
161            "FundingRateUpdate" => Ok(Self::FundingRate(
162                serde_json::from_value(value).map_err(D::Error::custom)?,
163            )),
164            "OptionGreeks" => Ok(Self::OptionGreeks(
165                serde_json::from_value(value).map_err(D::Error::custom)?,
166            )),
167            "InstrumentStatus" => Ok(Self::InstrumentStatus(
168                serde_json::from_value(value).map_err(D::Error::custom)?,
169            )),
170            "InstrumentClose" => Ok(Self::InstrumentClose(
171                serde_json::from_value(value).map_err(D::Error::custom)?,
172            )),
173            _ => {
174                if let Some(data) =
175                    deserialize_custom_from_json(&type_name, &value).map_err(D::Error::custom)?
176                {
177                    Ok(data)
178                } else {
179                    Err(D::Error::custom(format!("Unknown Data type: {type_name}")))
180                }
181            }
182        }
183    }
184}
185
186impl Clone for Data {
187    fn clone(&self) -> Self {
188        match self {
189            Self::Delta(x) => Self::Delta(*x),
190            Self::Deltas(x) => Self::Deltas(x.clone()),
191            Self::Depth10(x) => Self::Depth10(x.clone()),
192            Self::Quote(x) => Self::Quote(*x),
193            Self::Trade(x) => Self::Trade(*x),
194            Self::Bar(x) => Self::Bar(*x),
195            Self::MarkPrice(x) => Self::MarkPrice(*x),
196            Self::IndexPrice(x) => Self::IndexPrice(*x),
197            Self::FundingRate(x) => Self::FundingRate(*x),
198            Self::OptionGreeks(x) => Self::OptionGreeks(*x),
199            Self::InstrumentStatus(x) => Self::InstrumentStatus(*x),
200            Self::InstrumentClose(x) => Self::InstrumentClose(*x),
201            Self::Custom(x) => Self::Custom(x.clone()),
202            #[cfg(feature = "defi")]
203            Self::Defi(x) => Self::Defi(x.clone()),
204        }
205    }
206}
207
208impl PartialEq for Data {
209    fn eq(&self, other: &Self) -> bool {
210        match (self, other) {
211            (Self::Delta(a), Self::Delta(b)) => a == b,
212            (Self::Deltas(a), Self::Deltas(b)) => a == b,
213            (Self::Depth10(a), Self::Depth10(b)) => a == b,
214            (Self::Quote(a), Self::Quote(b)) => a == b,
215            (Self::Trade(a), Self::Trade(b)) => a == b,
216            (Self::Bar(a), Self::Bar(b)) => a == b,
217            (Self::MarkPrice(a), Self::MarkPrice(b)) => a == b,
218            (Self::IndexPrice(a), Self::IndexPrice(b)) => a == b,
219            (Self::FundingRate(a), Self::FundingRate(b)) => a == b,
220            (Self::OptionGreeks(a), Self::OptionGreeks(b)) => a == b,
221            (Self::InstrumentStatus(a), Self::InstrumentStatus(b)) => a == b,
222            (Self::InstrumentClose(a), Self::InstrumentClose(b)) => a == b,
223            (Self::Custom(a), Self::Custom(b)) => a == b,
224            #[cfg(feature = "defi")]
225            (Self::Defi(a), Self::Defi(b)) => a == b,
226            _ => false,
227        }
228    }
229}
230
231impl Serialize for Data {
232    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
233    where
234        S: serde::Serializer,
235    {
236        match self {
237            Self::Delta(x) => x.serialize(serializer),
238            Self::Deltas(x) => x.serialize(serializer),
239            Self::Depth10(x) => x.serialize(serializer),
240            Self::Quote(x) => x.serialize(serializer),
241            Self::Trade(x) => x.serialize(serializer),
242            Self::Bar(x) => x.serialize(serializer),
243            Self::MarkPrice(x) => x.serialize(serializer),
244            Self::IndexPrice(x) => x.serialize(serializer),
245            Self::FundingRate(x) => x.serialize(serializer),
246            Self::OptionGreeks(x) => x.serialize(serializer),
247            Self::InstrumentStatus(x) => x.serialize(serializer),
248            Self::InstrumentClose(x) => x.serialize(serializer),
249            Self::Custom(x) => x.serialize(serializer),
250            #[cfg(feature = "defi")]
251            Self::Defi(_) => Err(serde::ser::Error::custom(
252                "Data::Defi serialization is not supported",
253            )),
254        }
255    }
256}
257
258macro_rules! impl_try_from_data {
259    ($variant:ident, $type:ty) => {
260        impl TryFrom<Data> for $type {
261            type Error = ();
262
263            fn try_from(value: Data) -> Result<Self, Self::Error> {
264                match value {
265                    Data::$variant(x) => Ok(x),
266                    _ => Err(()),
267                }
268            }
269        }
270    };
271}
272
273impl TryFrom<Data> for OrderBookDepth10 {
274    type Error = ();
275
276    fn try_from(value: Data) -> Result<Self, Self::Error> {
277        match value {
278            Data::Depth10(x) => Ok(*x),
279            _ => Err(()),
280        }
281    }
282}
283
284impl TryFrom<Data> for OrderBookDeltas {
285    type Error = ();
286
287    fn try_from(value: Data) -> Result<Self, Self::Error> {
288        match value {
289            Data::Deltas(x) => Ok(*x),
290            _ => Err(()),
291        }
292    }
293}
294
295impl_try_from_data!(Quote, QuoteTick);
296impl_try_from_data!(Delta, OrderBookDelta);
297impl_try_from_data!(Trade, TradeTick);
298impl_try_from_data!(Bar, Bar);
299impl_try_from_data!(MarkPrice, MarkPriceUpdate);
300impl_try_from_data!(IndexPrice, IndexPriceUpdate);
301impl_try_from_data!(FundingRate, FundingRateUpdate);
302impl_try_from_data!(OptionGreeks, OptionGreeks);
303impl_try_from_data!(InstrumentStatus, InstrumentStatus);
304impl_try_from_data!(InstrumentClose, InstrumentClose);
305
306/// Converts a vector of `Data` items to a specific variant type.
307///
308/// Filters and converts the data vector, keeping only items that can be
309/// successfully converted to the target type `T`.
310#[must_use]
311pub fn to_variant<T: TryFrom<Data>>(data: Vec<Data>) -> Vec<T> {
312    data.into_iter()
313        .filter_map(|d| T::try_from(d).ok())
314        .collect()
315}
316
317impl Data {
318    /// Returns the instrument ID for the data.
319    #[must_use]
320    pub fn instrument_id(&self) -> InstrumentId {
321        match self {
322            Self::Delta(delta) => delta.instrument_id,
323            Self::Deltas(deltas) => deltas.instrument_id,
324            Self::Depth10(depth) => depth.instrument_id,
325            Self::Quote(quote) => quote.instrument_id,
326            Self::Trade(trade) => trade.instrument_id,
327            Self::Bar(bar) => bar.bar_type.instrument_id(),
328            Self::MarkPrice(mark_price) => mark_price.instrument_id,
329            Self::IndexPrice(index_price) => index_price.instrument_id,
330            Self::FundingRate(funding_rate) => funding_rate.instrument_id,
331            Self::OptionGreeks(greeks) => greeks.instrument_id,
332            Self::InstrumentStatus(status) => status.instrument_id,
333            Self::InstrumentClose(close) => close.instrument_id,
334            Self::Custom(custom) => custom
335                .data_type
336                .identifier()
337                .and_then(|s| InstrumentId::from_str(s).ok())
338                .or_else(|| {
339                    custom
340                        .data_type
341                        .metadata()
342                        .and_then(|m| m.get_str("instrument_id"))
343                        .and_then(|s| InstrumentId::from_str(s).ok())
344                })
345                .unwrap_or_else(|| InstrumentId::from("NULL.NULL")),
346            #[cfg(feature = "defi")]
347            Self::Defi(defi) => defi.instrument_id(),
348        }
349    }
350
351    /// Returns whether the data is a type of order book data.
352    #[must_use]
353    pub fn is_order_book_data(&self) -> bool {
354        matches!(self, Self::Delta(_) | Self::Deltas(_) | Self::Depth10(_))
355    }
356}
357
358/// Marker trait for types that carry a creation timestamp.
359///
360/// `ts_init` is the moment (UNIX nanoseconds) when this value was first generated or
361/// ingested by Nautilus. It can be used for sequencing, latency measurements,
362/// or monitoring data-pipeline delays.
363pub trait HasTsInit {
364    /// Returns the UNIX timestamp (nanoseconds) when the instance was created.
365    fn ts_init(&self) -> UnixNanos;
366}
367
368/// Trait for data types that have a catalog path prefix.
369pub trait CatalogPathPrefix {
370    /// Returns the path prefix (directory name) for this data type.
371    fn path_prefix() -> &'static str;
372}
373
374/// Macro for implementing [`CatalogPathPrefix`] for data types.
375///
376/// This macro provides a convenient way to implement the trait for multiple types
377/// with their corresponding path prefixes.
378///
379/// # Parameters
380///
381/// - `$type`: The data type to implement the trait for.
382/// - `$path`: The path prefix string for that type.
383#[macro_export]
384macro_rules! impl_catalog_path_prefix {
385    ($type:ty, $path:expr) => {
386        impl $crate::data::CatalogPathPrefix for $type {
387            fn path_prefix() -> &'static str {
388                $path
389            }
390        }
391    };
392}
393
394// Standard implementations for financial data types
395impl_catalog_path_prefix!(QuoteTick, "quotes");
396impl_catalog_path_prefix!(TradeTick, "trades");
397impl_catalog_path_prefix!(OrderBookDelta, "order_book_deltas");
398impl_catalog_path_prefix!(OrderBookDepth10, "order_book_depths");
399impl_catalog_path_prefix!(Bar, "bars");
400impl_catalog_path_prefix!(IndexPriceUpdate, "index_prices");
401impl_catalog_path_prefix!(MarkPriceUpdate, "mark_prices");
402impl_catalog_path_prefix!(FundingRateUpdate, "funding_rate_update");
403impl_catalog_path_prefix!(OptionGreeks, "option_greeks");
404impl_catalog_path_prefix!(InstrumentStatus, "instrument_status");
405impl_catalog_path_prefix!(InstrumentClose, "instrument_closes");
406
407use crate::instruments::InstrumentAny;
408impl_catalog_path_prefix!(InstrumentAny, "instruments");
409
410impl HasTsInit for Data {
411    fn ts_init(&self) -> UnixNanos {
412        match self {
413            Self::Delta(d) => d.ts_init,
414            Self::Deltas(d) => d.ts_init,
415            Self::Depth10(d) => d.ts_init,
416            Self::Quote(q) => q.ts_init,
417            Self::Trade(t) => t.ts_init,
418            Self::Bar(b) => b.ts_init,
419            Self::MarkPrice(p) => p.ts_init,
420            Self::IndexPrice(p) => p.ts_init,
421            Self::FundingRate(f) => f.ts_init,
422            Self::OptionGreeks(g) => g.ts_init,
423            Self::InstrumentStatus(s) => s.ts_init,
424            Self::InstrumentClose(c) => c.ts_init,
425            Self::Custom(c) => c.data.ts_init(),
426            #[cfg(feature = "defi")]
427            Self::Defi(d) => d.ts_init(),
428        }
429    }
430}
431
432/// Checks if the data slice is monotonically increasing by initialization timestamp.
433///
434/// Returns `true` if each element's `ts_init` is less than or equal to the next element's `ts_init`.
435pub fn is_monotonically_increasing_by_init<T: HasTsInit>(data: &[T]) -> bool {
436    data.array_windows()
437        .all(|[a, b]| a.ts_init() <= b.ts_init())
438}
439
440impl From<OrderBookDelta> for Data {
441    fn from(value: OrderBookDelta) -> Self {
442        Self::Delta(value)
443    }
444}
445
446impl From<OrderBookDeltas> for Data {
447    fn from(value: OrderBookDeltas) -> Self {
448        Self::Deltas(Box::new(value))
449    }
450}
451
452impl From<OrderBookDepth10> for Data {
453    fn from(value: OrderBookDepth10) -> Self {
454        Self::Depth10(Box::new(value))
455    }
456}
457
458impl From<QuoteTick> for Data {
459    fn from(value: QuoteTick) -> Self {
460        Self::Quote(value)
461    }
462}
463
464impl From<TradeTick> for Data {
465    fn from(value: TradeTick) -> Self {
466        Self::Trade(value)
467    }
468}
469
470impl From<Bar> for Data {
471    fn from(value: Bar) -> Self {
472        Self::Bar(value)
473    }
474}
475
476impl From<MarkPriceUpdate> for Data {
477    fn from(value: MarkPriceUpdate) -> Self {
478        Self::MarkPrice(value)
479    }
480}
481
482impl From<IndexPriceUpdate> for Data {
483    fn from(value: IndexPriceUpdate) -> Self {
484        Self::IndexPrice(value)
485    }
486}
487
488impl From<FundingRateUpdate> for Data {
489    fn from(value: FundingRateUpdate) -> Self {
490        Self::FundingRate(value)
491    }
492}
493
494impl From<OptionGreeks> for Data {
495    fn from(value: OptionGreeks) -> Self {
496        Self::OptionGreeks(value)
497    }
498}
499
500impl From<InstrumentStatus> for Data {
501    fn from(value: InstrumentStatus) -> Self {
502        Self::InstrumentStatus(value)
503    }
504}
505
506impl From<InstrumentClose> for Data {
507    fn from(value: InstrumentClose) -> Self {
508        Self::InstrumentClose(value)
509    }
510}
511
512#[cfg(feature = "defi")]
513impl From<DefiData> for Data {
514    fn from(value: DefiData) -> Self {
515        Self::Defi(Box::new(value))
516    }
517}
518
519/// Builds a string-only view of a JSON value for use in topic (key=value).
520fn value_to_topic_string(v: &JsonValue) -> String {
521    if let Some(s) = v.as_str() {
522        return s.to_string();
523    }
524
525    if let Some(n) = v.as_u64() {
526        return n.to_string();
527    }
528
529    if let Some(n) = v.as_i64() {
530        return n.to_string();
531    }
532
533    if let Some(b) = v.as_bool() {
534        return b.to_string();
535    }
536
537    if let Some(f) = v.as_f64() {
538        return f.to_string();
539    }
540
541    if v.is_null() {
542        return "null".to_string();
543    }
544    serde_json::to_string(v).unwrap_or_default()
545}
546
547/// Builds the topic suffix from Params (string-only view: key=value joined by ".").
548fn params_to_topic_suffix(params: &Params) -> String {
549    let mut entries = params.iter().collect::<Vec<_>>();
550    entries.sort_by_key(|(key, _)| *key);
551
552    entries
553        .into_iter()
554        .map(|(k, v)| format!("{k}={}", value_to_topic_string(v)))
555        .collect::<Vec<_>>()
556        .join(".")
557}
558
559/// Represents a data type including metadata.
560#[derive(Clone, Serialize)]
561#[cfg_attr(
562    feature = "python",
563    pyo3::pyclass(module = "nautilus_trader.model", from_py_object)
564)]
565#[cfg_attr(
566    feature = "python",
567    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
568)]
569pub struct DataType {
570    type_name: String,
571    metadata: Option<Params>,
572    topic: String,
573    hash: u64,
574    identifier: Option<String>,
575}
576
577impl<'de> Deserialize<'de> for DataType {
578    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
579    where
580        D: serde::Deserializer<'de>,
581    {
582        const FIELDS: &[&str] = &["type_name", "metadata", "topic", "hash", "identifier"];
583
584        #[derive(Deserialize)]
585        #[serde(field_identifier, rename_all = "snake_case")]
586        enum Field {
587            TypeName,
588            Metadata,
589            Topic,
590            Hash,
591            Identifier,
592            #[serde(other)]
593            Other,
594        }
595
596        fn finish(
597            type_name: &str,
598            metadata: Option<Params>,
599            topic: Option<String>,
600            identifier: Option<String>,
601        ) -> DataType {
602            let mut data_type = DataType::new(type_name, metadata, identifier);
603
604            if let Some(topic) = topic {
605                let mut hasher = std::collections::hash_map::DefaultHasher::new();
606                topic.hash(&mut hasher);
607                data_type.topic = topic;
608                data_type.hash = hasher.finish();
609            }
610
611            data_type
612        }
613
614        struct DataTypeVisitor;
615
616        impl<'de> Visitor<'de> for DataTypeVisitor {
617            type Value = DataType;
618
619            fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
620                formatter.write_str("struct DataType")
621            }
622
623            fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
624            where
625                A: SeqAccess<'de>,
626            {
627                let type_name: String = seq
628                    .next_element()?
629                    .ok_or_else(|| de::Error::invalid_length(0, &self))?;
630                // A non-empty Params cannot be decoded here by a non-self-describing format:
631                // it stores serde_json::Value, whose Deserialize requires deserialize_any.
632                // That is a pre-existing Params limitation, not one this path introduces.
633                let metadata = seq
634                    .next_element()?
635                    .ok_or_else(|| de::Error::invalid_length(1, &self))?;
636                let topic = seq
637                    .next_element()?
638                    .ok_or_else(|| de::Error::invalid_length(2, &self))?;
639                let _hash: u64 = seq
640                    .next_element()?
641                    .ok_or_else(|| de::Error::invalid_length(3, &self))?;
642                let identifier = seq
643                    .next_element()?
644                    .ok_or_else(|| de::Error::invalid_length(4, &self))?;
645
646                Ok(finish(&type_name, metadata, Some(topic), identifier))
647            }
648
649            fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
650            where
651                A: MapAccess<'de>,
652            {
653                let mut type_name: Option<String> = None;
654                let mut metadata = None;
655                let mut topic = None;
656                let mut hash_seen = false;
657                let mut identifier = None;
658
659                while let Some(key) = map.next_key()? {
660                    match key {
661                        Field::TypeName => {
662                            if type_name.is_some() {
663                                return Err(de::Error::duplicate_field("type_name"));
664                            }
665                            type_name = Some(map.next_value()?);
666                        }
667                        Field::Metadata => {
668                            if metadata.is_some() {
669                                return Err(de::Error::duplicate_field("metadata"));
670                            }
671                            metadata = Some(map.next_value()?);
672                        }
673                        Field::Topic => {
674                            if topic.is_some() {
675                                return Err(de::Error::duplicate_field("topic"));
676                            }
677                            topic = Some(map.next_value()?);
678                        }
679                        Field::Hash => {
680                            if hash_seen {
681                                return Err(de::Error::duplicate_field("hash"));
682                            }
683                            hash_seen = true;
684                            let _: Option<u64> = map.next_value()?;
685                        }
686                        Field::Identifier => {
687                            if identifier.is_some() {
688                                return Err(de::Error::duplicate_field("identifier"));
689                            }
690                            identifier = Some(map.next_value()?);
691                        }
692                        Field::Other => {
693                            let _: IgnoredAny = map.next_value()?;
694                        }
695                    }
696                }
697
698                let type_name = type_name.ok_or_else(|| de::Error::missing_field("type_name"))?;
699                Ok(finish(
700                    &type_name,
701                    metadata.unwrap_or(None),
702                    topic.unwrap_or(None),
703                    identifier.unwrap_or(None),
704                ))
705            }
706        }
707
708        deserializer.deserialize_struct("DataType", FIELDS, DataTypeVisitor)
709    }
710}
711
712impl DataType {
713    /// Creates a new [`DataType`] instance.
714    #[must_use]
715    pub fn new(type_name: &str, metadata: Option<Params>, identifier: Option<String>) -> Self {
716        // Precompute topic from type_name + metadata (string-only view for backward compatibility)
717        let topic = if let Some(ref meta) = metadata {
718            if meta.is_empty() {
719                type_name.to_string()
720            } else {
721                format!("{type_name}.{}", params_to_topic_suffix(meta))
722            }
723        } else {
724            type_name.to_string()
725        };
726
727        // Precompute hash
728        let mut hasher = std::collections::hash_map::DefaultHasher::new();
729        topic.hash(&mut hasher);
730
731        Self {
732            type_name: type_name.to_owned(),
733            metadata,
734            topic,
735            hash: hasher.finish(),
736            identifier,
737        }
738    }
739
740    /// Creates a [`DataType`] from persisted parts (`type_name`, topic, metadata).
741    /// Hash is recomputed from topic. Use when restoring from legacy `data_type` column.
742    /// Identifier is set to None.
743    #[must_use]
744    pub fn from_parts(type_name: &str, topic: &str, metadata: Option<Params>) -> Self {
745        let mut hasher = std::collections::hash_map::DefaultHasher::new();
746        topic.hash(&mut hasher);
747        Self {
748            type_name: type_name.to_owned(),
749            metadata,
750            topic: topic.to_owned(),
751            hash: hasher.finish(),
752            identifier: None,
753        }
754    }
755
756    /// Serializes to JSON for persistence (`type_name`, metadata, identifier; no topic, no hash).
757    ///
758    /// # Errors
759    ///
760    /// Returns a JSON serialization error if the data cannot be serialized.
761    pub fn to_persistence_json(&self) -> Result<String, serde_json::Error> {
762        let mut map = serde_json::Map::new();
763        map.insert(
764            "type_name".to_string(),
765            serde_json::Value::String(self.type_name.clone()),
766        );
767        map.insert(
768            "metadata".to_string(),
769            self.metadata.as_ref().map_or(serde_json::Value::Null, |m| {
770                serde_json::to_value(m).unwrap_or(serde_json::Value::Null)
771            }),
772        );
773
774        if let Some(ref id) = self.identifier {
775            map.insert(
776                "identifier".to_string(),
777                serde_json::Value::String(id.clone()),
778            );
779        }
780        serde_json::to_string(&serde_json::Value::Object(map))
781    }
782
783    /// Deserializes from JSON produced by `to_persistence_json`.
784    /// Accepts legacy JSON with `topic` (ignored); topic is rebuilt from `type_name` + metadata.
785    ///
786    /// # Errors
787    ///
788    /// Returns an error if the string is not valid JSON or missing required fields.
789    pub fn from_persistence_json(s: &str) -> Result<Self, anyhow::Error> {
790        let value: serde_json::Value =
791            serde_json::from_str(s).map_err(|e| anyhow::anyhow!("Invalid data_type JSON: {e}"))?;
792        let obj = value
793            .as_object()
794            .ok_or_else(|| anyhow::anyhow!("data_type must be a JSON object"))?;
795        let type_name = obj
796            .get("type_name")
797            .and_then(|v| v.as_str())
798            .ok_or_else(|| anyhow::anyhow!("data_type must have type_name"))?
799            .to_string();
800        let metadata = obj.get("metadata").and_then(|m| {
801            if m.is_null() {
802                None
803            } else {
804                let p: Params = serde_json::from_value(m.clone()).ok()?;
805                if p.is_empty() { None } else { Some(p) }
806            }
807        });
808        let identifier = obj
809            .get("identifier")
810            .and_then(|v| v.as_str())
811            .map(String::from);
812        Ok(Self::new(&type_name, metadata, identifier))
813    }
814
815    /// Returns the type name for the data type.
816    #[must_use]
817    pub fn type_name(&self) -> &str {
818        self.type_name.as_str()
819    }
820
821    /// Returns the metadata for the data type.
822    #[must_use]
823    pub fn metadata(&self) -> Option<&Params> {
824        self.metadata.as_ref()
825    }
826
827    /// Returns a string representation of the metadata.
828    #[must_use]
829    pub fn metadata_str(&self) -> String {
830        self.metadata.as_ref().map_or_else(
831            || "null".to_string(),
832            |metadata| {
833                let mut entries = metadata.iter().collect::<Vec<_>>();
834                entries.sort_by_key(|(key, _)| *key);
835
836                let mut metadata_map = serde_json::Map::new();
837                for (key, value) in entries {
838                    metadata_map.insert(key.clone(), value.clone());
839                }
840
841                serde_json::to_string(&metadata_map).unwrap_or_default()
842            },
843        )
844    }
845
846    /// Returns metadata as a string-only map (e.g. for Arrow schema metadata).
847    #[must_use]
848    pub fn metadata_string_map(&self) -> Option<std::collections::HashMap<String, String>> {
849        self.metadata.as_ref().map(|p| {
850            p.iter()
851                .map(|(k, v)| (k.clone(), value_to_topic_string(v)))
852                .collect()
853        })
854    }
855
856    /// Returns the precomputed hash for this data type.
857    #[must_use]
858    pub fn precomputed_hash(&self) -> u64 {
859        self.hash
860    }
861
862    /// Returns the messaging topic for the data type.
863    #[must_use]
864    pub fn topic(&self) -> &str {
865        self.topic.as_str()
866    }
867
868    /// Returns the optional catalog path identifier (can contain subdirs, e.g. `"venue//symbol"`).
869    #[must_use]
870    pub fn identifier(&self) -> Option<&str> {
871        self.identifier.as_deref()
872    }
873
874    /// Returns an [`Option<InstrumentId>`] parsed from the metadata.
875    ///
876    /// # Panics
877    ///
878    /// This function panics if:
879    /// - The `instrument_id` value contained in the metadata is invalid.
880    #[must_use]
881    pub fn instrument_id(&self) -> Option<InstrumentId> {
882        let metadata = self.metadata.as_ref()?;
883        let instrument_id = metadata.get_str("instrument_id")?;
884        Some(
885            InstrumentId::from_str(instrument_id)
886                .expect("Invalid `InstrumentId` for 'instrument_id'"),
887        )
888    }
889
890    /// Returns an [`Option<Venue>`] parsed from the metadata.
891    ///
892    /// # Panics
893    ///
894    /// This function panics if:
895    /// - The `venue` value contained in the metadata is invalid.
896    #[must_use]
897    pub fn venue(&self) -> Option<Venue> {
898        let metadata = self.metadata.as_ref()?;
899        let venue_str = metadata.get_str("venue")?;
900        Some(Venue::from(venue_str))
901    }
902
903    /// Returns an [`Option<UnixNanos>`] parsed from the metadata `start` field.
904    ///
905    /// # Panics
906    ///
907    /// This function panics if:
908    /// - The `start` value contained in the metadata is invalid.
909    #[must_use]
910    pub fn start(&self) -> Option<UnixNanos> {
911        let metadata = self.metadata.as_ref()?;
912        let start_str = metadata.get_str("start")?;
913        Some(UnixNanos::from_str(start_str).expect("Invalid `UnixNanos` for 'start'"))
914    }
915
916    /// Returns an [`Option<UnixNanos>`] parsed from the metadata `end` field.
917    ///
918    /// # Panics
919    ///
920    /// This function panics if:
921    /// - The `end` value contained in the metadata is invalid.
922    #[must_use]
923    pub fn end(&self) -> Option<UnixNanos> {
924        let metadata = self.metadata.as_ref()?;
925        let end_str = metadata.get_str("end")?;
926        Some(UnixNanos::from_str(end_str).expect("Invalid `UnixNanos` for 'end'"))
927    }
928
929    /// Returns an [`Option<usize>`] parsed from the metadata `limit` field.
930    ///
931    /// # Panics
932    ///
933    /// This function panics if:
934    /// - The `limit` value contained in the metadata is invalid.
935    #[must_use]
936    pub fn limit(&self) -> Option<usize> {
937        let metadata = self.metadata.as_ref()?;
938        metadata.get_usize("limit").or_else(|| {
939            metadata
940                .get_str("limit")
941                .map(|s| s.parse::<usize>().expect("Invalid `usize` for 'limit'"))
942        })
943    }
944}
945
946impl PartialEq for DataType {
947    fn eq(&self, other: &Self) -> bool {
948        self.topic == other.topic
949    }
950}
951
952impl Eq for DataType {}
953
954impl PartialOrd for DataType {
955    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
956        Some(self.cmp(other))
957    }
958}
959
960impl Ord for DataType {
961    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
962        self.topic.cmp(&other.topic)
963    }
964}
965
966impl Hash for DataType {
967    fn hash<H: Hasher>(&self, state: &mut H) {
968        self.hash.hash(state);
969    }
970}
971
972impl Display for DataType {
973    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
974        write!(f, "{}", self.topic)
975    }
976}
977
978impl Debug for DataType {
979    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
980        write!(
981            f,
982            "DataType(type_name={}, metadata={:?}, identifier={:?})",
983            self.type_name, self.metadata, self.identifier
984        )
985    }
986}
987
988#[cfg(test)]
989mod tests {
990    use std::hash::DefaultHasher;
991
992    use rstest::*;
993    use serde_json::json;
994
995    use super::*;
996
997    fn params_from_json(value: serde_json::Value) -> Params {
998        serde_json::from_value(value).expect("valid Params JSON")
999    }
1000
1001    fn hash_data_type(data_type: &DataType) -> u64 {
1002        let mut hasher = DefaultHasher::new();
1003        data_type.hash(&mut hasher);
1004        hasher.finish()
1005    }
1006
1007    #[rstest]
1008    fn test_data_type_creation_with_metadata() {
1009        let metadata = Some(params_from_json(
1010            json!({"key1": "value1", "key2": "value2"}),
1011        ));
1012        let data_type = DataType::new("ExampleType", metadata.clone(), None);
1013
1014        assert_eq!(data_type.type_name(), "ExampleType");
1015        assert_eq!(data_type.topic(), "ExampleType.key1=value1.key2=value2");
1016        assert_eq!(data_type.metadata(), metadata.as_ref());
1017    }
1018
1019    #[rstest]
1020    fn test_data_type_topic_identity_uses_canonical_metadata_order() {
1021        let mut metadata1 = Params::new();
1022        metadata1.insert("b".to_string(), json!(2));
1023        metadata1.insert("a".to_string(), json!(1));
1024        let mut metadata2 = Params::new();
1025        metadata2.insert("a".to_string(), json!(1));
1026        metadata2.insert("b".to_string(), json!(2));
1027
1028        let data_type1 = DataType::new("ExampleType", Some(metadata1), None);
1029        let data_type2 = DataType::new("ExampleType", Some(metadata2), None);
1030        let mut hasher1 = DefaultHasher::new();
1031        data_type1.hash(&mut hasher1);
1032        let hash1 = hasher1.finish();
1033        let mut hasher2 = DefaultHasher::new();
1034        data_type2.hash(&mut hasher2);
1035        let hash2 = hasher2.finish();
1036
1037        assert_eq!(data_type1.topic(), "ExampleType.a=1.b=2");
1038        assert_eq!(data_type1.topic(), data_type2.topic());
1039        assert_eq!(data_type1, data_type2);
1040        assert_eq!(hash1, hash2);
1041        assert_eq!(format!("{data_type1}"), format!("{data_type2}"));
1042        assert_eq!(data_type1.metadata_str(), r#"{"a":1,"b":2}"#);
1043        assert_eq!(data_type1.metadata_str(), data_type2.metadata_str());
1044    }
1045
1046    #[rstest]
1047    fn test_data_type_creation_without_metadata() {
1048        let data_type = DataType::new("ExampleType", None, None);
1049
1050        assert_eq!(data_type.type_name(), "ExampleType");
1051        assert_eq!(data_type.topic(), "ExampleType");
1052        assert_eq!(data_type.metadata(), None);
1053    }
1054
1055    #[rstest]
1056    fn test_data_type_equality() {
1057        let metadata1 = Some(params_from_json(json!({"key1": "value1"})));
1058        let metadata2 = Some(params_from_json(json!({"key1": "value1"})));
1059
1060        let data_type1 = DataType::new("ExampleType", metadata1, None);
1061        let data_type2 = DataType::new("ExampleType", metadata2, None);
1062
1063        assert_eq!(data_type1, data_type2);
1064    }
1065
1066    #[rstest]
1067    fn test_data_type_inequality() {
1068        let metadata1 = Some(params_from_json(json!({"key1": "value1"})));
1069        let metadata2 = Some(params_from_json(json!({"key2": "value2"})));
1070
1071        let data_type1 = DataType::new("ExampleType", metadata1, None);
1072        let data_type2 = DataType::new("ExampleType", metadata2, None);
1073
1074        assert_ne!(data_type1, data_type2);
1075    }
1076
1077    #[rstest]
1078    fn test_data_type_ordering() {
1079        let metadata1 = Some(params_from_json(json!({"key1": "value1"})));
1080        let metadata2 = Some(params_from_json(json!({"key2": "value2"})));
1081
1082        let data_type1 = DataType::new("ExampleTypeA", metadata1, None);
1083        let data_type2 = DataType::new("ExampleTypeB", metadata2, None);
1084
1085        assert!(data_type1 < data_type2);
1086    }
1087
1088    #[rstest]
1089    fn test_data_type_hash() {
1090        let metadata = Some(params_from_json(json!({"key1": "value1"})));
1091
1092        let data_type1 = DataType::new("ExampleType", metadata.clone(), None);
1093        let data_type2 = DataType::new("ExampleType", metadata, None);
1094
1095        let mut hasher1 = DefaultHasher::new();
1096        data_type1.hash(&mut hasher1);
1097        let hash1 = hasher1.finish();
1098
1099        let mut hasher2 = DefaultHasher::new();
1100        data_type2.hash(&mut hasher2);
1101        let hash2 = hasher2.finish();
1102
1103        assert_eq!(hash1, hash2);
1104    }
1105
1106    #[rstest]
1107    fn test_data_type_deserialization_recomputes_hash_from_topic() {
1108        let expected = DataType::from_parts(
1109            "ExampleType",
1110            "custom.topic",
1111            Some(params_from_json(json!({"key": "value"}))),
1112        );
1113        let payload = json!({
1114            "type_name": expected.type_name(),
1115            "metadata": expected.metadata(),
1116            "topic": expected.topic(),
1117            "hash": expected.precomputed_hash() ^ u64::MAX,
1118            "identifier": "catalog/path",
1119        });
1120
1121        let deserialized: DataType = serde_json::from_value(payload).unwrap();
1122
1123        assert_eq!(deserialized.topic(), expected.topic());
1124        assert_eq!(deserialized.precomputed_hash(), expected.precomputed_hash());
1125    }
1126
1127    #[rstest]
1128    fn test_data_type_deserialization_without_cache_fields_uses_constructor() {
1129        let payload = json!({
1130            "type_name": "ExampleType",
1131            "metadata": {"z": 9, "a": 1},
1132            "identifier": "catalog/path",
1133        });
1134        let expected = DataType::new(
1135            "ExampleType",
1136            Some(params_from_json(json!({"z": 9, "a": 1}))),
1137            Some("catalog/path".to_string()),
1138        );
1139
1140        let deserialized: DataType = serde_json::from_value(payload).unwrap();
1141
1142        assert_eq!(deserialized.topic(), expected.topic());
1143        assert_eq!(deserialized.precomputed_hash(), expected.precomputed_hash());
1144    }
1145
1146    #[rstest]
1147    fn test_data_type_deserialization_preserves_topic_without_hash() {
1148        let expected = DataType::from_parts("ExampleType", "custom.topic", None);
1149        let payload = json!({
1150            "type_name": "ExampleType",
1151            "metadata": null,
1152            "topic": "custom.topic",
1153        });
1154
1155        let deserialized: DataType = serde_json::from_value(payload).unwrap();
1156
1157        assert_eq!(deserialized.topic(), "custom.topic");
1158        assert_eq!(deserialized.precomputed_hash(), expected.precomputed_hash());
1159    }
1160
1161    #[rstest]
1162    fn test_data_type_deserialization_ignores_hash_without_topic() {
1163        let expected = DataType::new("ExampleType", None, None);
1164        let payload = json!({
1165            "type_name": "ExampleType",
1166            "metadata": null,
1167            "hash": expected.precomputed_hash() ^ u64::MAX,
1168        });
1169
1170        let deserialized: DataType = serde_json::from_value(payload).unwrap();
1171
1172        assert_eq!(deserialized.topic(), expected.topic());
1173        assert_eq!(deserialized.precomputed_hash(), expected.precomputed_hash());
1174    }
1175
1176    #[rstest]
1177    fn test_data_type_deserialization_rejects_duplicate_map_key() {
1178        let payload = r#"{"type_name":"ExampleType","topic":"first","topic":"second"}"#;
1179
1180        let error = serde_json::from_str::<DataType>(payload).unwrap_err();
1181
1182        assert!(error.to_string().contains("duplicate field `topic`"));
1183    }
1184
1185    #[rstest]
1186    #[case(
1187        r#"{"type_name":"ExampleType","topic":null,"topic":"second"}"#,
1188        "duplicate field `topic`"
1189    )]
1190    #[case(
1191        r#"{"type_name":"ExampleType","hash":null,"hash":7}"#,
1192        "duplicate field `hash`"
1193    )]
1194    #[case(
1195        r#"{"type_name":"ExampleType","metadata":null,"metadata":{"a":1}}"#,
1196        "duplicate field `metadata`"
1197    )]
1198    #[case(
1199        r#"{"type_name":"ExampleType","identifier":null,"identifier":"second"}"#,
1200        "duplicate field `identifier`"
1201    )]
1202    fn test_data_type_deserialization_rejects_duplicate_map_key_after_null(
1203        #[case] payload: &str,
1204        #[case] expected: &str,
1205    ) {
1206        // A null first occurrence must still count as "seen". A plain Option slot could not
1207        // tell an absent key from an explicit null, and would silently accept the duplicate.
1208        let error = serde_json::from_str::<DataType>(payload).unwrap_err();
1209
1210        assert!(error.to_string().contains(expected));
1211    }
1212
1213    #[rstest]
1214    fn test_data_type_serde_roundtrip_preserves_fields_and_repairs_hash() {
1215        let expected = DataType::from_parts(
1216            "ExampleType",
1217            "custom.topic",
1218            Some(params_from_json(json!({"key": "value"}))),
1219        );
1220        let payload = json!({
1221            "type_name": expected.type_name(),
1222            "metadata": expected.metadata(),
1223            "topic": expected.topic(),
1224            "hash": expected.precomputed_hash() ^ u64::MAX,
1225            "identifier": "catalog/path",
1226        });
1227        let deserialized: DataType = serde_json::from_value(payload).unwrap();
1228
1229        let json = serde_json::to_string(&deserialized).unwrap();
1230        let roundtripped: DataType = serde_json::from_str(&json).unwrap();
1231
1232        assert_eq!(roundtripped.type_name(), "ExampleType");
1233        assert_eq!(roundtripped.metadata(), expected.metadata());
1234        assert_eq!(roundtripped.identifier(), Some("catalog/path"));
1235        assert_eq!(roundtripped.topic(), "custom.topic");
1236        assert_eq!(roundtripped.precomputed_hash(), expected.precomputed_hash());
1237    }
1238
1239    #[rstest]
1240    fn test_data_type_serialized_cache_fields_remain_wire_compatible() {
1241        #[derive(Deserialize)]
1242        struct LegacyDataType {
1243            type_name: String,
1244            metadata: Option<Params>,
1245            topic: String,
1246            hash: u64,
1247            identifier: Option<String>,
1248        }
1249
1250        let expected = DataType::new(
1251            "ExampleType",
1252            Some(params_from_json(json!({"key": "value"}))),
1253            Some("catalog/path".to_string()),
1254        );
1255        let mut payload = serde_json::to_value(&expected).unwrap();
1256        payload["hash"] = json!(expected.precomputed_hash() ^ u64::MAX);
1257        let repaired: DataType = serde_json::from_value(payload).unwrap();
1258
1259        let serialized = serde_json::to_value(&repaired).unwrap();
1260        assert!(serialized.get("topic").is_some());
1261        assert!(serialized.get("hash").is_some());
1262
1263        let legacy: LegacyDataType = serde_json::from_value(serialized).unwrap();
1264        assert_eq!(legacy.type_name, expected.type_name());
1265        assert_eq!(legacy.metadata.as_ref(), expected.metadata());
1266        assert_eq!(legacy.topic, expected.topic());
1267        assert_eq!(legacy.hash, expected.precomputed_hash());
1268        assert_eq!(legacy.identifier.as_deref(), expected.identifier());
1269    }
1270
1271    #[rstest]
1272    fn test_data_type_display() {
1273        let metadata = Some(params_from_json(json!({"key1": "value1"})));
1274        let data_type = DataType::new("ExampleType", metadata, None);
1275
1276        assert_eq!(format!("{data_type}"), "ExampleType.key1=value1");
1277    }
1278
1279    #[rstest]
1280    fn test_data_type_debug() {
1281        let metadata = Some(params_from_json(json!({"key1": "value1"})));
1282        let data_type = DataType::new("ExampleType", metadata.clone(), None);
1283
1284        assert_eq!(
1285            format!("{data_type:?}"),
1286            format!("DataType(type_name=ExampleType, metadata={metadata:?}, identifier=None)")
1287        );
1288    }
1289
1290    #[rstest]
1291    fn test_parse_instrument_id_from_metadata() {
1292        let instrument_id_str = "MSFT.XNAS";
1293        let metadata = Some(params_from_json(
1294            json!({"instrument_id": instrument_id_str}),
1295        ));
1296        let data_type = DataType::new("InstrumentAny", metadata, None);
1297
1298        assert_eq!(
1299            data_type.instrument_id().unwrap(),
1300            InstrumentId::from_str(instrument_id_str).unwrap()
1301        );
1302    }
1303
1304    #[rstest]
1305    fn test_parse_venue_from_metadata() {
1306        let venue_str = "BINANCE";
1307        let metadata = Some(params_from_json(json!({"venue": venue_str})));
1308        let data_type = DataType::new(stringify!(InstrumentAny), metadata, None);
1309
1310        assert_eq!(data_type.venue().unwrap(), Venue::new(venue_str));
1311    }
1312
1313    #[rstest]
1314    fn test_parse_start_from_metadata() {
1315        let start_ns = 1_600_054_595_844_758_000;
1316        let metadata = Some(params_from_json(json!({"start": start_ns.to_string()})));
1317        let data_type = DataType::new(stringify!(TradeTick), metadata, None);
1318
1319        assert_eq!(data_type.start().unwrap(), UnixNanos::from(start_ns),);
1320    }
1321
1322    #[rstest]
1323    fn test_parse_end_from_metadata() {
1324        let end_ns = 1_720_954_595_844_758_000;
1325        let metadata = Some(params_from_json(json!({"end": end_ns.to_string()})));
1326        let data_type = DataType::new(stringify!(TradeTick), metadata, None);
1327
1328        assert_eq!(data_type.end().unwrap(), UnixNanos::from(end_ns),);
1329    }
1330
1331    #[rstest]
1332    fn test_parse_limit_from_metadata() {
1333        let limit = 1000;
1334        let metadata = Some(params_from_json(json!({"limit": limit})));
1335        let data_type = DataType::new(stringify!(TradeTick), metadata, None);
1336
1337        assert_eq!(data_type.limit().unwrap(), limit);
1338    }
1339
1340    #[rstest]
1341    fn test_data_type_metadata_accessors_return_none_without_metadata() {
1342        let data_type = DataType::new(stringify!(TradeTick), None, None);
1343
1344        assert_eq!(data_type.instrument_id(), None);
1345        assert_eq!(data_type.venue(), None);
1346        assert_eq!(data_type.start(), None);
1347        assert_eq!(data_type.end(), None);
1348    }
1349
1350    #[rstest]
1351    fn test_data_type_persistence_json_with_identifier() {
1352        let data_type = DataType::new("MyCustomType", None, Some("venue//symbol".to_string()));
1353        let json = data_type.to_persistence_json().unwrap();
1354        assert!(!json.contains("topic"));
1355        assert!(json.contains("\"identifier\":\"venue//symbol\""));
1356        let restored = DataType::from_persistence_json(&json).unwrap();
1357        assert_eq!(restored.type_name(), "MyCustomType");
1358        assert_eq!(restored.identifier(), Some("venue//symbol"));
1359        assert_eq!(restored.topic(), "MyCustomType");
1360    }
1361
1362    #[rstest]
1363    fn test_data_type_from_persistence_json_rebuilds_canonical_topic() {
1364        let json = r#"{
1365            "type_name": "ExampleType",
1366            "topic": "ExampleType.z=9.a=1",
1367            "metadata": {"z": 9, "a": 1}
1368        }"#;
1369
1370        let restored = DataType::from_persistence_json(json).unwrap();
1371
1372        assert_eq!(restored.topic(), "ExampleType.a=1.z=9");
1373    }
1374
1375    #[rstest]
1376    fn test_data_type_persistence_result_hashes_like_equal_deserialized_value() {
1377        let persistence_json = r#"{
1378            "type_name": "ExampleType",
1379            "topic": "ignored.legacy.topic",
1380            "metadata": {"z": 9, "a": 1},
1381            "identifier": "catalog/path"
1382        }"#;
1383        let persisted = DataType::from_persistence_json(persistence_json).unwrap();
1384        let payload = json!({
1385            "type_name": persisted.type_name(),
1386            "metadata": persisted.metadata(),
1387            "topic": persisted.topic(),
1388            "hash": persisted.precomputed_hash() ^ u64::MAX,
1389            "identifier": persisted.identifier(),
1390        });
1391        let deserialized: DataType = serde_json::from_value(payload).unwrap();
1392
1393        assert_eq!(persisted.topic(), "ExampleType.a=1.z=9");
1394        assert_eq!(persisted.identifier(), Some("catalog/path"));
1395        assert_eq!(deserialized, persisted);
1396        assert_eq!(hash_data_type(&deserialized), hash_data_type(&persisted));
1397    }
1398
1399    #[rstest]
1400    fn test_data_type_identifier_getter() {
1401        let data_type = DataType::new("T", None, Some("id".to_string()));
1402        assert_eq!(data_type.identifier(), Some("id"));
1403        let data_type_no_id = DataType::new("T", None, None);
1404        assert_eq!(data_type_no_id.identifier(), None);
1405    }
1406}