Skip to main content

fin_stream/
error.rs

1//! Typed error hierarchy for fin-stream.
2
3#[derive(Debug, thiserror::Error)]
4pub enum StreamError {
5    /// WebSocket connection failed.
6    #[error("WebSocket connection failed to '{url}': {reason}")]
7    ConnectionFailed { url: String, reason: String },
8
9    /// WebSocket disconnected unexpectedly.
10    #[error("WebSocket disconnected from '{url}'")]
11    Disconnected { url: String },
12
13    /// Reconnection attempts exhausted.
14    #[error("Reconnection exhausted after {attempts} attempts to '{url}'")]
15    ReconnectExhausted { url: String, attempts: u32 },
16
17    /// Tick deserialization failed.
18    #[error("Tick parse error from {exchange}: {reason}")]
19    ParseError { exchange: String, reason: String },
20
21    /// Feed is stale — no data received within staleness threshold.
22    #[error("Feed '{feed_id}' is stale: last tick was {elapsed_ms}ms ago (threshold: {threshold_ms}ms)")]
23    StaleFeed { feed_id: String, elapsed_ms: u64, threshold_ms: u64 },
24
25    /// Order book reconstruction failed.
26    #[error("Order book reconstruction failed for '{symbol}': {reason}")]
27    BookReconstructionFailed { symbol: String, reason: String },
28
29    /// Order book is crossed (bid >= ask).
30    #[error("Order book crossed for '{symbol}': best bid {bid} >= best ask {ask}")]
31    BookCrossed { symbol: String, bid: String, ask: String },
32
33    /// Backpressure: the downstream channel is full.
34    #[error("Backpressure on channel '{channel}': {depth}/{capacity} slots used")]
35    Backpressure { channel: String, depth: usize, capacity: usize },
36
37    /// Invalid exchange format.
38    #[error("Unknown exchange format: '{0}'")]
39    UnknownExchange(String),
40
41    /// I/O error.
42    #[error("I/O error: {0}")]
43    Io(String),
44
45    /// WebSocket protocol error.
46    #[error("WebSocket error: {0}")]
47    WebSocket(String),
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53
54    #[test]
55    fn test_connection_failed_display() {
56        let e = StreamError::ConnectionFailed {
57            url: "wss://example.com".into(),
58            reason: "timeout".into(),
59        };
60        assert!(e.to_string().contains("example.com"));
61        assert!(e.to_string().contains("timeout"));
62    }
63
64    #[test]
65    fn test_disconnected_display() {
66        let e = StreamError::Disconnected { url: "wss://feed.io".into() };
67        assert!(e.to_string().contains("feed.io"));
68    }
69
70    #[test]
71    fn test_reconnect_exhausted_display() {
72        let e = StreamError::ReconnectExhausted { url: "wss://x.io".into(), attempts: 5 };
73        assert!(e.to_string().contains("5"));
74    }
75
76    #[test]
77    fn test_parse_error_display() {
78        let e = StreamError::ParseError { exchange: "Binance".into(), reason: "missing field".into() };
79        assert!(e.to_string().contains("Binance"));
80    }
81
82    #[test]
83    fn test_stale_feed_display() {
84        let e = StreamError::StaleFeed {
85            feed_id: "BTC-USD".into(),
86            elapsed_ms: 5000,
87            threshold_ms: 2000,
88        };
89        assert!(e.to_string().contains("BTC-USD"));
90        assert!(e.to_string().contains("5000"));
91    }
92
93    #[test]
94    fn test_book_reconstruction_failed_display() {
95        let e = StreamError::BookReconstructionFailed {
96            symbol: "ETH-USD".into(),
97            reason: "gap in sequence".into(),
98        };
99        assert!(e.to_string().contains("ETH-USD"));
100    }
101
102    #[test]
103    fn test_book_crossed_display() {
104        let e = StreamError::BookCrossed {
105            symbol: "BTC-USD".into(),
106            bid: "50001".into(),
107            ask: "50000".into(),
108        };
109        assert!(e.to_string().contains("crossed"));
110    }
111
112    #[test]
113    fn test_backpressure_display() {
114        let e = StreamError::Backpressure { channel: "ticks".into(), depth: 1000, capacity: 1000 };
115        assert!(e.to_string().contains("1000"));
116    }
117
118    #[test]
119    fn test_unknown_exchange_display() {
120        let e = StreamError::UnknownExchange("Kraken".into());
121        assert!(e.to_string().contains("Kraken"));
122    }
123}