lastfm_client/
error.rs

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    /// Represents a Last.fm API error with code and message
14    /// Access details via the struct fields: `method`, `message`, `error_code`, `retryable`
15    #[error("api request failed")]
16    Api {
17        method: String,
18        message: String,
19        error_code: u32,
20        retryable: bool,
21    },
22
23    /// Represents rate limiting error
24    /// Access `retry_after` via the struct field
25    #[error("rate limit exceeded")]
26    RateLimited { retry_after: Option<Duration> },
27
28    /// Represents HTTP/network errors
29    /// Access source error via `Error::source()`
30    #[error("network error")]
31    Network(#[from] reqwest::Error),
32
33    /// Represents JSON parsing errors
34    /// Access source error via `Error::source()`
35    #[error("failed to parse response")]
36    Parse(#[from] serde_json::Error),
37
38    /// Represents file I/O errors
39    /// Access source error via `Error::source()`
40    #[error("file operation failed")]
41    Io(#[from] std::io::Error),
42
43    /// Represents CSV errors
44    /// Access source error via `Error::source()`
45    #[error("csv operation failed")]
46    Csv(#[from] csv::Error),
47
48    /// Represents missing environment variable errors
49    #[error("missing required environment variable")]
50    MissingEnvVar(String),
51
52    /// Represents configuration errors
53    #[error("configuration error")]
54    Config(String),
55
56    /// Represents HTTP response errors (non-success status codes)
57    #[error("http error")]
58    Http {
59        status: u16,
60        #[source]
61        source: Option<Box<dyn std::error::Error + Send + Sync>>,
62    },
63
64    /// Represents URL parsing errors
65    #[error("invalid url")]
66    Url {
67        #[source]
68        source: url::ParseError,
69    },
70}
71
72impl LastFmError {
73    /// Check if this error is retryable
74    #[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    /// Get the retry delay if specified
84    #[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    /// Get the API method name if this is an API error
93    #[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    /// Get the API error code if this is an API error
102    #[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    /// Get the API error message if this is an API error
111    #[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    /// Get the environment variable name if this is a missing env var error
120    #[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    /// Get the HTTP status code if this is an HTTP error
129    #[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
144/// Helper type for Result with `LastFmError`
145pub 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        // Test that non-API errors return None
215        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        // All Display messages should be lowercase and concise
248        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}