use serde::{Deserialize, Serialize};
use serde_json::Value;
use thiserror::Error;
#[derive(Error, Debug, Deserialize, Serialize)]
pub enum SurgeError {
#[error("HTTP error: {0}")]
Http(String),
#[error("API error (status: {status:?}): {message}")]
Api {
status: Option<u16>,
message: String,
details: Value,
},
#[error("TLS error: {0}")]
Tls(String),
#[error("JSON error: {0}")]
Json(String),
#[error("IO error: {0}")]
Io(String),
#[error("Ignore error: {0}")]
Ignore(String),
#[error("Invalid project: {0}")]
InvalidProject(String),
#[error("Authentication error: {0}")]
Auth(String),
#[error("Network error: {0}")]
Network(String),
#[error("Configuration error: {0}")]
Config(String),
#[error("Event error: {0}")]
Event(String),
#[error("Unknown error: {0}")]
Unknown(String),
}
impl SurgeError {
pub fn api(status: Option<u16>, message: impl Into<String>, details: Value) -> Self {
SurgeError::Api {
status,
message: message.into(),
details,
}
}
}
impl From<reqwest::Error> for SurgeError {
fn from(err: reqwest::Error) -> Self {
if err.is_status() {
SurgeError::Http(format!("HTTP status error: {}", err))
} else if err.is_timeout() {
SurgeError::Network(format!("Request timeout: {}", err))
} else if err.is_connect() {
SurgeError::Network(format!("Connection error: {}", err))
} else {
SurgeError::Http(format!("HTTP error: {}", err))
}
}
}
impl From<std::io::Error> for SurgeError {
fn from(err: std::io::Error) -> Self {
SurgeError::Io(err.to_string())
}
}
impl From<serde_json::Error> for SurgeError {
fn from(err: serde_json::Error) -> Self {
SurgeError::Json(err.to_string())
}
}
impl From<ignore::Error> for SurgeError {
fn from(err: ignore::Error) -> Self {
SurgeError::Ignore(err.to_string())
}
}
impl From<url::ParseError> for SurgeError {
fn from(err: url::ParseError) -> Self {
SurgeError::Config(format!("URL parse error: {}", err))
}
}
impl From<rustls::Error> for SurgeError {
fn from(err: rustls::Error) -> Self {
SurgeError::Tls(err.to_string())
}
}
impl From<std::path::StripPrefixError> for SurgeError {
fn from(err: std::path::StripPrefixError) -> Self {
SurgeError::InvalidProject(err.to_string())
}
}
impl From<tokio::task::JoinError> for SurgeError {
fn from(err: tokio::task::JoinError) -> Self {
SurgeError::Unknown(err.to_string())
}
}
impl From<Box<dyn std::error::Error + Send + Sync>> for SurgeError {
fn from(err: Box<dyn std::error::Error + Send + Sync>) -> Self {
SurgeError::Unknown(err.to_string())
}
}
#[derive(Debug, Deserialize, Serialize)]
pub struct ApiErrorResponse {
pub errors: Vec<String>,
pub details: Value,
pub status: Option<u16>,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_surge_error_from_strip_prefix() {
let strip_err = std::path::Path::new("/a").strip_prefix("/b").unwrap_err();
let surge_err = SurgeError::from(strip_err);
assert!(matches!(surge_err, SurgeError::InvalidProject(_))); if let SurgeError::InvalidProject(msg) = surge_err {
assert!(!msg.is_empty());
}
}
#[tokio::test]
async fn test_surge_error_from_join_error() {
let join_err = tokio::task::spawn(async { panic!("test panic") })
.await
.unwrap_err();
let surge_err = SurgeError::from(join_err);
assert!(matches!(surge_err, SurgeError::Unknown(_))); if let SurgeError::Unknown(msg) = surge_err {
assert!(msg.contains("test panic"));
}
}
#[test]
fn test_api_error_deserialization() {
let json = json!({
"errors": ["Invalid token"],
"details": {},
"status": 401
});
let api_err: ApiErrorResponse = serde_json::from_value(json).unwrap();
assert_eq!(api_err.errors, vec!["Invalid token"]);
assert_eq!(api_err.status, Some(401));
assert_eq!(api_err.details, serde_json::json!({}));
}
}