1use rskit_errors::{AppError, ErrorCode, ProblemDetail};
2use tonic::Status;
3
4pub fn status_to_app_error(status: Status) -> AppError {
9 if !status.details().is_empty()
10 && let Ok(problem) = serde_json::from_slice::<ProblemDetail>(status.details())
11 {
12 return AppError::new(problem.code, problem.detail)
13 .retryable(problem.retryable)
14 .with_details(problem.details);
15 }
16
17 AppError::new(
18 grpc_code_to_error_code(status.code()),
19 status.message().to_string(),
20 )
21}
22
23pub fn app_error_to_status(err: &AppError) -> Status {
27 let problem = ProblemDetail::from(err);
28 if let Ok(json_bytes) = serde_json::to_vec(&problem) {
29 Status::with_details(
30 error_code_to_grpc_code(err.code()),
31 err.message().to_string(),
32 json_bytes.into(),
33 )
34 } else {
35 Status::new(
36 error_code_to_grpc_code(err.code()),
37 err.message().to_string(),
38 )
39 }
40}
41
42#[must_use]
44pub fn error_code_to_grpc_code(code: ErrorCode) -> tonic::Code {
45 match code {
46 ErrorCode::ServiceUnavailable | ErrorCode::ConnectionFailed => tonic::Code::Unavailable,
47 ErrorCode::Timeout => tonic::Code::DeadlineExceeded,
48 ErrorCode::RateLimited => tonic::Code::ResourceExhausted,
49 ErrorCode::NotFound => tonic::Code::NotFound,
50 ErrorCode::AlreadyExists => tonic::Code::AlreadyExists,
51 ErrorCode::Conflict => tonic::Code::Aborted,
52 ErrorCode::InvalidInput | ErrorCode::MissingField | ErrorCode::InvalidFormat => {
53 tonic::Code::InvalidArgument
54 }
55 ErrorCode::Unauthorized | ErrorCode::TokenExpired | ErrorCode::InvalidToken => {
56 tonic::Code::Unauthenticated
57 }
58 ErrorCode::Forbidden => tonic::Code::PermissionDenied,
59 ErrorCode::Internal | ErrorCode::DatabaseError | ErrorCode::ExternalService => {
60 tonic::Code::Internal
61 }
62 ErrorCode::Cancelled => tonic::Code::Cancelled,
63 _ => tonic::Code::Unknown,
64 }
65}
66
67#[must_use]
69pub fn grpc_code_to_error_code(code: tonic::Code) -> ErrorCode {
70 match code {
71 tonic::Code::Unavailable => ErrorCode::ServiceUnavailable,
72 tonic::Code::DeadlineExceeded => ErrorCode::Timeout,
73 tonic::Code::ResourceExhausted => ErrorCode::RateLimited,
74 tonic::Code::NotFound => ErrorCode::NotFound,
75 tonic::Code::AlreadyExists => ErrorCode::AlreadyExists,
76 tonic::Code::Aborted | tonic::Code::FailedPrecondition => ErrorCode::Conflict,
77 tonic::Code::InvalidArgument | tonic::Code::OutOfRange => ErrorCode::InvalidInput,
78 tonic::Code::Unauthenticated => ErrorCode::Unauthorized,
79 tonic::Code::PermissionDenied => ErrorCode::Forbidden,
80 tonic::Code::Cancelled => ErrorCode::Cancelled,
81 tonic::Code::Internal | tonic::Code::Unknown | tonic::Code::Unimplemented => {
82 ErrorCode::Internal
83 }
84 tonic::Code::DataLoss => ErrorCode::ExternalService,
85 _ => ErrorCode::ExternalService,
86 }
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92
93 #[test]
94 fn error_code_to_grpc_code_covers_known_error_codes() {
95 let cases = [
96 (ErrorCode::ServiceUnavailable, tonic::Code::Unavailable),
97 (ErrorCode::ConnectionFailed, tonic::Code::Unavailable),
98 (ErrorCode::Timeout, tonic::Code::DeadlineExceeded),
99 (ErrorCode::RateLimited, tonic::Code::ResourceExhausted),
100 (ErrorCode::NotFound, tonic::Code::NotFound),
101 (ErrorCode::AlreadyExists, tonic::Code::AlreadyExists),
102 (ErrorCode::Conflict, tonic::Code::Aborted),
103 (ErrorCode::InvalidInput, tonic::Code::InvalidArgument),
104 (ErrorCode::MissingField, tonic::Code::InvalidArgument),
105 (ErrorCode::InvalidFormat, tonic::Code::InvalidArgument),
106 (ErrorCode::Unauthorized, tonic::Code::Unauthenticated),
107 (ErrorCode::Forbidden, tonic::Code::PermissionDenied),
108 (ErrorCode::TokenExpired, tonic::Code::Unauthenticated),
109 (ErrorCode::InvalidToken, tonic::Code::Unauthenticated),
110 (ErrorCode::Internal, tonic::Code::Internal),
111 (ErrorCode::DatabaseError, tonic::Code::Internal),
112 (ErrorCode::ExternalService, tonic::Code::Internal),
113 (ErrorCode::Cancelled, tonic::Code::Cancelled),
114 ];
115
116 for (error_code, grpc_code) in cases {
117 assert_eq!(error_code_to_grpc_code(error_code), grpc_code);
118 }
119 }
120
121 #[test]
122 fn grpc_code_to_error_code_covers_all_tonic_codes() {
123 let cases = [
124 (tonic::Code::Ok, ErrorCode::ExternalService),
125 (tonic::Code::Cancelled, ErrorCode::Cancelled),
126 (tonic::Code::Unknown, ErrorCode::Internal),
127 (tonic::Code::InvalidArgument, ErrorCode::InvalidInput),
128 (tonic::Code::DeadlineExceeded, ErrorCode::Timeout),
129 (tonic::Code::NotFound, ErrorCode::NotFound),
130 (tonic::Code::AlreadyExists, ErrorCode::AlreadyExists),
131 (tonic::Code::PermissionDenied, ErrorCode::Forbidden),
132 (tonic::Code::ResourceExhausted, ErrorCode::RateLimited),
133 (tonic::Code::FailedPrecondition, ErrorCode::Conflict),
134 (tonic::Code::Aborted, ErrorCode::Conflict),
135 (tonic::Code::OutOfRange, ErrorCode::InvalidInput),
136 (tonic::Code::Unimplemented, ErrorCode::Internal),
137 (tonic::Code::Internal, ErrorCode::Internal),
138 (tonic::Code::Unavailable, ErrorCode::ServiceUnavailable),
139 (tonic::Code::DataLoss, ErrorCode::ExternalService),
140 (tonic::Code::Unauthenticated, ErrorCode::Unauthorized),
141 ];
142
143 for (grpc_code, error_code) in cases {
144 assert_eq!(grpc_code_to_error_code(grpc_code), error_code);
145 }
146 }
147
148 #[test]
149 fn test_status_to_error_not_found() {
150 let status = tonic::Status::not_found("user not found");
151 let err = status_to_app_error(status);
152 assert_eq!(err.code(), ErrorCode::NotFound);
153 }
154
155 #[test]
156 fn test_status_to_error_invalid_argument() {
157 let status = tonic::Status::invalid_argument("invalid request");
158 let err = status_to_app_error(status);
159 assert_eq!(err.code(), ErrorCode::InvalidInput);
160 }
161
162 #[test]
163 fn test_status_to_error_unavailable() {
164 let status = tonic::Status::unavailable("service down");
165 let err = status_to_app_error(status);
166 assert_eq!(err.code(), ErrorCode::ServiceUnavailable);
167 }
168
169 #[test]
170 fn test_status_to_error_unauthenticated() {
171 let status = tonic::Status::unauthenticated("invalid token");
172 let err = status_to_app_error(status);
173 assert_eq!(err.code(), ErrorCode::Unauthorized);
174 }
175
176 #[test]
177 fn test_app_error_to_status_not_found() {
178 let err = AppError::new(ErrorCode::NotFound, "user not found");
179 let status = app_error_to_status(&err);
180 assert_eq!(status.code(), tonic::Code::NotFound);
181 }
182
183 #[test]
184 fn test_app_error_to_status_invalid_input() {
185 let err = AppError::new(ErrorCode::InvalidInput, "bad request");
186 let status = app_error_to_status(&err);
187 assert_eq!(status.code(), tonic::Code::InvalidArgument);
188 }
189
190 #[test]
191 fn test_app_error_to_status_unauthorized() {
192 let err = AppError::new(ErrorCode::Unauthorized, "missing token");
193 let status = app_error_to_status(&err);
194 assert_eq!(status.code(), tonic::Code::Unauthenticated);
195 }
196
197 #[test]
198 fn test_status_to_error_cancelled_uses_canonical_errors_mapping() {
199 let err = status_to_app_error(tonic::Status::cancelled("client cancelled"));
200 assert_eq!(err.code(), ErrorCode::Cancelled);
201 }
202
203 #[test]
204 fn app_error_to_status_preserves_problem_details() {
205 let err = AppError::new(ErrorCode::NotFound, "user 42 not found").with_detail("id", "42");
206
207 let status = app_error_to_status(&err);
208 let recovered = status_to_app_error(status);
209
210 assert_eq!(recovered.code(), ErrorCode::NotFound);
211 assert_eq!(recovered.message(), "user 42 not found");
212 assert_eq!(
213 recovered
214 .details()
215 .get("id")
216 .and_then(|value| value.as_str()),
217 Some("42")
218 );
219 }
220}