Skip to main content

rustrade_data/
error.rs

1#[cfg(feature = "databento")]
2use crate::exchange::databento::DatabentoErrorKind;
3use crate::subscription::{SubKind, candle::CandleInterval};
4use rustrade_instrument::{exchange::ExchangeId, index::error::IndexError};
5use rustrade_integration::{error::SocketError, subscription::SubscriptionId};
6use serde::{Deserialize, Serialize};
7use thiserror::Error;
8
9/// All errors generated in `rustrade-data`.
10///
11/// # Non-exhaustive
12/// Variants are added as integrations land, and two of the existing ones (`Databento`, `Lse` —
13/// not linked: each is behind its own feature, and this block is not) appear only under that
14/// feature, so the variant set a downstream `match` sees already depends on the feature selection
15/// it built
16/// with. A wildcard arm is required either way; `#[non_exhaustive]` makes that a compile-time
17/// contract rather than something a consumer discovers when a feature flag or a release moves.
18/// Matches *within* this crate stay exhaustively checked.
19#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, Error)]
20#[non_exhaustive]
21pub enum DataError {
22    #[error("failed to index market data Subscriptions: {0}")]
23    Index(#[from] IndexError),
24
25    #[error("failed to initialise reconnecting MarketStream due to empty subscriptions")]
26    SubscriptionsEmpty,
27
28    #[error("unsupported DynamicStreams Subscription SubKind: {0}")]
29    UnsupportedSubKind(SubKind),
30
31    #[error("initial snapshot missing for: {0}")]
32    InitialSnapshotMissing(SubscriptionId),
33
34    #[error("initial snapshot invalid: {0}")]
35    InitialSnapshotInvalid(String),
36
37    #[error("SocketError: {0}")]
38    Socket(String),
39
40    /// Databento-specific error with categorized kind for programmatic handling.
41    #[cfg(feature = "databento")]
42    #[error("Databento {kind} error ({context}): {message}")]
43    Databento {
44        kind: DatabentoErrorKind,
45        context: String,
46        message: String,
47    },
48
49    /// A London Strategic Edge integration error, flattened into this crate's common error type.
50    ///
51    /// [`LseError`](crate::exchange::lse::error::LseError) carries a `reqwest::Error`, which is
52    /// neither `Clone` nor serialisable, so it cannot be nested here structurally. Callers wanting
53    /// the full cause should handle `LseError` at the call site; this variant exists so an
54    /// LSE-sourced stream can compose with the crate's generic stream helpers. Same flattening as
55    /// [`Socket`](Self::Socket) applies to `SocketError`.
56    ///
57    /// `kind` survives that flattening, so a consumer can still tell a resumable
58    /// [`RateLimit`](crate::exchange::lse::error::LseErrorKind::RateLimit) from a terminal
59    /// [`Decode`](crate::exchange::lse::error::LseErrorKind::Decode) without substring-matching
60    /// `message` — which is the only thing a bare `String` left it. Same shape as the `Databento`
61    /// variant (not linked: it is behind the `databento` feature, which this variant is not).
62    #[cfg(feature = "lse")]
63    #[error("London Strategic Edge {kind} error: {message}")]
64    Lse {
65        kind: crate::exchange::lse::error::LseErrorKind,
66        message: String,
67    },
68
69    #[error("unsupported dynamic Subscription for exchange: {exchange}, kind: {sub_kind}")]
70    Unsupported {
71        exchange: ExchangeId,
72        sub_kind: SubKind,
73    },
74
75    #[error("exchange {exchange} does not support candle interval: {interval}")]
76    UnsupportedInterval {
77        exchange: ExchangeId,
78        interval: CandleInterval,
79    },
80
81    #[error(
82        "\
83        InvalidSequence: first_update_id {first_update_id} does not follow on from the \
84        prev_last_update_id {prev_last_update_id} \
85    "
86    )]
87    InvalidSequence {
88        prev_last_update_id: u64,
89        first_update_id: u64,
90    },
91}
92
93impl DataError {
94    /// Determine if an error requires a [`MarketStream`](super::MarketStream) to re-initialise.
95    // Explicit `match` (not `matches!`) is kept so additional terminal variants can be classified
96    // arm-by-arm as they are added; the lint would otherwise push this to a single `matches!`.
97    #[allow(clippy::match_like_matches_macro)]
98    pub fn is_terminal(&self) -> bool {
99        match self {
100            DataError::InvalidSequence { .. } => true,
101            _ => false,
102        }
103    }
104}
105
106impl From<SocketError> for DataError {
107    fn from(value: SocketError) -> Self {
108        Self::Socket(value.to_string())
109    }
110}
111
112#[cfg(feature = "lse")]
113impl From<crate::exchange::lse::error::LseError> for DataError {
114    fn from(value: crate::exchange::lse::error::LseError) -> Self {
115        Self::Lse {
116            kind: value.kind(),
117            message: value.to_string(),
118        }
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    #[test]
127    fn test_data_error_is_terminal() {
128        struct TestCase {
129            input: DataError,
130            expected: bool,
131        }
132
133        let tests = vec![
134            TestCase {
135                // TC0: is terminal w/ DataError::InvalidSequence
136                input: DataError::InvalidSequence {
137                    prev_last_update_id: 0,
138                    first_update_id: 0,
139                },
140                expected: true,
141            },
142            TestCase {
143                // TC1: is not terminal w/ DataError::Socket
144                input: DataError::from(SocketError::Sink),
145                expected: false,
146            },
147            TestCase {
148                // TC2: not terminal w/ DataError::UnsupportedInterval — a caller
149                // configuration error, not a stream condition warranting re-init.
150                input: DataError::UnsupportedInterval {
151                    exchange: ExchangeId::HyperliquidPerp,
152                    interval: CandleInterval::Sec1,
153                },
154                expected: false,
155            },
156        ];
157
158        for (index, test) in tests.into_iter().enumerate() {
159            let actual = test.input.is_terminal();
160            assert_eq!(actual, test.expected, "TC{} failed", index);
161        }
162    }
163}