Skip to main content

angzarr_client/
error.rs

1//! Error types for the Angzarr client library.
2
3use tonic::{Code, Status};
4
5/// Error message constants for testing and consistency.
6pub mod errmsg {
7    /// Connection failure prefix.
8    pub const CONNECTION_FAILED: &str = "connection failed: ";
9    /// Transport error prefix.
10    pub const TRANSPORT_ERROR: &str = "transport error: ";
11    /// gRPC error prefix.
12    pub const GRPC_ERROR: &str = "grpc error: ";
13    /// Invalid argument prefix.
14    pub const INVALID_ARGUMENT: &str = "invalid argument: ";
15    /// Invalid timestamp prefix.
16    pub const INVALID_TIMESTAMP: &str = "invalid timestamp: ";
17}
18
19/// Result type for client operations.
20pub type Result<T> = std::result::Result<T, ClientError>;
21
22/// Errors that can occur during client operations.
23#[derive(Debug, thiserror::Error)]
24pub enum ClientError {
25    /// Failed to establish connection to the server.
26    #[error("{}{msg}", errmsg::CONNECTION_FAILED)]
27    Connection { msg: String },
28
29    /// Transport-level error from tonic.
30    #[error("{}{source}", errmsg::TRANSPORT_ERROR)]
31    Transport {
32        #[from]
33        source: tonic::transport::Error,
34    },
35
36    /// gRPC error from the server.
37    #[error("{}{status}", errmsg::GRPC_ERROR)]
38    Grpc { status: Box<Status> },
39
40    /// Invalid argument provided by caller.
41    #[error("{}{msg}", errmsg::INVALID_ARGUMENT)]
42    InvalidArgument { msg: String },
43
44    /// Failed to parse timestamp.
45    #[error("{}{msg}", errmsg::INVALID_TIMESTAMP)]
46    InvalidTimestamp { msg: String },
47}
48
49impl From<Status> for ClientError {
50    fn from(status: Status) -> Self {
51        ClientError::Grpc {
52            status: Box::new(status),
53        }
54    }
55}
56
57impl ClientError {
58    /// Returns the error message.
59    pub fn message(&self) -> String {
60        match self {
61            ClientError::Connection { msg } => msg.clone(),
62            ClientError::Transport { source } => source.to_string(),
63            ClientError::Grpc { status } => status.message().to_string(),
64            ClientError::InvalidArgument { msg } => msg.clone(),
65            ClientError::InvalidTimestamp { msg } => msg.clone(),
66        }
67    }
68
69    /// Returns the gRPC status code if this is a gRPC error.
70    pub fn code(&self) -> Option<Code> {
71        match self {
72            ClientError::Grpc { status } => Some(status.code()),
73            _ => None,
74        }
75    }
76
77    /// Returns the underlying gRPC Status if this is a gRPC error.
78    pub fn status(&self) -> Option<&Status> {
79        match self {
80            ClientError::Grpc { status } => Some(status),
81            _ => None,
82        }
83    }
84
85    /// Returns true if this is a "not found" error.
86    pub fn is_not_found(&self) -> bool {
87        matches!(self.code(), Some(Code::NotFound))
88    }
89
90    /// Returns true if this is a "precondition failed" error.
91    pub fn is_precondition_failed(&self) -> bool {
92        matches!(self.code(), Some(Code::FailedPrecondition))
93    }
94
95    /// Returns true if this is an "invalid argument" error.
96    pub fn is_invalid_argument(&self) -> bool {
97        matches!(self.code(), Some(Code::InvalidArgument))
98            || matches!(self, ClientError::InvalidArgument { .. })
99    }
100
101    /// Returns true if this is a connection or transport error.
102    pub fn is_connection_error(&self) -> bool {
103        matches!(
104            self,
105            ClientError::Connection { .. } | ClientError::Transport { .. }
106        )
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn test_connection_error_display() {
116        let err = ClientError::Connection {
117            msg: "refused".to_string(),
118        };
119        assert_eq!(err.to_string(), "connection failed: refused");
120    }
121
122    #[test]
123    fn test_connection_error_message() {
124        let err = ClientError::Connection {
125            msg: "timeout".to_string(),
126        };
127        assert_eq!(err.message(), "timeout");
128    }
129
130    #[test]
131    fn test_invalid_argument_error_display() {
132        let err = ClientError::InvalidArgument {
133            msg: "missing field".to_string(),
134        };
135        assert_eq!(err.to_string(), "invalid argument: missing field");
136    }
137
138    #[test]
139    fn test_invalid_argument_error_message() {
140        let err = ClientError::InvalidArgument {
141            msg: "bad value".to_string(),
142        };
143        assert_eq!(err.message(), "bad value");
144    }
145
146    #[test]
147    fn test_invalid_timestamp_error_display() {
148        let err = ClientError::InvalidTimestamp {
149            msg: "bad format".to_string(),
150        };
151        assert_eq!(err.to_string(), "invalid timestamp: bad format");
152    }
153
154    #[test]
155    fn test_invalid_timestamp_error_message() {
156        let err = ClientError::InvalidTimestamp {
157            msg: "parse failed".to_string(),
158        };
159        assert_eq!(err.message(), "parse failed");
160    }
161
162    #[test]
163    fn test_grpc_error_from_status() {
164        let status = Status::not_found("resource not found");
165        let err: ClientError = status.into();
166        assert!(matches!(err, ClientError::Grpc { .. }));
167    }
168
169    #[test]
170    fn test_grpc_error_message() {
171        let status = Status::internal("server error");
172        let err = ClientError::Grpc {
173            status: Box::new(status),
174        };
175        assert_eq!(err.message(), "server error");
176    }
177
178    #[test]
179    fn test_grpc_error_code() {
180        let status = Status::not_found("missing");
181        let err = ClientError::Grpc {
182            status: Box::new(status),
183        };
184        assert_eq!(err.code(), Some(Code::NotFound));
185    }
186
187    #[test]
188    fn test_grpc_error_status() {
189        let status = Status::permission_denied("access denied");
190        let err = ClientError::Grpc {
191            status: Box::new(status),
192        };
193        let s = err.status().unwrap();
194        assert_eq!(s.code(), Code::PermissionDenied);
195        assert_eq!(s.message(), "access denied");
196    }
197
198    #[test]
199    fn test_non_grpc_error_code_is_none() {
200        let err = ClientError::Connection {
201            msg: "refused".to_string(),
202        };
203        assert_eq!(err.code(), None);
204    }
205
206    #[test]
207    fn test_non_grpc_error_status_is_none() {
208        let err = ClientError::InvalidArgument {
209            msg: "bad".to_string(),
210        };
211        assert!(err.status().is_none());
212    }
213
214    #[test]
215    fn test_is_not_found_true() {
216        let status = Status::not_found("missing");
217        let err = ClientError::Grpc {
218            status: Box::new(status),
219        };
220        assert!(err.is_not_found());
221    }
222
223    #[test]
224    fn test_is_not_found_false_other_code() {
225        let status = Status::internal("error");
226        let err = ClientError::Grpc {
227            status: Box::new(status),
228        };
229        assert!(!err.is_not_found());
230    }
231
232    #[test]
233    fn test_is_not_found_false_non_grpc() {
234        let err = ClientError::Connection {
235            msg: "refused".to_string(),
236        };
237        assert!(!err.is_not_found());
238    }
239
240    #[test]
241    fn test_is_precondition_failed_true() {
242        let status = Status::failed_precondition("conflict");
243        let err = ClientError::Grpc {
244            status: Box::new(status),
245        };
246        assert!(err.is_precondition_failed());
247    }
248
249    #[test]
250    fn test_is_precondition_failed_false() {
251        let status = Status::not_found("missing");
252        let err = ClientError::Grpc {
253            status: Box::new(status),
254        };
255        assert!(!err.is_precondition_failed());
256    }
257
258    #[test]
259    fn test_is_invalid_argument_grpc_true() {
260        let status = Status::invalid_argument("bad input");
261        let err = ClientError::Grpc {
262            status: Box::new(status),
263        };
264        assert!(err.is_invalid_argument());
265    }
266
267    #[test]
268    fn test_is_invalid_argument_client_error_true() {
269        let err = ClientError::InvalidArgument {
270            msg: "missing".to_string(),
271        };
272        assert!(err.is_invalid_argument());
273    }
274
275    #[test]
276    fn test_is_invalid_argument_false() {
277        let err = ClientError::Connection {
278            msg: "refused".to_string(),
279        };
280        assert!(!err.is_invalid_argument());
281    }
282
283    #[test]
284    fn test_is_connection_error_connection_true() {
285        let err = ClientError::Connection {
286            msg: "refused".to_string(),
287        };
288        assert!(err.is_connection_error());
289    }
290
291    #[test]
292    fn test_is_connection_error_grpc_false() {
293        let status = Status::internal("error");
294        let err = ClientError::Grpc {
295            status: Box::new(status),
296        };
297        assert!(!err.is_connection_error());
298    }
299
300    #[test]
301    fn test_is_connection_error_invalid_argument_false() {
302        let err = ClientError::InvalidArgument {
303            msg: "bad".to_string(),
304        };
305        assert!(!err.is_connection_error());
306    }
307}