1mod codes;
3
4use codes::ErrorCodeExt;
5pub use ecat_protos::errors::ErrorCode;
6use std::collections::HashMap;
7
8#[derive(Debug, thiserror::Error)]
9pub struct Error {
10 pub code: ErrorCode,
11 pub reason: String,
12 pub message: String,
13 #[source]
14 pub cause: Option<Box<dyn std::error::Error + Send + Sync>>,
15 pub metadata: HashMap<String, String>,
16}
17
18impl std::fmt::Display for Error {
19 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20 write!(f, "[{:?}] {}: {}", self.code, self.reason, self.message)
21 }
22}
23
24impl Error {
25 pub fn new(code: ErrorCode, reason: impl Into<String>, message: impl Into<String>) -> Self {
26 Self {
27 code,
28 reason: reason.into(),
29 message: message.into(),
30 cause: None,
31 metadata: HashMap::new(),
32 }
33 }
34
35 pub fn message(&self) -> &str {
36 &self.message
37 }
38
39 pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
40 self.metadata.insert(key.into(), value.into());
41 self
42 }
43
44 pub fn from_status(status: tonic::Status) -> Self {
45 let code = match status.code() {
46 tonic::Code::Ok => ErrorCode::Ok,
47 tonic::Code::NotFound => ErrorCode::NotFound,
48 tonic::Code::InvalidArgument => ErrorCode::InvalidArgument,
49 tonic::Code::PermissionDenied => ErrorCode::PermissionDenied,
50 tonic::Code::Unauthenticated => ErrorCode::Unauthenticated,
51 tonic::Code::ResourceExhausted => ErrorCode::ResourceExhausted,
52 tonic::Code::AlreadyExists => ErrorCode::AlreadyExists,
53 tonic::Code::Unavailable => ErrorCode::Unavailable,
54 tonic::Code::DeadlineExceeded => ErrorCode::DeadlineExceeded,
55 tonic::Code::Unknown => ErrorCode::Unknown,
56 _ => ErrorCode::Internal,
57 };
58 Self::new(code, "grpc_error", status.message().to_string())
59 }
60
61 pub fn to_http_status(&self) -> http::StatusCode {
62 self.code.http_status()
63 }
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69 use http::StatusCode;
70
71 #[test]
72 fn test_error_code_http_mapping() {
73 assert_eq!(ErrorCode::Ok.http_status(), StatusCode::OK);
74 assert_eq!(ErrorCode::NotFound.http_status(), StatusCode::NOT_FOUND);
75 assert_eq!(
76 ErrorCode::InvalidArgument.http_status(),
77 StatusCode::BAD_REQUEST
78 );
79 assert_eq!(
80 ErrorCode::PermissionDenied.http_status(),
81 StatusCode::FORBIDDEN
82 );
83 assert_eq!(
84 ErrorCode::Unauthenticated.http_status(),
85 StatusCode::UNAUTHORIZED
86 );
87 assert_eq!(
88 ErrorCode::Internal.http_status(),
89 StatusCode::INTERNAL_SERVER_ERROR
90 );
91 assert_eq!(
92 ErrorCode::Unavailable.http_status(),
93 StatusCode::SERVICE_UNAVAILABLE
94 );
95 assert_eq!(
96 ErrorCode::DeadlineExceeded.http_status(),
97 StatusCode::GATEWAY_TIMEOUT
98 );
99 }
100
101 #[test]
102 fn test_from_status_maps_codes() {
103 let err = Error::from_status(tonic::Status::not_found("missing"));
104 assert_eq!(err.code, ErrorCode::NotFound);
105
106 let err = Error::from_status(tonic::Status::permission_denied("nope"));
107 assert_eq!(err.code, ErrorCode::PermissionDenied);
108
109 let err = Error::from_status(tonic::Status::unauthenticated("bad token"));
110 assert_eq!(err.code, ErrorCode::Unauthenticated);
111 }
112
113 #[test]
114 fn test_with_metadata_accumulates() {
115 let err = Error::new(ErrorCode::Ok, "test", "msg")
116 .with_metadata("key1", "val1")
117 .with_metadata("key2", "val2");
118 assert_eq!(err.metadata.get("key1").unwrap(), "val1");
119 assert_eq!(err.metadata.get("key2").unwrap(), "val2");
120 }
121
122 #[test]
123 fn test_display_format() {
124 let err = Error::new(ErrorCode::NotFound, "user_not_found", "user 42 not found");
125 let s = err.to_string();
126 assert!(s.contains("NotFound"));
127 assert!(s.contains("user_not_found"));
128 assert!(s.contains("user 42 not found"));
129 }
130
131 #[test]
132 fn test_remaining_http_mappings() {
133 assert_eq!(ErrorCode::AlreadyExists.http_status(), StatusCode::CONFLICT);
134 assert_eq!(
135 ErrorCode::ResourceExhausted.http_status(),
136 StatusCode::TOO_MANY_REQUESTS
137 );
138 assert_eq!(
139 ErrorCode::Unknown.http_status(),
140 StatusCode::INTERNAL_SERVER_ERROR
141 );
142 assert_eq!(
143 ErrorCode::Internal.http_status(),
144 StatusCode::INTERNAL_SERVER_ERROR
145 );
146 }
147
148 #[test]
149 fn test_from_status_remaining_codes() {
150 assert_eq!(
151 Error::from_status(tonic::Status::already_exists("dup")).code,
152 ErrorCode::AlreadyExists
153 );
154 assert_eq!(
155 Error::from_status(tonic::Status::resource_exhausted("limit")).code,
156 ErrorCode::ResourceExhausted
157 );
158 assert_eq!(
159 Error::from_status(tonic::Status::unavailable("down")).code,
160 ErrorCode::Unavailable
161 );
162 assert_eq!(
163 Error::from_status(tonic::Status::deadline_exceeded("slow")).code,
164 ErrorCode::DeadlineExceeded
165 );
166 assert_eq!(
167 Error::from_status(tonic::Status::unknown("x")).code,
168 ErrorCode::Unknown
169 );
170 assert_eq!(
171 Error::from_status(tonic::Status::internal("boom")).code,
172 ErrorCode::Internal
173 );
174 }
175
176 #[test]
177 fn test_from_status_unmapped_code_becomes_internal() {
178 let err = Error::from_status(tonic::Status::failed_precondition("nope"));
179 assert_eq!(err.code, ErrorCode::Internal);
180 let err = Error::from_status(tonic::Status::new(tonic::Code::Ok, ""));
181 assert_eq!(err.code, ErrorCode::Ok);
182 }
183
184 #[test]
185 fn test_to_http_status() {
186 let err = Error::new(ErrorCode::Unavailable, "x", "y");
187 assert_eq!(err.to_http_status(), StatusCode::SERVICE_UNAVAILABLE);
188 }
189
190 #[test]
191 fn test_cause_is_exposed_via_source() {
192 use std::error::Error as _;
193 let inner = std::io::Error::other("io boom");
194 let err = Error {
195 cause: Some(Box::new(inner)),
196 ..Error::new(ErrorCode::Internal, "io", "failed")
197 };
198 assert!(err.source().is_some());
199 let no_cause = Error::new(ErrorCode::Ok, "x", "y");
200 assert!(no_cause.source().is_none());
201 }
202}