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: {method} - {message} (code: {error_code})")]
15 Api {
16 method: String,
17 message: String,
18 error_code: u32,
19 retryable: bool,
20 },
21
22 #[error("Rate limit exceeded. Retry after {retry_after:?}")]
24 RateLimited { retry_after: Option<Duration> },
25
26 #[error("Network error: {0}")]
28 Network(#[from] reqwest::Error),
29
30 #[error("Parse error: {0}")]
32 Parse(#[from] serde_json::Error),
33
34 #[error("I/O error: {0}")]
36 Io(#[from] std::io::Error),
37
38 #[error("CSV error: {0}")]
40 Csv(#[from] csv::Error),
41
42 #[error(
44 "Missing required environment variable: {0}\nPlease set it in your environment or .env file"
45 )]
46 MissingEnvVar(String),
47
48 #[error("Configuration error: {0}")]
50 Config(String),
51
52 #[error("{0}")]
54 Other(String),
55}
56
57impl LastFmError {
58 #[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 #[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
78impl 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
85pub 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}