1use serde::{Deserialize, Serialize};
2use std::time::Duration;
3use thiserror::Error;
4
5#[derive(Debug, Deserialize, Serialize)]
6pub struct LastFmErrorResponse {
7 pub message: String,
8 pub error: u32,
9}
10
11#[derive(Debug, Error)]
12pub enum LastFmError {
13 #[error("api request failed")]
16 Api {
17 method: String,
18 message: String,
19 error_code: u32,
20 retryable: bool,
21 },
22
23 #[error("rate limit exceeded")]
26 RateLimited { retry_after: Option<Duration> },
27
28 #[error("network error")]
31 Network(#[from] reqwest::Error),
32
33 #[error("failed to parse response")]
36 Parse(#[from] serde_json::Error),
37
38 #[error("file operation failed")]
41 Io(#[from] std::io::Error),
42
43 #[error("csv operation failed")]
46 Csv(#[from] csv::Error),
47
48 #[error("missing required environment variable")]
50 MissingEnvVar(String),
51
52 #[error("configuration error")]
54 Config(String),
55
56 #[error("http error")]
58 Http {
59 status: u16,
60 #[source]
61 source: Option<Box<dyn std::error::Error + Send + Sync>>,
62 },
63
64 #[error("invalid url")]
66 Url {
67 #[source]
68 source: url::ParseError,
69 },
70}
71
72impl LastFmError {
73 #[must_use]
75 pub fn is_retryable(&self) -> bool {
76 match self {
77 LastFmError::Api { retryable, .. } => *retryable,
78 LastFmError::RateLimited { .. } | LastFmError::Network(_) => true,
79 _ => false,
80 }
81 }
82
83 #[must_use]
85 pub fn retry_after(&self) -> Option<Duration> {
86 match self {
87 LastFmError::RateLimited { retry_after } => *retry_after,
88 _ => None,
89 }
90 }
91
92 #[must_use]
94 pub fn api_method(&self) -> Option<&str> {
95 match self {
96 LastFmError::Api { method, .. } => Some(method),
97 _ => None,
98 }
99 }
100
101 #[must_use]
103 pub fn api_error_code(&self) -> Option<u32> {
104 match self {
105 LastFmError::Api { error_code, .. } => Some(*error_code),
106 _ => None,
107 }
108 }
109
110 #[must_use]
112 pub fn api_message(&self) -> Option<&str> {
113 match self {
114 LastFmError::Api { message, .. } => Some(message),
115 _ => None,
116 }
117 }
118
119 #[must_use]
121 pub fn env_var_name(&self) -> Option<&str> {
122 match self {
123 LastFmError::MissingEnvVar(name) => Some(name),
124 _ => None,
125 }
126 }
127
128 #[must_use]
130 pub fn http_status(&self) -> Option<u16> {
131 match self {
132 LastFmError::Http { status, .. } => Some(*status),
133 _ => None,
134 }
135 }
136}
137
138impl From<url::ParseError> for LastFmError {
139 fn from(err: url::ParseError) -> Self {
140 LastFmError::Url { source: err }
141 }
142}
143
144pub type Result<T> = std::result::Result<T, LastFmError>;
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 #[test]
152 fn test_retryable_errors() {
153 let api_error = LastFmError::Api {
154 method: "test.method".to_string(),
155 message: "Temporary error".to_string(),
156 error_code: 500,
157 retryable: true,
158 };
159 assert!(api_error.is_retryable());
160
161 let non_retryable = LastFmError::Api {
162 method: "test.method".to_string(),
163 message: "Invalid API key".to_string(),
164 error_code: 10,
165 retryable: false,
166 };
167 assert!(!non_retryable.is_retryable());
168
169 let rate_limited = LastFmError::RateLimited {
170 retry_after: Some(Duration::from_secs(5)),
171 };
172 assert!(rate_limited.is_retryable());
173
174 let parse_error = LastFmError::Parse(serde_json::from_str::<()>("invalid").unwrap_err());
175 assert!(!parse_error.is_retryable());
176 }
177
178 #[test]
179 fn test_rate_limit_retry_after() {
180 let error = LastFmError::RateLimited {
181 retry_after: Some(Duration::from_secs(5)),
182 };
183 assert_eq!(error.retry_after(), Some(Duration::from_secs(5)));
184
185 let api_error = LastFmError::Api {
186 method: "test".to_string(),
187 message: "Error".to_string(),
188 error_code: 500,
189 retryable: true,
190 };
191 assert_eq!(api_error.retry_after(), None);
192 }
193
194 #[test]
195 fn test_error_display() {
196 let error = LastFmError::MissingEnvVar("LAST_FM_API_KEY".to_string());
197 let display = format!("{error}");
198 assert_eq!(display, "missing required environment variable");
199 }
200
201 #[test]
202 fn test_api_error_accessors() {
203 let error = LastFmError::Api {
204 method: "user.getrecenttracks".to_string(),
205 message: "Invalid API key".to_string(),
206 error_code: 10,
207 retryable: false,
208 };
209
210 assert_eq!(error.api_method(), Some("user.getrecenttracks"));
211 assert_eq!(error.api_error_code(), Some(10));
212 assert_eq!(error.api_message(), Some("Invalid API key"));
213
214 let parse_error = LastFmError::Parse(serde_json::from_str::<()>("invalid").unwrap_err());
216 assert_eq!(parse_error.api_method(), None);
217 assert_eq!(parse_error.api_error_code(), None);
218 assert_eq!(parse_error.api_message(), None);
219 }
220
221 #[test]
222 fn test_env_var_accessor() {
223 let error = LastFmError::MissingEnvVar("LAST_FM_API_KEY".to_string());
224 assert_eq!(error.env_var_name(), Some("LAST_FM_API_KEY"));
225
226 let api_error = LastFmError::Api {
227 method: "test".to_string(),
228 message: "Error".to_string(),
229 error_code: 10,
230 retryable: false,
231 };
232 assert_eq!(api_error.env_var_name(), None);
233 }
234
235 #[test]
236 fn test_http_error() {
237 let error = LastFmError::Http {
238 status: 404,
239 source: None,
240 };
241 assert_eq!(error.http_status(), Some(404));
242 assert_eq!(format!("{error}"), "http error");
243 }
244
245 #[test]
246 fn test_display_messages_format() {
247 assert_eq!(
249 format!(
250 "{}",
251 LastFmError::Api {
252 method: "test".to_string(),
253 message: "msg".to_string(),
254 error_code: 1,
255 retryable: false
256 }
257 ),
258 "api request failed"
259 );
260 assert_eq!(
261 format!("{}", LastFmError::RateLimited { retry_after: None }),
262 "rate limit exceeded"
263 );
264 assert_eq!(
265 format!("{}", LastFmError::MissingEnvVar("TEST".to_string())),
266 "missing required environment variable"
267 );
268 assert_eq!(
269 format!("{}", LastFmError::Config("test".to_string())),
270 "configuration error"
271 );
272 }
273}