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, 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/// 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(OrderBookDeltas_API),
108    Depth10(Box<OrderBookDepth10>), // This variant is significantly larger
109    Quote(QuoteTick),
110    Trade(TradeTick),
111    Bar(Bar),
112    MarkPriceUpdate(MarkPriceUpdate), // TODO: Rename to MarkPrice once Cython gone
113    IndexPriceUpdate(IndexPriceUpdate), // TODO: Rename to IndexPrice once Cython gone
114    FundingRateUpdate(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
123/// A C-compatible representation of [`Data`] for FFI.
124///
125/// This enum matches the standard variants of [`Data`] but excludes the `Custom`
126/// variant which is not FFI-safe.
127#[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/// Converts a vector of `Data` items to a specific variant type.
365///
366/// Filters and converts the data vector, keeping only items that can be
367/// successfully converted to the target type `T`.
368#[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    /// Returns the instrument ID for the data.
377    #[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    /// Returns whether the data is a type of order book data.
410    #[must_use]
411    pub fn is_order_book_data(&self) -> bool {
412        matches!(self, Self::Delta(_) | Self::Deltas(_) | Self::Depth10(_))
413    }
414}
415
416/// Marker trait for types that carry a creation timestamp.
417///
418/// `ts_init` is the moment (UNIX nanoseconds) when this value was first generated or
419/// ingested by Nautilus. It can be used for sequencing, latency measurements,
420/// or monitoring data-pipeline delays.
421pub trait HasTsInit {
422    /// Returns the UNIX timestamp (nanoseconds) when the instance was created.
423    fn ts_init(&self) -> UnixNanos;
424}
425
426/// Trait for data types that have a catalog path prefix.
427pub trait CatalogPathPrefix {
428    /// Returns the path prefix (directory name) for this data type.
429    fn path_prefix() -> &'static str;
430}
431
432/// Macro for implementing [`CatalogPathPrefix`] for data types.
433///
434/// This macro provides a convenient way to implement the trait for multiple types
435/// with their corresponding path prefixes.
436///
437/// # Parameters
438///
439/// - `$type`: The data type to implement the trait for.
440/// - `$path`: The path prefix string for that type.
441#[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
452// Standard implementations for financial data types
453impl_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
490/// Checks if the data slice is monotonically increasing by initialization timestamp.
491///
492/// Returns `true` if each element's `ts_init` is less than or equal to the next element's `ts_init`.
493pub 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
577/// Builds a string-only view of a JSON value for use in topic (key=value).
578fn 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
605/// Builds the topic suffix from Params (string-only view: key=value joined by ".").
606fn 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/// Represents a data type including metadata.
618#[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                // A non-empty Params cannot be decoded here by a non-self-describing format:
689                // it stores serde_json::Value, whose Deserialize requires deserialize_any.
690                // That is a pre-existing Params limitation, not one this path introduces.
691                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    /// Creates a new [`DataType`] instance.
772    #[must_use]
773    pub fn new(type_name: &str, metadata: Option<Params>, identifier: Option<String>) -> Self {
774        // Precompute topic from type_name + metadata (string-only view for backward compatibility)
775        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        // Precompute hash
786        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    /// Creates a [`DataType`] from persisted parts (`type_name`, topic, metadata).
799    /// Hash is recomputed from topic. Use when restoring from legacy `data_type` column.
800    /// Identifier is set to None.
801    #[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    /// Serializes to JSON for persistence (`type_name`, metadata, identifier; no topic, no hash).
815    ///
816    /// # Errors
817    ///
818    /// Returns a JSON serialization error if the data cannot be serialized.
819    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    /// Deserializes from JSON produced by `to_persistence_json`.
842    /// Accepts legacy JSON with `topic` (ignored); topic is rebuilt from `type_name` + metadata.
843    ///
844    /// # Errors
845    ///
846    /// Returns an error if the string is not valid JSON or missing required fields.
847    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    /// Returns the type name for the data type.
874    #[must_use]
875    pub fn type_name(&self) -> &str {
876        self.type_name.as_str()
877    }
878
879    /// Returns the metadata for the data type.
880    #[must_use]
881    pub fn metadata(&self) -> Option<&Params> {
882        self.metadata.as_ref()
883    }
884
885    /// Returns a string representation of the metadata.
886    #[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    /// Returns metadata as a string-only map (e.g. for Arrow schema metadata).
905    #[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    /// Returns the precomputed hash for this data type.
915    #[must_use]
916    pub fn precomputed_hash(&self) -> u64 {
917        self.hash
918    }
919
920    /// Returns the messaging topic for the data type.
921    #[must_use]
922    pub fn topic(&self) -> &str {
923        self.topic.as_str()
924    }
925
926    /// Returns the optional catalog path identifier (can contain subdirs, e.g. `"venue//symbol"`).
927    #[must_use]
928    pub fn identifier(&self) -> Option<&str> {
929        self.identifier.as_deref()
930    }
931
932    /// Returns an [`Option<InstrumentId>`] parsed from the metadata.
933    ///
934    /// # Panics
935    ///
936    /// This function panics if:
937    /// - The `instrument_id` value contained in the metadata is invalid.
938    #[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    /// Returns an [`Option<Venue>`] parsed from the metadata.
949    ///
950    /// # Panics
951    ///
952    /// This function panics if:
953    /// - The `venue` value contained in the metadata is invalid.
954    #[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    /// Returns an [`Option<UnixNanos>`] parsed from the metadata `start` field.
962    ///
963    /// # Panics
964    ///
965    /// This function panics if:
966    /// - The `start` value contained in the metadata is invalid.
967    #[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    /// Returns an [`Option<UnixNanos>`] parsed from the metadata `end` field.
975    ///
976    /// # Panics
977    ///
978    /// This function panics if:
979    /// - The `end` value contained in the metadata is invalid.
980    #[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    /// Returns an [`Option<usize>`] parsed from the metadata `limit` field.
988    ///
989    /// # Panics
990    ///
991    /// This function panics if:
992    /// - The `limit` value contained in the metadata is invalid.
993    #[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        // A null first occurrence must still count as "seen". A plain Option slot could not
1285        // tell an absent key from an explicit null, and would silently accept the duplicate.
1286        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}