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