ccxt_exchanges/binance/error.rs
1//! Binance-specific error types.
2//!
3//! This module provides module-specific error types for Binance WebSocket operations.
4//! These errors can be converted to the core `Error` type while preserving
5//! structured information for downcast capability.
6//!
7//! # Example
8//!
9//! ```rust
10//! use ccxt_exchanges::binance::error::BinanceWsError;
11//! use ccxt_core::error::Error as CoreError;
12//!
13//! // Create a Binance-specific error
14//! let ws_err = BinanceWsError::subscription_failed("btcusdt@ticker", "Invalid stream");
15//!
16//! // Convert to core error for public API
17//! let core_err: CoreError = ws_err.into();
18//!
19//! // Downcast back to access structured data
20//! if let Some(binance_err) = core_err.downcast_websocket::<BinanceWsError>() {
21//! if let Some(stream) = binance_err.stream_name() {
22//! println!("Failed stream: {}", stream);
23//! }
24//! }
25//! ```
26
27use ccxt_core::error::Error as CoreError;
28use thiserror::Error;
29
30#[cfg(test)]
31use std::error::Error as StdError;
32
33/// Binance WebSocket specific errors.
34///
35/// Implements `StdError + Send + Sync` for use with `CoreError::WebSocket`.
36/// Uses `#[non_exhaustive]` to allow adding variants in future versions.
37///
38/// # Variants
39///
40/// - `MissingStream`: Stream name missing in WebSocket message
41/// - `UnsupportedEvent`: Received an unsupported event type
42/// - `SubscriptionFailed`: Stream subscription failed
43/// - `Connection`: WebSocket connection error
44/// - `Core`: Passthrough for core errors
45#[derive(Error, Debug)]
46#[non_exhaustive]
47pub enum BinanceWsError {
48 /// Stream name missing in message.
49 ///
50 /// This occurs when a WebSocket message doesn't contain the expected
51 /// stream identifier, making it impossible to route the message.
52 #[error("Stream name missing in message: {raw}")]
53 MissingStream {
54 /// The raw message content (truncated for display)
55 raw: String,
56 },
57
58 /// Unsupported event type.
59 ///
60 /// This occurs when the WebSocket receives an event type that
61 /// is not handled by the current implementation.
62 #[error("Unsupported event type: {event}")]
63 UnsupportedEvent {
64 /// The event type that was not recognized
65 event: String,
66 },
67
68 /// Subscription failed.
69 ///
70 /// This occurs when a stream subscription request is rejected
71 /// by the Binance WebSocket server.
72 #[error("Subscription failed for {stream}: {reason}")]
73 SubscriptionFailed {
74 /// The stream name that failed to subscribe
75 stream: String,
76 /// The reason for the failure
77 reason: String,
78 },
79
80 /// WebSocket connection error.
81 ///
82 /// General connection-related errors that don't fit other categories.
83 #[error("WebSocket connection error: {0}")]
84 Connection(String),
85
86 /// Core error passthrough.
87 ///
88 /// Allows wrapping core errors within Binance-specific error handling
89 /// while maintaining the ability to extract the original error.
90 #[error(transparent)]
91 Core(#[from] CoreError),
92}
93
94impl BinanceWsError {
95 /// Creates a new `MissingStream` error.
96 ///
97 /// # Arguments
98 ///
99 /// * `raw` - The raw message content (will be truncated if too long)
100 ///
101 /// # Example
102 ///
103 /// ```rust
104 /// use ccxt_exchanges::binance::error::BinanceWsError;
105 ///
106 /// let err = BinanceWsError::missing_stream(r#"{"data": "unknown"}"#);
107 /// ```
108 pub fn missing_stream(raw: impl Into<String>) -> Self {
109 let mut raw_str = raw.into();
110 // Truncate long messages for display
111 if raw_str.len() > 200 {
112 raw_str.truncate(200);
113 raw_str.push_str("...");
114 }
115 Self::MissingStream { raw: raw_str }
116 }
117
118 /// Creates a new `UnsupportedEvent` error.
119 ///
120 /// # Arguments
121 ///
122 /// * `event` - The unsupported event type
123 ///
124 /// # Example
125 ///
126 /// ```rust
127 /// use ccxt_exchanges::binance::error::BinanceWsError;
128 ///
129 /// let err = BinanceWsError::unsupported_event("unknownEvent");
130 /// ```
131 pub fn unsupported_event(event: impl Into<String>) -> Self {
132 Self::UnsupportedEvent {
133 event: event.into(),
134 }
135 }
136
137 /// Creates a new `SubscriptionFailed` error.
138 ///
139 /// # Arguments
140 ///
141 /// * `stream` - The stream name that failed
142 /// * `reason` - The reason for the failure
143 ///
144 /// # Example
145 ///
146 /// ```rust
147 /// use ccxt_exchanges::binance::error::BinanceWsError;
148 ///
149 /// let err = BinanceWsError::subscription_failed("btcusdt@ticker", "Invalid symbol");
150 /// ```
151 pub fn subscription_failed(stream: impl Into<String>, reason: impl Into<String>) -> Self {
152 Self::SubscriptionFailed {
153 stream: stream.into(),
154 reason: reason.into(),
155 }
156 }
157
158 /// Creates a new `Connection` error.
159 ///
160 /// # Arguments
161 ///
162 /// * `message` - The connection error message
163 ///
164 /// # Example
165 ///
166 /// ```rust
167 /// use ccxt_exchanges::binance::error::BinanceWsError;
168 ///
169 /// let err = BinanceWsError::connection("Failed to establish connection");
170 /// ```
171 pub fn connection(message: impl Into<String>) -> Self {
172 Self::Connection(message.into())
173 }
174
175 /// Returns the stream name if this error is related to a specific stream.
176 ///
177 /// # Returns
178 ///
179 /// - `Some(&str)` - The stream name for `SubscriptionFailed` variant
180 /// - `None` - For all other variants
181 ///
182 /// # Example
183 ///
184 /// ```rust
185 /// use ccxt_exchanges::binance::error::BinanceWsError;
186 ///
187 /// let err = BinanceWsError::subscription_failed("btcusdt@ticker", "Invalid");
188 /// assert_eq!(err.stream_name(), Some("btcusdt@ticker"));
189 ///
190 /// let err = BinanceWsError::connection("Failed");
191 /// assert_eq!(err.stream_name(), None);
192 /// ```
193 pub fn stream_name(&self) -> Option<&str> {
194 match self {
195 Self::MissingStream { .. }
196 | Self::UnsupportedEvent { .. }
197 | Self::Connection(_)
198 | Self::Core(_) => None,
199 Self::SubscriptionFailed { stream, .. } => Some(stream),
200 }
201 }
202
203 /// Returns the event type if this is an `UnsupportedEvent` error.
204 ///
205 /// # Returns
206 ///
207 /// - `Some(&str)` - The event type for `UnsupportedEvent` variant
208 /// - `None` - For all other variants
209 pub fn event_type(&self) -> Option<&str> {
210 match self {
211 Self::UnsupportedEvent { event } => Some(event),
212 _ => None,
213 }
214 }
215
216 /// Returns the raw message if this is a `MissingStream` error.
217 ///
218 /// # Returns
219 ///
220 /// - `Some(&str)` - The raw message for `MissingStream` variant
221 /// - `None` - For all other variants
222 pub fn raw_message(&self) -> Option<&str> {
223 match self {
224 Self::MissingStream { raw } => Some(raw),
225 _ => None,
226 }
227 }
228}
229
230/// Conversion from `BinanceWsError` to `CoreError`.
231///
232/// This implementation allows Binance-specific errors to be returned from
233/// public API methods that return `CoreError`. The conversion preserves
234/// the original error for downcast capability.
235///
236/// # Conversion Rules
237///
238/// - `BinanceWsError::Core(core)` → Returns the inner `CoreError` directly
239/// - All other variants → Boxed into `CoreError::WebSocket` for downcast
240impl From<BinanceWsError> for CoreError {
241 fn from(e: BinanceWsError) -> Self {
242 match e {
243 // Passthrough: extract the inner CoreError
244 BinanceWsError::Core(core) => core,
245 // Preserve the original error for downcast capability
246 other => CoreError::WebSocket(Box::new(other)),
247 }
248 }
249}
250
251#[cfg(test)]
252mod tests {
253 use super::*;
254
255 #[test]
256 fn test_missing_stream_error() {
257 let err = BinanceWsError::missing_stream(r#"{"data": "test"}"#);
258 assert!(matches!(err, BinanceWsError::MissingStream { .. }));
259 assert!(err.to_string().contains("Stream name missing"));
260 assert_eq!(err.stream_name(), None);
261 assert!(err.raw_message().is_some());
262 }
263
264 #[test]
265 fn test_missing_stream_truncation() {
266 let long_message = "x".repeat(300);
267 let err = BinanceWsError::missing_stream(long_message);
268 if let BinanceWsError::MissingStream { raw } = &err {
269 assert!(raw.len() <= 203); // 200 + "..."
270 assert!(raw.ends_with("..."));
271 } else {
272 panic!("Expected MissingStream variant");
273 }
274 }
275
276 #[test]
277 fn test_unsupported_event_error() {
278 let err = BinanceWsError::unsupported_event("unknownEvent");
279 assert!(matches!(err, BinanceWsError::UnsupportedEvent { .. }));
280 assert!(err.to_string().contains("unknownEvent"));
281 assert_eq!(err.event_type(), Some("unknownEvent"));
282 assert_eq!(err.stream_name(), None);
283 }
284
285 #[test]
286 fn test_subscription_failed_error() {
287 let err = BinanceWsError::subscription_failed("btcusdt@ticker", "Invalid symbol");
288 assert!(matches!(err, BinanceWsError::SubscriptionFailed { .. }));
289 assert!(err.to_string().contains("btcusdt@ticker"));
290 assert!(err.to_string().contains("Invalid symbol"));
291 assert_eq!(err.stream_name(), Some("btcusdt@ticker"));
292 }
293
294 #[test]
295 fn test_connection_error() {
296 let err = BinanceWsError::connection("Connection refused");
297 assert!(matches!(err, BinanceWsError::Connection(_)));
298 assert!(err.to_string().contains("Connection refused"));
299 assert_eq!(err.stream_name(), None);
300 }
301
302 #[test]
303 fn test_core_passthrough() {
304 let core_err = CoreError::authentication("Invalid API key");
305 let ws_err = BinanceWsError::Core(core_err);
306 assert!(matches!(ws_err, BinanceWsError::Core(_)));
307 }
308
309 #[test]
310 fn test_conversion_to_core_error() {
311 // Test non-Core variant conversion
312 let ws_err = BinanceWsError::subscription_failed("btcusdt@ticker", "Invalid");
313 let core_err: CoreError = ws_err.into();
314 assert!(matches!(core_err, CoreError::WebSocket(_)));
315
316 // Verify downcast works
317 let downcast = core_err.downcast_websocket::<BinanceWsError>();
318 assert!(downcast.is_some());
319 if let Some(binance_err) = downcast {
320 assert_eq!(binance_err.stream_name(), Some("btcusdt@ticker"));
321 }
322 }
323
324 #[test]
325 fn test_core_passthrough_conversion() {
326 // Test Core variant passthrough
327 let original = CoreError::authentication("Invalid API key");
328 let ws_err = BinanceWsError::Core(original);
329 let core_err: CoreError = ws_err.into();
330
331 // Should be Authentication, not WebSocket
332 assert!(matches!(core_err, CoreError::Authentication(_)));
333 assert!(core_err.to_string().contains("Invalid API key"));
334 }
335
336 #[test]
337 fn test_error_is_send_sync() {
338 fn assert_send_sync<T: Send + Sync>() {}
339 assert_send_sync::<BinanceWsError>();
340 }
341
342 #[test]
343 fn test_error_is_std_error() {
344 fn assert_std_error<T: StdError>() {}
345 assert_std_error::<BinanceWsError>();
346 }
347}