use thiserror::Error;
#[derive(Debug, Error)]
pub enum TapError {
#[error("error-atproto-tap-connection-1 WebSocket connection failed: {0}")]
ConnectionFailed(String),
#[error("error-atproto-tap-connection-2 Connection closed unexpectedly")]
ConnectionClosed,
#[error(
"error-atproto-tap-connection-3 Maximum reconnection attempts exceeded after {0} attempts"
)]
MaxReconnectAttemptsExceeded(u32),
#[error("error-atproto-tap-auth-1 Authentication failed: {0}")]
AuthenticationFailed(String),
#[error("error-atproto-tap-parse-1 Failed to parse message: {0}")]
ParseError(String),
#[error("error-atproto-tap-ack-1 Failed to send acknowledgment: {0}")]
AckFailed(String),
#[error("error-atproto-tap-http-1 HTTP request failed: {0}")]
HttpError(String),
#[error("error-atproto-tap-http-2 HTTP error response: {status} - {message}")]
HttpResponseError {
status: u16,
message: String,
},
#[error("error-atproto-tap-url-1 Invalid URL: {0}")]
InvalidUrl(String),
#[error("error-atproto-tap-io-1 I/O error: {0}")]
IoError(#[from] std::io::Error),
#[error("error-atproto-tap-json-1 JSON error: {0}")]
JsonError(#[from] serde_json::Error),
#[error("error-atproto-tap-stream-1 Stream is closed")]
StreamClosed,
#[error("error-atproto-tap-timeout-1 Operation timed out")]
Timeout,
}
impl TapError {
pub fn is_connection_error(&self) -> bool {
matches!(
self,
TapError::ConnectionFailed(_)
| TapError::ConnectionClosed
| TapError::IoError(_)
| TapError::Timeout
)
}
pub fn is_parse_error(&self) -> bool {
matches!(self, TapError::ParseError(_) | TapError::JsonError(_))
}
pub fn is_fatal(&self) -> bool {
matches!(
self,
TapError::MaxReconnectAttemptsExceeded(_)
| TapError::AuthenticationFailed(_)
| TapError::StreamClosed
)
}
}
impl From<reqwest::Error> for TapError {
fn from(err: reqwest::Error) -> Self {
if err.is_timeout() {
TapError::Timeout
} else if err.is_connect() {
TapError::ConnectionFailed(err.to_string())
} else {
TapError::HttpError(err.to_string())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_classification() {
assert!(TapError::ConnectionFailed("test".into()).is_connection_error());
assert!(TapError::ConnectionClosed.is_connection_error());
assert!(TapError::Timeout.is_connection_error());
assert!(TapError::ParseError("test".into()).is_parse_error());
assert!(
TapError::JsonError(serde_json::from_str::<()>("invalid").unwrap_err())
.is_parse_error()
);
assert!(TapError::MaxReconnectAttemptsExceeded(5).is_fatal());
assert!(TapError::AuthenticationFailed("test".into()).is_fatal());
assert!(TapError::StreamClosed.is_fatal());
assert!(!TapError::ConnectionFailed("test".into()).is_fatal());
assert!(!TapError::ParseError("test".into()).is_fatal());
}
#[test]
fn test_error_display() {
let err = TapError::ConnectionFailed("refused".to_string());
assert!(err.to_string().contains("error-atproto-tap-connection-1"));
assert!(err.to_string().contains("refused"));
let err = TapError::HttpResponseError {
status: 404,
message: "Not Found".to_string(),
};
assert!(err.to_string().contains("404"));
assert!(err.to_string().contains("Not Found"));
}
}