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