pub type Result<T> = std::result::Result<T, ZealError>;
#[derive(Debug, thiserror::Error)]
pub enum ZealError {
#[error("Network error: {source}")]
NetworkError {
#[source]
source: reqwest::Error,
retryable: bool,
},
#[error("WebSocket error: {message}")]
WebSocketError { message: String },
#[error("JSON error: {source}")]
JsonError {
#[source]
source: serde_json::Error,
},
#[error("Configuration error: {message}")]
ConfigurationError { message: String },
#[error("API error ({status}): {message}")]
ApiError {
status: u16,
message: String,
error_code: Option<String>,
},
#[error("Resource not found: {resource} with ID '{id}'")]
NotFound { resource: String, id: String },
#[error("Validation error in field '{field}': {message}")]
ValidationError { field: String, message: String },
#[error("Authentication error: {message}")]
AuthenticationError { message: String },
#[error("Rate limit exceeded: {message}")]
RateLimitError {
message: String,
retry_after: Option<std::time::Duration>,
},
#[error("Operation timed out: {operation}")]
TimeoutError { operation: String },
#[error("Connection error: {message}")]
ConnectionError { message: String },
#[error("Serialization error: {source}")]
SerializationError {
#[source]
source: Box<dyn std::error::Error + Send + Sync>,
},
#[error("Invalid URL: {source}")]
InvalidUrl {
#[source]
source: url::ParseError,
},
#[error("I/O error: {source}")]
IoError {
#[source]
source: std::io::Error,
},
#[error("Error: {message}")]
Other { message: String },
}
impl Clone for ZealError {
fn clone(&self) -> Self {
match self {
Self::NetworkError { .. } => Self::Other {
message: "Network error".to_string(),
},
Self::WebSocketError { message } => Self::WebSocketError {
message: message.clone(),
},
Self::JsonError { .. } => Self::Other {
message: "JSON parsing error".to_string(),
},
Self::ConfigurationError { message } => Self::ConfigurationError {
message: message.clone(),
},
Self::ApiError {
status,
message,
error_code,
} => Self::ApiError {
status: *status,
message: message.clone(),
error_code: error_code.clone(),
},
Self::NotFound { resource, id } => Self::NotFound {
resource: resource.clone(),
id: id.clone(),
},
Self::ValidationError { field, message } => Self::ValidationError {
field: field.clone(),
message: message.clone(),
},
Self::AuthenticationError { message } => Self::AuthenticationError {
message: message.clone(),
},
Self::RateLimitError {
message,
retry_after,
} => Self::RateLimitError {
message: message.clone(),
retry_after: *retry_after,
},
Self::TimeoutError { operation } => Self::TimeoutError {
operation: operation.clone(),
},
Self::ConnectionError { message } => Self::ConnectionError {
message: message.clone(),
},
Self::SerializationError { .. } => Self::Other {
message: "Serialization error".to_string(),
},
Self::InvalidUrl { .. } => Self::Other {
message: "Invalid URL".to_string(),
},
Self::IoError { .. } => Self::Other {
message: "IO error".to_string(),
},
Self::Other { message } => Self::Other {
message: message.clone(),
},
}
}
}
impl ZealError {
pub fn network_error(source: reqwest::Error) -> Self {
let retryable = source.is_timeout()
|| source.is_connect()
|| source
.status()
.is_some_and(|s| matches!(s.as_u16(), 408 | 429 | 500..=599));
Self::NetworkError { source, retryable }
}
pub fn websocket_error<S: Into<String>>(message: S) -> Self {
Self::WebSocketError {
message: message.into(),
}
}
pub fn configuration_error<S: Into<String>>(message: S) -> Self {
Self::ConfigurationError {
message: message.into(),
}
}
pub fn api_error(status: u16, message: String, error_code: Option<String>) -> Self {
Self::ApiError {
status,
message,
error_code,
}
}
pub fn not_found<S: Into<String>>(resource: S, id: S) -> Self {
Self::NotFound {
resource: resource.into(),
id: id.into(),
}
}
pub fn validation_error<S: Into<String>>(field: S, message: S) -> Self {
Self::ValidationError {
field: field.into(),
message: message.into(),
}
}
pub fn authentication_error<S: Into<String>>(message: S) -> Self {
Self::AuthenticationError {
message: message.into(),
}
}
pub fn rate_limit_error<S: Into<String>>(
message: S,
retry_after: Option<std::time::Duration>,
) -> Self {
Self::RateLimitError {
message: message.into(),
retry_after,
}
}
pub fn timeout_error<S: Into<String>>(operation: S) -> Self {
Self::TimeoutError {
operation: operation.into(),
}
}
pub fn connection_error<S: Into<String>>(message: S) -> Self {
Self::ConnectionError {
message: message.into(),
}
}
pub fn other<S: Into<String>>(message: S) -> Self {
Self::Other {
message: message.into(),
}
}
pub fn is_retryable(&self) -> bool {
match self {
Self::NetworkError { retryable, .. } => *retryable,
Self::RateLimitError { .. } => true,
Self::TimeoutError { .. } => true,
Self::ConnectionError { .. } => true,
Self::ApiError { status, .. } => matches!(*status, 408 | 429 | 500..=599),
_ => false,
}
}
pub fn retry_after(&self) -> Option<std::time::Duration> {
match self {
Self::RateLimitError { retry_after, .. } => *retry_after,
_ => None,
}
}
pub fn is_client_error(&self) -> bool {
match self {
Self::ApiError { status, .. } => matches!(*status, 400..=499),
Self::NotFound { .. } => true,
Self::ValidationError { .. } => true,
Self::AuthenticationError { .. } => true,
_ => false,
}
}
pub fn is_server_error(&self) -> bool {
match self {
Self::ApiError { status, .. } => matches!(*status, 500..=599),
_ => false,
}
}
}
impl From<reqwest::Error> for ZealError {
fn from(err: reqwest::Error) -> Self {
Self::network_error(err)
}
}
impl From<serde_json::Error> for ZealError {
fn from(err: serde_json::Error) -> Self {
Self::JsonError { source: err }
}
}
impl From<url::ParseError> for ZealError {
fn from(err: url::ParseError) -> Self {
Self::InvalidUrl { source: err }
}
}
impl From<std::io::Error> for ZealError {
fn from(err: std::io::Error) -> Self {
Self::IoError { source: err }
}
}
impl From<tokio_tungstenite::tungstenite::Error> for ZealError {
fn from(err: tokio_tungstenite::tungstenite::Error) -> Self {
Self::websocket_error(err.to_string())
}
}
#[derive(Debug, Default)]
pub struct ErrorBuilder {
message: Option<String>,
source: Option<Box<dyn std::error::Error + Send + Sync>>,
retryable: bool,
status: Option<u16>,
error_code: Option<String>,
}
impl ErrorBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn message<S: Into<String>>(mut self, message: S) -> Self {
self.message = Some(message.into());
self
}
pub fn source<E: std::error::Error + Send + Sync + 'static>(mut self, source: E) -> Self {
self.source = Some(Box::new(source));
self
}
pub fn retryable(mut self, retryable: bool) -> Self {
self.retryable = retryable;
self
}
pub fn status(mut self, status: u16) -> Self {
self.status = Some(status);
self
}
pub fn error_code<S: Into<String>>(mut self, code: S) -> Self {
self.error_code = Some(code.into());
self
}
pub fn build(self) -> ZealError {
let message = self.message.unwrap_or_else(|| "Unknown error".to_string());
if let Some(status) = self.status {
ZealError::ApiError {
status,
message,
error_code: self.error_code,
}
} else if let Some(source) = self.source {
ZealError::SerializationError { source }
} else {
ZealError::Other { message }
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_creation() {
let err = ZealError::not_found("template", "test-id");
assert!(matches!(err, ZealError::NotFound { .. }));
}
#[test]
fn test_retryable_errors() {
let err = ZealError::api_error(500, "Server error".to_string(), None);
assert!(err.is_retryable());
let err = ZealError::api_error(400, "Bad request".to_string(), None);
assert!(!err.is_retryable());
}
#[test]
fn test_error_builder() {
let err = ErrorBuilder::new()
.message("Test error")
.status(404)
.build();
assert!(matches!(err, ZealError::ApiError { status: 404, .. }));
}
#[test]
fn test_client_server_error_classification() {
let client_err = ZealError::api_error(400, "Bad request".to_string(), None);
assert!(client_err.is_client_error());
assert!(!client_err.is_server_error());
let server_err = ZealError::api_error(500, "Server error".to_string(), None);
assert!(!server_err.is_client_error());
assert!(server_err.is_server_error());
}
}