1#[derive(Debug, thiserror::Error)]
14pub enum StreamError {
15 #[error("WebSocket connection failed to '{url}': {reason}")]
17 ConnectionFailed {
18 url: String,
20 reason: String,
22 },
23
24 #[error("WebSocket disconnected from '{url}'")]
26 Disconnected {
27 url: String,
29 },
30
31 #[error("Reconnection exhausted after {attempts} attempts to '{url}'")]
33 ReconnectExhausted {
34 url: String,
36 attempts: u32,
38 },
39
40 #[error("Tick parse error from {exchange}: {reason}")]
42 ParseError {
43 exchange: String,
45 reason: String,
47 },
48
49 #[error(
51 "Feed '{feed_id}' is stale: last tick was {elapsed_ms}ms ago (threshold: {threshold_ms}ms)"
52 )]
53 StaleFeed {
54 feed_id: String,
56 elapsed_ms: u64,
58 threshold_ms: u64,
60 },
61
62 #[error("Order book reconstruction failed for '{symbol}': {reason}")]
64 BookReconstructionFailed {
65 symbol: String,
67 reason: String,
69 },
70
71 #[error("Order book crossed for '{symbol}': best bid {bid} >= best ask {ask}")]
73 BookCrossed {
74 symbol: String,
76 bid: String,
78 ask: String,
80 },
81
82 #[error("Backpressure on channel '{channel}': {depth}/{capacity} slots used")]
84 Backpressure {
85 channel: String,
87 depth: usize,
89 capacity: usize,
91 },
92
93 #[error("Unknown exchange format: '{0}'")]
95 UnknownExchange(String),
96
97 #[error("I/O error: {0}")]
99 Io(String),
100
101 #[error("WebSocket error: {0}")]
103 WebSocket(String),
104
105 #[error("SPSC ring buffer is full (capacity: {capacity})")]
111 RingBufferFull {
112 capacity: usize,
114 },
115
116 #[error("SPSC ring buffer is empty")]
121 RingBufferEmpty,
122
123 #[error("OHLCV aggregation error: {reason}")]
128 AggregationError {
129 reason: String,
131 },
132
133 #[error("Normalization error: {reason}")]
138 NormalizationError {
139 reason: String,
141 },
142
143 #[error("Invalid tick: {reason}")]
148 InvalidTick {
149 reason: String,
151 },
152
153 #[error("Lorentz config error: {reason}")]
159 LorentzConfigError {
160 reason: String,
162 },
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168
169 #[test]
170 fn test_connection_failed_display() {
171 let e = StreamError::ConnectionFailed {
172 url: "wss://example.com".into(),
173 reason: "timeout".into(),
174 };
175 assert!(e.to_string().contains("example.com"));
176 assert!(e.to_string().contains("timeout"));
177 }
178
179 #[test]
180 fn test_disconnected_display() {
181 let e = StreamError::Disconnected {
182 url: "wss://feed.io".into(),
183 };
184 assert!(e.to_string().contains("feed.io"));
185 }
186
187 #[test]
188 fn test_reconnect_exhausted_display() {
189 let e = StreamError::ReconnectExhausted {
190 url: "wss://x.io".into(),
191 attempts: 5,
192 };
193 assert!(e.to_string().contains("5"));
194 }
195
196 #[test]
197 fn test_parse_error_display() {
198 let e = StreamError::ParseError {
199 exchange: "Binance".into(),
200 reason: "missing field".into(),
201 };
202 assert!(e.to_string().contains("Binance"));
203 }
204
205 #[test]
206 fn test_stale_feed_display() {
207 let e = StreamError::StaleFeed {
208 feed_id: "BTC-USD".into(),
209 elapsed_ms: 5000,
210 threshold_ms: 2000,
211 };
212 assert!(e.to_string().contains("BTC-USD"));
213 assert!(e.to_string().contains("5000"));
214 }
215
216 #[test]
217 fn test_book_reconstruction_failed_display() {
218 let e = StreamError::BookReconstructionFailed {
219 symbol: "ETH-USD".into(),
220 reason: "gap in sequence".into(),
221 };
222 assert!(e.to_string().contains("ETH-USD"));
223 }
224
225 #[test]
226 fn test_book_crossed_display() {
227 let e = StreamError::BookCrossed {
228 symbol: "BTC-USD".into(),
229 bid: "50001".into(),
230 ask: "50000".into(),
231 };
232 assert!(e.to_string().contains("crossed"));
233 }
234
235 #[test]
236 fn test_backpressure_display() {
237 let e = StreamError::Backpressure {
238 channel: "ticks".into(),
239 depth: 1000,
240 capacity: 1000,
241 };
242 assert!(e.to_string().contains("1000"));
243 }
244
245 #[test]
246 fn test_unknown_exchange_display() {
247 let e = StreamError::UnknownExchange("Kraken".into());
248 assert!(e.to_string().contains("Kraken"));
249 }
250
251 #[test]
252 fn test_ring_buffer_full_display() {
253 let e = StreamError::RingBufferFull { capacity: 1024 };
254 assert!(e.to_string().contains("1024"));
255 assert!(e.to_string().contains("full"));
256 }
257
258 #[test]
259 fn test_ring_buffer_empty_display() {
260 let e = StreamError::RingBufferEmpty;
261 assert!(e.to_string().contains("empty"));
262 }
263
264 #[test]
265 fn test_aggregation_error_display() {
266 let e = StreamError::AggregationError {
267 reason: "wrong symbol".into(),
268 };
269 assert!(e.to_string().contains("wrong symbol"));
270 }
271
272 #[test]
273 fn test_normalization_error_display() {
274 let e = StreamError::NormalizationError {
275 reason: "window not seeded".into(),
276 };
277 assert!(e.to_string().contains("window not seeded"));
278 }
279
280 #[test]
281 fn test_invalid_tick_display() {
282 let e = StreamError::InvalidTick {
283 reason: "negative price".into(),
284 };
285 assert!(e.to_string().contains("negative price"));
286 }
287
288 #[test]
289 fn test_lorentz_config_error_display() {
290 let e = StreamError::LorentzConfigError {
291 reason: "beta >= 1".into(),
292 };
293 assert!(e.to_string().contains("beta >= 1"));
294 }
295}