1use std::io;
4use thiserror::Error;
5use tokio::task::JoinError;
6
7#[derive(Error, Debug)]
9pub enum DukascopyError {
10 #[error("HTTP error: {0}")]
12 HttpError(String),
13
14 #[error("LZMA decompression error: {0}")]
16 LzmaError(String),
17
18 #[error("Invalid tick data: data is malformed or contains invalid values")]
20 InvalidTickData,
21
22 #[error("Invalid currency code '{code}': {reason}")]
24 InvalidCurrencyCode {
25 code: String,
27 reason: String,
29 },
30
31 #[error("Market is closed: {0}")]
33 MarketClosed(String),
34
35 #[error("Data not found for {pair} at {timestamp}")]
37 DataNotFoundFor {
38 pair: String,
40 timestamp: String,
42 },
43
44 #[error("Data not found for the specified time")]
46 DataNotFound,
47
48 #[error("Rate limit exceeded. Please wait before making more requests.")]
50 RateLimitExceeded,
51
52 #[error("Unauthorized access")]
54 Unauthorized,
55
56 #[error("Access forbidden")]
58 Forbidden,
59
60 #[error("Invalid request: {0}")]
62 InvalidRequest(String),
63
64 #[error("Request timed out after {0} seconds")]
66 Timeout(u64),
67
68 #[error("Cache error: {0}")]
70 CacheError(String),
71
72 #[error("Unknown error: {0}")]
74 Unknown(String),
75}
76
77impl DukascopyError {
78 pub fn is_retryable(&self) -> bool {
85 matches!(
86 self,
87 Self::RateLimitExceeded | Self::Timeout(_) | Self::HttpError(_)
88 )
89 }
90
91 pub fn is_not_found(&self) -> bool {
93 matches!(self, Self::DataNotFound | Self::DataNotFoundFor { .. })
94 }
95
96 pub fn is_validation_error(&self) -> bool {
98 matches!(
99 self,
100 Self::InvalidCurrencyCode { .. } | Self::InvalidTickData | Self::InvalidRequest(_)
101 )
102 }
103}
104
105impl From<reqwest::Error> for DukascopyError {
106 fn from(err: reqwest::Error) -> Self {
107 if err.is_timeout() {
108 DukascopyError::Timeout(30)
109 } else if err.is_connect() {
110 DukascopyError::HttpError(format!("Connection failed: {}", err))
111 } else {
112 DukascopyError::HttpError(err.to_string())
113 }
114 }
115}
116
117impl From<lzma_rs::error::Error> for DukascopyError {
118 fn from(err: lzma_rs::error::Error) -> Self {
119 DukascopyError::LzmaError(err.to_string())
120 }
121}
122
123impl From<io::Error> for DukascopyError {
124 fn from(err: io::Error) -> Self {
125 match err.kind() {
126 io::ErrorKind::TimedOut => DukascopyError::Timeout(30),
127 io::ErrorKind::NotFound => DukascopyError::DataNotFound,
128 _ => DukascopyError::Unknown(format!("IO error: {}", err)),
129 }
130 }
131}
132
133impl From<JoinError> for DukascopyError {
134 fn from(err: JoinError) -> Self {
135 if err.is_cancelled() {
136 DukascopyError::Unknown("Task was cancelled".to_string())
137 } else {
138 DukascopyError::Unknown(format!("Task panicked: {}", err))
139 }
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 #[test]
148 fn test_is_retryable() {
149 assert!(DukascopyError::RateLimitExceeded.is_retryable());
150 assert!(DukascopyError::Timeout(30).is_retryable());
151 assert!(DukascopyError::HttpError("test".into()).is_retryable());
152
153 assert!(!DukascopyError::InvalidTickData.is_retryable());
154 assert!(!DukascopyError::DataNotFound.is_retryable());
155 }
156
157 #[test]
158 fn test_is_not_found() {
159 assert!(DukascopyError::DataNotFound.is_not_found());
160 assert!(DukascopyError::DataNotFoundFor {
161 pair: "EUR/USD".into(),
162 timestamp: "2024-01-01".into()
163 }
164 .is_not_found());
165
166 assert!(!DukascopyError::InvalidTickData.is_not_found());
167 }
168
169 #[test]
170 fn test_is_validation_error() {
171 assert!(DukascopyError::InvalidTickData.is_validation_error());
172 assert!(DukascopyError::InvalidCurrencyCode {
173 code: "XX".into(),
174 reason: "too short".into()
175 }
176 .is_validation_error());
177
178 assert!(!DukascopyError::DataNotFound.is_validation_error());
179 }
180
181 #[test]
182 fn test_error_display() {
183 let err = DukascopyError::InvalidCurrencyCode {
184 code: "XX".into(),
185 reason: "must be 3 characters".into(),
186 };
187 assert_eq!(
188 err.to_string(),
189 "Invalid currency code 'XX': must be 3 characters"
190 );
191
192 let err = DukascopyError::DataNotFoundFor {
193 pair: "EUR/USD".into(),
194 timestamp: "2024-01-01 12:00:00".into(),
195 };
196 assert!(err.to_string().contains("EUR/USD"));
197 assert!(err.to_string().contains("2024-01-01"));
198 }
199}