1use thiserror::Error;
6
7#[derive(Debug, Error)]
9pub enum TapError {
10 #[error("error-atproto-tap-connection-1 WebSocket connection failed: {0}")]
12 ConnectionFailed(String),
13
14 #[error("error-atproto-tap-connection-2 Connection closed unexpectedly")]
16 ConnectionClosed,
17
18 #[error(
20 "error-atproto-tap-connection-3 Maximum reconnection attempts exceeded after {0} attempts"
21 )]
22 MaxReconnectAttemptsExceeded(u32),
23
24 #[error("error-atproto-tap-auth-1 Authentication failed: {0}")]
26 AuthenticationFailed(String),
27
28 #[error("error-atproto-tap-parse-1 Failed to parse message: {0}")]
30 ParseError(String),
31
32 #[error("error-atproto-tap-ack-1 Failed to send acknowledgment: {0}")]
34 AckFailed(String),
35
36 #[error("error-atproto-tap-http-1 HTTP request failed: {0}")]
38 HttpError(String),
39
40 #[error("error-atproto-tap-http-2 HTTP error response: {status} - {message}")]
42 HttpResponseError {
43 status: u16,
45 message: String,
47 },
48
49 #[error("error-atproto-tap-url-1 Invalid URL: {0}")]
51 InvalidUrl(String),
52
53 #[error("error-atproto-tap-io-1 I/O error: {0}")]
55 IoError(#[from] std::io::Error),
56
57 #[error("error-atproto-tap-json-1 JSON error: {0}")]
59 JsonError(#[from] serde_json::Error),
60
61 #[error("error-atproto-tap-stream-1 Stream is closed")]
63 StreamClosed,
64
65 #[error("error-atproto-tap-timeout-1 Operation timed out")]
67 Timeout,
68}
69
70impl TapError {
71 pub fn is_connection_error(&self) -> bool {
73 matches!(
74 self,
75 TapError::ConnectionFailed(_)
76 | TapError::ConnectionClosed
77 | TapError::IoError(_)
78 | TapError::Timeout
79 )
80 }
81
82 pub fn is_parse_error(&self) -> bool {
84 matches!(self, TapError::ParseError(_) | TapError::JsonError(_))
85 }
86
87 pub fn is_fatal(&self) -> bool {
89 matches!(
90 self,
91 TapError::MaxReconnectAttemptsExceeded(_)
92 | TapError::AuthenticationFailed(_)
93 | TapError::StreamClosed
94 )
95 }
96}
97
98impl From<reqwest::Error> for TapError {
99 fn from(err: reqwest::Error) -> Self {
100 if err.is_timeout() {
101 TapError::Timeout
102 } else if err.is_connect() {
103 TapError::ConnectionFailed(err.to_string())
104 } else {
105 TapError::HttpError(err.to_string())
106 }
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113
114 #[test]
115 fn test_error_classification() {
116 assert!(TapError::ConnectionFailed("test".into()).is_connection_error());
117 assert!(TapError::ConnectionClosed.is_connection_error());
118 assert!(TapError::Timeout.is_connection_error());
119
120 assert!(TapError::ParseError("test".into()).is_parse_error());
121 assert!(
122 TapError::JsonError(serde_json::from_str::<()>("invalid").unwrap_err())
123 .is_parse_error()
124 );
125
126 assert!(TapError::MaxReconnectAttemptsExceeded(5).is_fatal());
127 assert!(TapError::AuthenticationFailed("test".into()).is_fatal());
128 assert!(TapError::StreamClosed.is_fatal());
129
130 assert!(!TapError::ConnectionFailed("test".into()).is_fatal());
132 assert!(!TapError::ParseError("test".into()).is_fatal());
133 }
134
135 #[test]
136 fn test_error_display() {
137 let err = TapError::ConnectionFailed("refused".to_string());
138 assert!(err.to_string().contains("error-atproto-tap-connection-1"));
139 assert!(err.to_string().contains("refused"));
140
141 let err = TapError::HttpResponseError {
142 status: 404,
143 message: "Not Found".to_string(),
144 };
145 assert!(err.to_string().contains("404"));
146 assert!(err.to_string().contains("Not Found"));
147 }
148}