Skip to main content

rustrade_data/
error.rs

1#[cfg(feature = "databento")]
2use crate::exchange::databento::DatabentoErrorKind;
3use crate::subscription::SubKind;
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#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, Error)]
11pub enum DataError {
12    #[error("failed to index market data Subscriptions: {0}")]
13    Index(#[from] IndexError),
14
15    #[error("failed to initialise reconnecting MarketStream due to empty subscriptions")]
16    SubscriptionsEmpty,
17
18    #[error("unsupported DynamicStreams Subscription SubKind: {0}")]
19    UnsupportedSubKind(SubKind),
20
21    #[error("initial snapshot missing for: {0}")]
22    InitialSnapshotMissing(SubscriptionId),
23
24    #[error("initial snapshot invalid: {0}")]
25    InitialSnapshotInvalid(String),
26
27    #[error("SocketError: {0}")]
28    Socket(String),
29
30    /// Databento-specific error with categorized kind for programmatic handling.
31    #[cfg(feature = "databento")]
32    #[error("Databento {kind} error ({context}): {message}")]
33    Databento {
34        kind: DatabentoErrorKind,
35        context: String,
36        message: String,
37    },
38
39    #[error("unsupported dynamic Subscription for exchange: {exchange}, kind: {sub_kind}")]
40    Unsupported {
41        exchange: ExchangeId,
42        sub_kind: SubKind,
43    },
44
45    #[error(
46        "\
47        InvalidSequence: first_update_id {first_update_id} does not follow on from the \
48        prev_last_update_id {prev_last_update_id} \
49    "
50    )]
51    InvalidSequence {
52        prev_last_update_id: u64,
53        first_update_id: u64,
54    },
55}
56
57impl DataError {
58    /// Determine if an error requires a [`MarketStream`](super::MarketStream) to re-initialise.
59    #[allow(clippy::match_like_matches_macro)]
60    pub fn is_terminal(&self) -> bool {
61        match self {
62            DataError::InvalidSequence { .. } => true,
63            _ => false,
64        }
65    }
66}
67
68impl From<SocketError> for DataError {
69    fn from(value: SocketError) -> Self {
70        Self::Socket(value.to_string())
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn test_data_error_is_terminal() {
80        struct TestCase {
81            input: DataError,
82            expected: bool,
83        }
84
85        let tests = vec![
86            TestCase {
87                // TC0: is terminal w/ DataError::InvalidSequence
88                input: DataError::InvalidSequence {
89                    prev_last_update_id: 0,
90                    first_update_id: 0,
91                },
92                expected: true,
93            },
94            TestCase {
95                // TC1: is not terminal w/ DataError::Socket
96                input: DataError::from(SocketError::Sink),
97                expected: false,
98            },
99        ];
100
101        for (index, test) in tests.into_iter().enumerate() {
102            let actual = test.input.is_terminal();
103            assert_eq!(actual, test.expected, "TC{} failed", index);
104        }
105    }
106}