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}