Skip to main content

indodax_cli/
errors.rs

1#[derive(Debug, thiserror::Error)]
2pub enum ErrorCategory {
3    #[error("connection_error")]
4    Connection,
5    #[error("authentication_error")]
6    Authentication,
7    #[error("rate_limit")]
8    RateLimit,
9    #[error("validation_error")]
10    Validation,
11    #[error("server_error")]
12    Server,
13    #[error("config_error")]
14    Config,
15    #[error("unknown_error")]
16    Unknown,
17}
18
19#[derive(Debug, thiserror::Error)]
20pub enum IndodaxError {
21    #[error("HTTP request failed: {0}")]
22    Http(#[from] reqwest::Error),
23
24    #[error("WebSocket error: {0}")]
25    WebSocket(#[from] tokio_tungstenite::tungstenite::Error),
26
27    #[error("JSON parse error: {0}")]
28    Json(#[from] serde_json::Error),
29
30    #[error("{message}")]
31    Api {
32        category: ErrorCategory,
33        message: String,
34        code: Option<String>,
35        retryable: bool,
36    },
37
38    #[error("{0}")]
39    Config(String),
40
41    #[error("{0}")]
42    Parse(String),
43
44    #[error("WebSocket token generation failed: {0}")]
45    WsToken(String),
46
47    #[error("{0}")]
48    Other(String),
49}
50
51impl IndodaxError {
52    pub fn api(message: impl Into<String>, category: ErrorCategory, code: Option<String>) -> Self {
53        let retryable = matches!(
54            category,
55            ErrorCategory::Connection | ErrorCategory::Server | ErrorCategory::RateLimit
56        );
57        IndodaxError::Api {
58            category,
59            message: message.into(),
60            code,
61            retryable,
62        }
63    }
64
65    pub fn category(&self) -> String {
66        match self {
67            IndodaxError::Api { category, .. } => category.to_string(),
68            IndodaxError::Http(_) => "connection_error".to_string(),
69            IndodaxError::WebSocket(_) => "connection_error".to_string(),
70            IndodaxError::Json(_) => "validation_error".to_string(),
71            IndodaxError::Config(_) => "config_error".to_string(),
72            IndodaxError::Parse(_) => "validation_error".to_string(),
73            IndodaxError::WsToken(_) => "authentication_error".to_string(),
74            IndodaxError::Other(_) => "unknown_error".to_string(),
75        }
76    }
77
78    pub fn is_retryable(&self) -> bool {
79        match self {
80            IndodaxError::Api { retryable, .. } => *retryable,
81            IndodaxError::Http(_) | IndodaxError::WebSocket(_) => true,
82            _ => false,
83        }
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    #[test]
92    fn test_error_category_connection() {
93        let cat = ErrorCategory::Connection;
94        assert_eq!(format!("{}", cat), "connection_error");
95    }
96
97    #[test]
98    fn test_error_category_authentication() {
99        let cat = ErrorCategory::Authentication;
100        assert_eq!(format!("{}", cat), "authentication_error");
101    }
102
103    #[test]
104    fn test_error_category_rate_limit() {
105        let cat = ErrorCategory::RateLimit;
106        assert_eq!(format!("{}", cat), "rate_limit");
107    }
108
109    #[test]
110    fn test_error_category_validation() {
111        let cat = ErrorCategory::Validation;
112        assert_eq!(format!("{}", cat), "validation_error");
113    }
114
115    #[test]
116    fn test_error_category_server() {
117        let cat = ErrorCategory::Server;
118        assert_eq!(format!("{}", cat), "server_error");
119    }
120
121    #[test]
122    fn test_error_category_config() {
123        let cat = ErrorCategory::Config;
124        assert_eq!(format!("{}", cat), "config_error");
125    }
126
127    #[test]
128    fn test_error_category_unknown() {
129        let cat = ErrorCategory::Unknown;
130        assert_eq!(format!("{}", cat), "unknown_error");
131    }
132
133    #[test]
134    fn test_indodax_error_http() {
135        // Test that Http error wraps reqwest::Error
136        // Since we can't easily create a reqwest error, we skip this test
137        // or use a mock approach
138        assert!(true); // Placeholder test
139    }
140
141    #[test]
142    fn test_indodax_error_api_basic() {
143        let err = IndodaxError::api("test message", ErrorCategory::Server, Some("500".into()));
144        let msg = err.to_string();
145        assert!(msg.contains("test message"));
146    }
147
148    #[test]
149    fn test_indodax_error_api_category_connection() {
150        let err = IndodaxError::api("conn error", ErrorCategory::Connection, None);
151        assert_eq!(err.category(), "connection_error");
152        assert!(err.is_retryable());
153    }
154
155    #[test]
156    fn test_indodax_error_api_category_server() {
157        let err = IndodaxError::api("server error", ErrorCategory::Server, None);
158        assert_eq!(err.category(), "server_error");
159        assert!(err.is_retryable());
160    }
161
162    #[test]
163    fn test_indodax_error_api_category_rate_limit() {
164        let err = IndodaxError::api("rate limit", ErrorCategory::RateLimit, None);
165        assert_eq!(err.category(), "rate_limit");
166        assert!(err.is_retryable());
167    }
168
169    #[test]
170    fn test_indodax_error_api_category_not_retryable() {
171        let err = IndodaxError::api("auth error", ErrorCategory::Authentication, None);
172        assert!(!err.is_retryable());
173    }
174
175    #[test]
176    fn test_indodax_error_config() {
177        let err = IndodaxError::Config("config error message".into());
178        assert_eq!(err.category(), "config_error");
179        let msg = err.to_string();
180        assert!(msg.contains("config error message"));
181    }
182
183    #[test]
184    fn test_indodax_error_parse() {
185        let err = IndodaxError::Parse("parse error".into());
186        assert_eq!(err.category(), "validation_error");
187        let msg = err.to_string();
188        assert!(msg.contains("parse error"));
189    }
190
191    #[test]
192    fn test_indodax_error_other() {
193        let err = IndodaxError::Other("other error".into());
194        assert_eq!(err.category(), "unknown_error");
195        let msg = err.to_string();
196        assert!(msg.contains("other error"));
197    }
198
199    #[test]
200    fn test_indodax_error_json() {
201        let err = IndodaxError::Json(serde_json::from_str::<serde_json::Value>("invalid").unwrap_err());
202        assert_eq!(err.category(), "validation_error");
203    }
204
205    #[test]
206    fn test_indodax_error_websocket() {
207        // WebSocket errors are harder to construct, but we can test the category
208        let err = IndodaxError::WebSocket(tokio_tungstenite::tungstenite::Error::ConnectionClosed);
209        assert_eq!(err.category(), "connection_error");
210        assert!(err.is_retryable());
211    }
212
213    #[test]
214    fn test_api_error_retryable_field() {
215        let err = IndodaxError::api("test", ErrorCategory::Connection, None);
216        match err {
217            IndodaxError::Api { retryable, .. } => assert!(retryable),
218            _ => assert!(false, "Expected Api error, got {:?}", err),
219        }
220    }
221
222    #[test]
223    fn test_api_error_code() {
224        let err = IndodaxError::api("test", ErrorCategory::Unknown, Some("ERR_123".into()));
225        match err {
226            IndodaxError::Api { code, .. } => assert_eq!(code, Some("ERR_123".into())),
227            _ => assert!(false, "Expected Api error, got {:?}", err),
228        }
229    }
230}