Skip to main content

rustrade_data/exchange/databento/
error.rs

1//! Error types and conversions for Databento integration.
2
3use crate::error::DataError;
4use serde::{Deserialize, Serialize};
5use std::error::Error as StdError;
6use std::fmt;
7
8/// Categorized Databento error for programmatic handling.
9///
10/// Enables callers to implement proper retry logic:
11/// - [`Authentication`](DatabentoErrorKind::Authentication): Don't retry, fix credentials
12/// - [`RateLimit`](DatabentoErrorKind::RateLimit): Retry with exponential backoff
13/// - [`Network`](DatabentoErrorKind::Network): Retry with backoff
14/// - [`Decode`](DatabentoErrorKind::Decode): Skip record or abort depending on context
15/// - [`Api`](DatabentoErrorKind::Api): Check message for details
16#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
17pub enum DatabentoErrorKind {
18    /// API key missing, invalid, or expired.
19    Authentication,
20    /// Request throttled due to rate limits.
21    RateLimit,
22    /// Connection or network-level failure.
23    Network,
24    /// Failed to decode DBN record or response.
25    Decode,
26    /// Other API error (check message for details).
27    Api,
28}
29
30impl fmt::Display for DatabentoErrorKind {
31    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32        match self {
33            Self::Authentication => write!(f, "authentication"),
34            Self::RateLimit => write!(f, "rate limit"),
35            Self::Network => write!(f, "network"),
36            Self::Decode => write!(f, "decode"),
37            Self::Api => write!(f, "API"),
38        }
39    }
40}
41
42/// Internal error wrapper that preserves the source error chain.
43#[derive(Debug)]
44pub(crate) struct DatabentoError {
45    pub(crate) kind: DatabentoErrorKind,
46    context: &'static str,
47    source: Box<dyn StdError + Send + Sync + 'static>,
48}
49
50impl DatabentoError {
51    pub(crate) fn new(
52        context: &'static str,
53        source: impl StdError + Send + Sync + 'static,
54    ) -> Self {
55        let kind = classify_error(&source);
56        Self {
57            kind,
58            context,
59            source: Box::new(source),
60        }
61    }
62}
63
64impl fmt::Display for DatabentoError {
65    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66        write!(f, "Databento {}: {}", self.context, self.source)
67    }
68}
69
70impl StdError for DatabentoError {
71    fn source(&self) -> Option<&(dyn StdError + 'static)> {
72        Some(self.source.as_ref())
73    }
74}
75
76impl From<DatabentoError> for DataError {
77    fn from(err: DatabentoError) -> Self {
78        DataError::Databento {
79            kind: err.kind,
80            context: err.context.to_string(),
81            message: err.source.to_string(),
82        }
83    }
84}
85
86/// Classify an error into a [`DatabentoErrorKind`] based on error message heuristics.
87fn classify_error(err: &(impl StdError + ?Sized)) -> DatabentoErrorKind {
88    let mut msg = err.to_string();
89    msg.make_ascii_lowercase();
90
91    if msg.contains("api key")
92        || msg.contains("apikey")
93        || msg.contains("unauthorized")
94        || msg.contains("authentication")
95        || msg.contains("invalid key")
96        || msg.contains("expired")
97    {
98        return DatabentoErrorKind::Authentication;
99    }
100
101    if msg.contains("rate limit") || msg.contains("too many requests") || msg.contains("throttl") {
102        return DatabentoErrorKind::RateLimit;
103    }
104
105    if msg.contains("connect")
106        || msg.contains("timeout")
107        || msg.contains("network")
108        || msg.contains("dns")
109        || msg.contains("socket")
110        || msg.contains("unexpected end of file")
111    {
112        return DatabentoErrorKind::Network;
113    }
114
115    if msg.contains("decode") || msg.contains("parse") || msg.contains("invalid record") {
116        return DatabentoErrorKind::Decode;
117    }
118
119    DatabentoErrorKind::Api
120}
121
122/// Extension trait for adding context to databento errors. Crate-internal
123/// to avoid shadowing similarly-named methods on traits like `anyhow::Context`.
124pub(crate) trait DatabentoResultExt<T> {
125    fn with_context(self, ctx: &'static str) -> Result<T, DatabentoError>;
126}
127
128impl<T, E: StdError + Send + Sync + 'static> DatabentoResultExt<T> for Result<T, E> {
129    fn with_context(self, ctx: &'static str) -> Result<T, DatabentoError> {
130        self.map_err(|e| DatabentoError::new(ctx, e))
131    }
132}
133
134/// Create a decode error from a message (for use in iterator/stream contexts).
135pub(crate) fn decode_error(message: impl Into<String>) -> DataError {
136    DataError::Databento {
137        kind: DatabentoErrorKind::Decode,
138        context: "decoding DBN record".to_string(),
139        message: message.into(),
140    }
141}