1use serde::{Deserialize, Serialize};
2use std::time::Duration;
3use thiserror::Error;
4
5#[derive(Debug, Deserialize, Serialize)]
7#[non_exhaustive]
8pub struct LastFmErrorResponse {
9 pub message: String,
11 pub error: u32,
13}
14
15#[derive(Debug, Error)]
17#[non_exhaustive]
18pub enum LastFmError {
19 #[error("api error (method: {method}, code: {error_code}): {message}")]
22 Api {
23 method: String,
25 message: String,
27 error_code: u32,
29 retryable: bool,
31 },
32
33 #[error("rate limit exceeded{}", retry_after.map(|d| format!(" (retry after {}ms)", d.as_millis())).unwrap_or_default())]
36 RateLimited {
37 retry_after: Option<Duration>,
39 },
40
41 #[error("network error: {0}")]
44 Network(#[from] reqwest::Error),
45
46 #[error("failed to parse response: {0}")]
49 Parse(#[from] serde_json::Error),
50
51 #[error("file operation failed: {0}")]
54 Io(#[from] std::io::Error),
55
56 #[error("csv operation failed: {0}")]
59 Csv(#[from] csv::Error),
60
61 #[error("missing required environment variable: {0}")]
63 MissingEnvVar(String),
64
65 #[error("configuration error: {0}")]
67 Config(String),
68
69 #[error("http error (status: {status})")]
71 Http {
72 status: u16,
74 #[source]
76 source: Option<Box<dyn std::error::Error + Send + Sync>>,
77 },
78
79 #[error("invalid url: {source}")]
81 Url {
82 #[source]
84 source: url::ParseError,
85 },
86}
87
88impl LastFmError {
89 #[must_use]
91 pub const fn is_retryable(&self) -> bool {
92 match self {
93 Self::Api { retryable, .. } => *retryable,
94 Self::RateLimited { .. } | Self::Network(_) => true,
95 _ => false,
96 }
97 }
98
99 #[must_use]
101 pub const fn retry_after(&self) -> Option<Duration> {
102 match self {
103 Self::RateLimited { retry_after } => *retry_after,
104 _ => None,
105 }
106 }
107
108 #[must_use]
110 pub fn api_method(&self) -> Option<&str> {
111 match self {
112 Self::Api { method, .. } => Some(method),
113 _ => None,
114 }
115 }
116
117 #[must_use]
119 pub const fn api_error_code(&self) -> Option<u32> {
120 match self {
121 Self::Api { error_code, .. } => Some(*error_code),
122 _ => None,
123 }
124 }
125
126 #[must_use]
128 pub fn api_message(&self) -> Option<&str> {
129 match self {
130 Self::Api { message, .. } => Some(message),
131 _ => None,
132 }
133 }
134
135 #[must_use]
137 pub fn env_var_name(&self) -> Option<&str> {
138 match self {
139 Self::MissingEnvVar(name) => Some(name),
140 _ => None,
141 }
142 }
143
144 #[must_use]
146 pub const fn http_status(&self) -> Option<u16> {
147 match self {
148 Self::Http { status, .. } => Some(*status),
149 _ => None,
150 }
151 }
152}
153
154impl From<url::ParseError> for LastFmError {
155 fn from(err: url::ParseError) -> Self {
156 Self::Url { source: err }
157 }
158}
159
160pub type Result<T> = std::result::Result<T, LastFmError>;
162
163#[cfg(test)]
164#[allow(clippy::unwrap_used)]
165mod tests {
166 use super::*;
167
168 #[test]
169 fn test_retryable_errors() {
170 let api_error = LastFmError::Api {
171 method: "test.method".to_string(),
172 message: "Temporary error".to_string(),
173 error_code: 500,
174 retryable: true,
175 };
176 assert!(api_error.is_retryable());
177
178 let non_retryable = LastFmError::Api {
179 method: "test.method".to_string(),
180 message: "Invalid API key".to_string(),
181 error_code: 10,
182 retryable: false,
183 };
184 assert!(!non_retryable.is_retryable());
185
186 let rate_limited = LastFmError::RateLimited {
187 retry_after: Some(Duration::from_secs(5)),
188 };
189 assert!(rate_limited.is_retryable());
190
191 let parse_error = LastFmError::Parse(serde_json::from_str::<()>("invalid").unwrap_err());
192 assert!(!parse_error.is_retryable());
193 }
194
195 #[test]
196 fn test_rate_limit_retry_after() {
197 let error = LastFmError::RateLimited {
198 retry_after: Some(Duration::from_secs(5)),
199 };
200 assert_eq!(error.retry_after(), Some(Duration::from_secs(5)));
201
202 let api_error = LastFmError::Api {
203 method: "test".to_string(),
204 message: "Error".to_string(),
205 error_code: 500,
206 retryable: true,
207 };
208 assert_eq!(api_error.retry_after(), None);
209 }
210
211 #[test]
212 fn test_error_display() {
213 let error = LastFmError::MissingEnvVar("LAST_FM_API_KEY".to_string());
214 let display = format!("{error}");
215 assert_eq!(
216 display,
217 "missing required environment variable: LAST_FM_API_KEY"
218 );
219 }
220
221 #[test]
222 fn test_api_error_accessors() {
223 let error = LastFmError::Api {
224 method: "user.getrecenttracks".to_string(),
225 message: "Invalid API key".to_string(),
226 error_code: 10,
227 retryable: false,
228 };
229
230 assert_eq!(error.api_method(), Some("user.getrecenttracks"));
231 assert_eq!(error.api_error_code(), Some(10));
232 assert_eq!(error.api_message(), Some("Invalid API key"));
233
234 let parse_error = LastFmError::Parse(serde_json::from_str::<()>("invalid").unwrap_err());
236 assert_eq!(parse_error.api_method(), None);
237 assert_eq!(parse_error.api_error_code(), None);
238 assert_eq!(parse_error.api_message(), None);
239 }
240
241 #[test]
242 fn test_env_var_accessor() {
243 let error = LastFmError::MissingEnvVar("LAST_FM_API_KEY".to_string());
244 assert_eq!(error.env_var_name(), Some("LAST_FM_API_KEY"));
245
246 let api_error = LastFmError::Api {
247 method: "test".to_string(),
248 message: "Error".to_string(),
249 error_code: 10,
250 retryable: false,
251 };
252 assert_eq!(api_error.env_var_name(), None);
253 }
254
255 #[test]
256 fn test_http_error() {
257 let error = LastFmError::Http {
258 status: 404,
259 source: None,
260 };
261 assert_eq!(error.http_status(), Some(404));
262 assert_eq!(format!("{error}"), "http error (status: 404)");
263 }
264
265 #[test]
266 fn test_display_messages_format() {
267 assert_eq!(
268 format!(
269 "{}",
270 LastFmError::Api {
271 method: "user.getrecenttracks".to_string(),
272 message: "Invalid API key".to_string(),
273 error_code: 10,
274 retryable: false
275 }
276 ),
277 "api error (method: user.getrecenttracks, code: 10): Invalid API key"
278 );
279 assert_eq!(
280 format!("{}", LastFmError::RateLimited { retry_after: None }),
281 "rate limit exceeded"
282 );
283 assert_eq!(
284 format!(
285 "{}",
286 LastFmError::RateLimited {
287 retry_after: Some(Duration::from_secs(5))
288 }
289 ),
290 "rate limit exceeded (retry after 5000ms)"
291 );
292 assert_eq!(
293 format!("{}", LastFmError::MissingEnvVar("TEST".to_string())),
294 "missing required environment variable: TEST"
295 );
296 assert_eq!(
297 format!("{}", LastFmError::Config("bad value".to_string())),
298 "configuration error: bad value"
299 );
300 }
301}