nautilus_architect_ax/websocket/
error.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Error types produced by the Ax WebSocket client implementation.
17
18use serde::{Deserialize, Serialize};
19use thiserror::Error;
20use tokio_tungstenite::tungstenite;
21
22/// A typed error enumeration for the Ax WebSocket client.
23#[derive(Debug, Clone, Error)]
24pub enum AxWsError {
25    /// Failure to parse incoming message.
26    #[error("Parsing error: {0}")]
27    ParsingError(String),
28    /// Errors returned directly by AX Exchange API.
29    #[error("Ax error: {0}")]
30    ApiError(String),
31    /// Failure during JSON serialization/deserialization.
32    #[error("JSON error: {0}")]
33    JsonError(String),
34    /// Generic client error.
35    #[error("Client error: {0}")]
36    ClientError(String),
37    /// Authentication error (invalid/expired token, etc.).
38    #[error("Authentication error: {0}")]
39    AuthenticationError(String),
40    /// Connection error during WebSocket setup.
41    #[error("Connection error: {0}")]
42    ConnectionError(String),
43    /// Subscription error (invalid symbol, already subscribed, etc.).
44    #[error("Subscription error: {0}")]
45    SubscriptionError(String),
46    /// Order operation error (rejection, cancellation failure, etc.).
47    #[error("Order error: {0}")]
48    OrderError(String),
49    /// WebSocket transport error.
50    #[error("Tungstenite error: {0}")]
51    TungsteniteError(String),
52    /// Channel communication error.
53    #[error("Channel error: {0}")]
54    ChannelError(String),
55    /// Timeout waiting for response.
56    #[error("Timeout: {0}")]
57    Timeout(String),
58}
59
60impl From<tungstenite::Error> for AxWsError {
61    fn from(error: tungstenite::Error) -> Self {
62        Self::TungsteniteError(error.to_string())
63    }
64}
65
66impl From<serde_json::Error> for AxWsError {
67    fn from(error: serde_json::Error) -> Self {
68        Self::JsonError(error.to_string())
69    }
70}
71
72impl From<String> for AxWsError {
73    fn from(msg: String) -> Self {
74        Self::ClientError(msg)
75    }
76}
77
78/// Represents an error response from the Ax WebSocket API.
79#[derive(Clone, Debug, Serialize, Deserialize)]
80pub struct AxWsErrorResponse {
81    /// Error code.
82    #[serde(default)]
83    pub code: Option<String>,
84    /// Error message.
85    #[serde(default)]
86    pub message: Option<String>,
87    /// Request ID if available.
88    #[serde(default)]
89    pub rid: Option<i64>,
90}
91
92impl From<AxWsErrorResponse> for AxWsError {
93    fn from(error: AxWsErrorResponse) -> Self {
94        let message = error
95            .message
96            .or(error.code)
97            .unwrap_or_else(|| "Unknown error".to_string());
98        Self::ApiError(message)
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use rstest::rstest;
105
106    use super::*;
107
108    #[rstest]
109    fn test_architect_ws_error_display() {
110        let error = AxWsError::ParsingError("invalid message format".to_string());
111        assert_eq!(error.to_string(), "Parsing error: invalid message format");
112
113        let error = AxWsError::ApiError("INSUFFICIENT_MARGIN".to_string());
114        assert_eq!(error.to_string(), "Ax error: INSUFFICIENT_MARGIN");
115
116        let error = AxWsError::AuthenticationError("token expired".to_string());
117        assert_eq!(error.to_string(), "Authentication error: token expired");
118    }
119
120    #[rstest]
121    fn test_architect_ws_error_from_json_error() {
122        let json_err = serde_json::from_str::<serde_json::Value>("invalid json")
123            .expect_err("Should fail to parse");
124        let ws_err = AxWsError::from(json_err);
125
126        assert!(matches!(ws_err, AxWsError::JsonError(_)));
127    }
128
129    #[rstest]
130    fn test_architect_ws_error_from_string() {
131        let error = AxWsError::from("Test client error".to_string());
132        assert_eq!(error.to_string(), "Client error: Test client error");
133    }
134
135    #[rstest]
136    fn test_architect_ws_error_response_to_error() {
137        let error_response = AxWsErrorResponse {
138            code: Some("ORDER_NOT_FOUND".to_string()),
139            message: Some("Order does not exist".to_string()),
140            rid: Some(123),
141        };
142
143        let ws_error = AxWsError::from(error_response);
144        assert_eq!(ws_error.to_string(), "Ax error: Order does not exist");
145    }
146
147    #[rstest]
148    fn test_architect_ws_error_response_fallback_to_code() {
149        let error_response = AxWsErrorResponse {
150            code: Some("INVALID_REQUEST".to_string()),
151            message: None,
152            rid: None,
153        };
154
155        let ws_error = AxWsError::from(error_response);
156        assert_eq!(ws_error.to_string(), "Ax error: INVALID_REQUEST");
157    }
158}