use serde::{Deserialize, Serialize};
use std::time::Duration;
use thiserror::Error;
#[derive(Debug, Deserialize, Serialize)]
pub struct LastFmErrorResponse {
pub message: String,
pub error: u32,
}
#[derive(Debug, Error)]
pub enum LastFmError {
#[error("api request failed")]
Api {
method: String,
message: String,
error_code: u32,
retryable: bool,
},
#[error("rate limit exceeded")]
RateLimited { retry_after: Option<Duration> },
#[error("network error")]
Network(#[from] reqwest::Error),
#[error("failed to parse response")]
Parse(#[from] serde_json::Error),
#[error("file operation failed")]
Io(#[from] std::io::Error),
#[error("csv operation failed")]
Csv(#[from] csv::Error),
#[error("missing required environment variable")]
MissingEnvVar(String),
#[error("configuration error")]
Config(String),
#[error("http error")]
Http {
status: u16,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
#[error("invalid url")]
Url {
#[source]
source: url::ParseError,
},
}
impl LastFmError {
#[must_use]
pub fn is_retryable(&self) -> bool {
match self {
LastFmError::Api { retryable, .. } => *retryable,
LastFmError::RateLimited { .. } | LastFmError::Network(_) => true,
_ => false,
}
}
#[must_use]
pub fn retry_after(&self) -> Option<Duration> {
match self {
LastFmError::RateLimited { retry_after } => *retry_after,
_ => None,
}
}
#[must_use]
pub fn api_method(&self) -> Option<&str> {
match self {
LastFmError::Api { method, .. } => Some(method),
_ => None,
}
}
#[must_use]
pub fn api_error_code(&self) -> Option<u32> {
match self {
LastFmError::Api { error_code, .. } => Some(*error_code),
_ => None,
}
}
#[must_use]
pub fn api_message(&self) -> Option<&str> {
match self {
LastFmError::Api { message, .. } => Some(message),
_ => None,
}
}
#[must_use]
pub fn env_var_name(&self) -> Option<&str> {
match self {
LastFmError::MissingEnvVar(name) => Some(name),
_ => None,
}
}
#[must_use]
pub fn http_status(&self) -> Option<u16> {
match self {
LastFmError::Http { status, .. } => Some(*status),
_ => None,
}
}
}
impl From<url::ParseError> for LastFmError {
fn from(err: url::ParseError) -> Self {
LastFmError::Url { source: err }
}
}
pub type Result<T> = std::result::Result<T, LastFmError>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_retryable_errors() {
let api_error = LastFmError::Api {
method: "test.method".to_string(),
message: "Temporary error".to_string(),
error_code: 500,
retryable: true,
};
assert!(api_error.is_retryable());
let non_retryable = LastFmError::Api {
method: "test.method".to_string(),
message: "Invalid API key".to_string(),
error_code: 10,
retryable: false,
};
assert!(!non_retryable.is_retryable());
let rate_limited = LastFmError::RateLimited {
retry_after: Some(Duration::from_secs(5)),
};
assert!(rate_limited.is_retryable());
let parse_error = LastFmError::Parse(serde_json::from_str::<()>("invalid").unwrap_err());
assert!(!parse_error.is_retryable());
}
#[test]
fn test_rate_limit_retry_after() {
let error = LastFmError::RateLimited {
retry_after: Some(Duration::from_secs(5)),
};
assert_eq!(error.retry_after(), Some(Duration::from_secs(5)));
let api_error = LastFmError::Api {
method: "test".to_string(),
message: "Error".to_string(),
error_code: 500,
retryable: true,
};
assert_eq!(api_error.retry_after(), None);
}
#[test]
fn test_error_display() {
let error = LastFmError::MissingEnvVar("LAST_FM_API_KEY".to_string());
let display = format!("{error}");
assert_eq!(display, "missing required environment variable");
}
#[test]
fn test_api_error_accessors() {
let error = LastFmError::Api {
method: "user.getrecenttracks".to_string(),
message: "Invalid API key".to_string(),
error_code: 10,
retryable: false,
};
assert_eq!(error.api_method(), Some("user.getrecenttracks"));
assert_eq!(error.api_error_code(), Some(10));
assert_eq!(error.api_message(), Some("Invalid API key"));
let parse_error = LastFmError::Parse(serde_json::from_str::<()>("invalid").unwrap_err());
assert_eq!(parse_error.api_method(), None);
assert_eq!(parse_error.api_error_code(), None);
assert_eq!(parse_error.api_message(), None);
}
#[test]
fn test_env_var_accessor() {
let error = LastFmError::MissingEnvVar("LAST_FM_API_KEY".to_string());
assert_eq!(error.env_var_name(), Some("LAST_FM_API_KEY"));
let api_error = LastFmError::Api {
method: "test".to_string(),
message: "Error".to_string(),
error_code: 10,
retryable: false,
};
assert_eq!(api_error.env_var_name(), None);
}
#[test]
fn test_http_error() {
let error = LastFmError::Http {
status: 404,
source: None,
};
assert_eq!(error.http_status(), Some(404));
assert_eq!(format!("{error}"), "http error");
}
#[test]
fn test_display_messages_format() {
assert_eq!(
format!(
"{}",
LastFmError::Api {
method: "test".to_string(),
message: "msg".to_string(),
error_code: 1,
retryable: false
}
),
"api request failed"
);
assert_eq!(
format!("{}", LastFmError::RateLimited { retry_after: None }),
"rate limit exceeded"
);
assert_eq!(
format!("{}", LastFmError::MissingEnvVar("TEST".to_string())),
"missing required environment variable"
);
assert_eq!(
format!("{}", LastFmError::Config("test".to_string())),
"configuration error"
);
}
}