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    #[error("API request failed: {method} - {message} (code: {error_code})")]
15    Api {
16        method: String,
17        message: String,
18        error_code: u32,
19        retryable: bool,
20    },
21
22    /// Represents rate limiting error
23    #[error("Rate limit exceeded. Retry after {retry_after:?}")]
24    RateLimited { retry_after: Option<Duration> },
25
26    /// Represents HTTP/network errors
27    #[error("Network error: {0}")]
28    Network(#[from] reqwest::Error),
29
30    /// Represents JSON parsing errors
31    #[error("Parse error: {0}")]
32    Parse(#[from] serde_json::Error),
33
34    /// Represents file I/O errors
35    #[error("I/O error: {0}")]
36    Io(#[from] std::io::Error),
37
38    /// Represents CSV errors
39    #[error("CSV error: {0}")]
40    Csv(#[from] csv::Error),
41
42    /// Represents missing environment variable errors
43    #[error(
44        "Missing required environment variable: {0}\nPlease set it in your environment or .env file"
45    )]
46    MissingEnvVar(String),
47
48    /// Represents configuration errors
49    #[error("Configuration error: {0}")]
50    Config(String),
51
52    /// Represents other errors
53    #[error("{0}")]
54    Other(String),
55}
56
57impl LastFmError {
58    /// Check if this error is retryable
59    #[must_use]
60    pub fn is_retryable(&self) -> bool {
61        match self {
62            LastFmError::Api { retryable, .. } => *retryable,
63            LastFmError::RateLimited { .. } | LastFmError::Network(_) => true,
64            _ => false,
65        }
66    }
67
68    /// Get the retry delay if specified
69    #[must_use]
70    pub fn retry_after(&self) -> Option<Duration> {
71        match self {
72            LastFmError::RateLimited { retry_after } => *retry_after,
73            _ => None,
74        }
75    }
76}
77
78// Handle Box<dyn std::error::Error> for compatibility with existing code
79impl From<Box<dyn std::error::Error>> for LastFmError {
80    fn from(err: Box<dyn std::error::Error>) -> Self {
81        LastFmError::Other(err.to_string())
82    }
83}
84
85/// Helper type for Result with `LastFmError`
86pub type Result<T> = std::result::Result<T, LastFmError>;
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91
92    #[test]
93    fn test_retryable_errors() {
94        let api_error = LastFmError::Api {
95            method: "test.method".to_string(),
96            message: "Temporary error".to_string(),
97            error_code: 500,
98            retryable: true,
99        };
100        assert!(api_error.is_retryable());
101
102        let non_retryable = LastFmError::Api {
103            method: "test.method".to_string(),
104            message: "Invalid API key".to_string(),
105            error_code: 10,
106            retryable: false,
107        };
108        assert!(!non_retryable.is_retryable());
109
110        let rate_limited = LastFmError::RateLimited {
111            retry_after: Some(Duration::from_secs(5)),
112        };
113        assert!(rate_limited.is_retryable());
114
115        let parse_error = LastFmError::Parse(serde_json::from_str::<()>("invalid").unwrap_err());
116        assert!(!parse_error.is_retryable());
117    }
118
119    #[test]
120    fn test_rate_limit_retry_after() {
121        let error = LastFmError::RateLimited {
122            retry_after: Some(Duration::from_secs(5)),
123        };
124        assert_eq!(error.retry_after(), Some(Duration::from_secs(5)));
125
126        let api_error = LastFmError::Api {
127            method: "test".to_string(),
128            message: "Error".to_string(),
129            error_code: 500,
130            retryable: true,
131        };
132        assert_eq!(api_error.retry_after(), None);
133    }
134
135    #[test]
136    fn test_error_display() {
137        let error = LastFmError::MissingEnvVar("LAST_FM_API_KEY".to_string());
138        let display = format!("{error}");
139        assert!(display.contains("LAST_FM_API_KEY"));
140        assert!(display.contains("Please set it"));
141    }
142}