1use tonic::{Code, Status};
4
5pub mod errmsg {
7 pub const CONNECTION_FAILED: &str = "connection failed: ";
9 pub const TRANSPORT_ERROR: &str = "transport error: ";
11 pub const GRPC_ERROR: &str = "grpc error: ";
13 pub const INVALID_ARGUMENT: &str = "invalid argument: ";
15 pub const INVALID_TIMESTAMP: &str = "invalid timestamp: ";
17}
18
19pub type Result<T> = std::result::Result<T, ClientError>;
21
22#[derive(Debug, thiserror::Error)]
24pub enum ClientError {
25 #[error("{}{msg}", errmsg::CONNECTION_FAILED)]
27 Connection { msg: String },
28
29 #[error("{}{source}", errmsg::TRANSPORT_ERROR)]
31 Transport {
32 #[from]
33 source: tonic::transport::Error,
34 },
35
36 #[error("{}{status}", errmsg::GRPC_ERROR)]
38 Grpc { status: Box<Status> },
39
40 #[error("{}{msg}", errmsg::INVALID_ARGUMENT)]
42 InvalidArgument { msg: String },
43
44 #[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 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 pub fn code(&self) -> Option<Code> {
71 match self {
72 ClientError::Grpc { status } => Some(status.code()),
73 _ => None,
74 }
75 }
76
77 pub fn status(&self) -> Option<&Status> {
79 match self {
80 ClientError::Grpc { status } => Some(status),
81 _ => None,
82 }
83 }
84
85 pub fn is_not_found(&self) -> bool {
87 matches!(self.code(), Some(Code::NotFound))
88 }
89
90 pub fn is_precondition_failed(&self) -> bool {
92 matches!(self.code(), Some(Code::FailedPrecondition))
93 }
94
95 pub fn is_invalid_argument(&self) -> bool {
97 matches!(self.code(), Some(Code::InvalidArgument))
98 || matches!(self, ClientError::InvalidArgument { .. })
99 }
100
101 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}