dbrest_core/error/
response.rs1use axum::{
6 Json,
7 response::{IntoResponse, Response},
8};
9use http::header;
10use serde::Serialize;
11
12use super::Error;
13
14#[derive(Debug, Serialize)]
27pub struct ErrorResponse {
28 pub code: &'static str,
30
31 pub message: String,
33
34 #[serde(skip_serializing_if = "Option::is_none")]
36 pub details: Option<String>,
37
38 #[serde(skip_serializing_if = "Option::is_none")]
40 pub hint: Option<String>,
41}
42
43impl From<&Error> for ErrorResponse {
44 fn from(err: &Error) -> Self {
45 let (details, hint) = err.details_and_hint();
46
47 Self {
48 code: err.code(),
49 message: err.to_string(),
50 details,
51 hint,
52 }
53 }
54}
55
56impl IntoResponse for Error {
57 fn into_response(self) -> Response {
58 let status = self.status();
59 let body = ErrorResponse::from(&self);
60
61 if status.is_server_error() {
62 tracing::error!(
63 error_code = body.code,
64 http_status = status.as_u16(),
65 details = body.details.as_deref().unwrap_or(""),
66 "{}",
67 body.message
68 );
69 } else if status.is_client_error() {
70 tracing::warn!(
71 error_code = body.code,
72 http_status = status.as_u16(),
73 "{}",
74 body.message
75 );
76 }
77
78 let mut response = (status, Json(body)).into_response();
79
80 if let Error::JwtAuth(jwt_err) = &self
82 && let Some(www_auth) = jwt_err.www_authenticate()
83 && let Ok(header_value) = http::HeaderValue::from_str(&www_auth)
84 {
85 response
86 .headers_mut()
87 .insert(header::WWW_AUTHENTICATE, header_value);
88 }
89
90 response
91 }
92}
93
94pub type AppResult<T> = Result<T, Error>;
96
97pub trait ResultExt<T> {
99 fn with_context<F>(self, f: F) -> Result<T, Error>
101 where
102 F: FnOnce() -> String;
103
104 fn table_context(self, table: &str) -> Result<T, Error>;
106
107 fn column_context(self, table: &str, column: &str) -> Result<T, Error>;
109}
110
111impl<T, E> ResultExt<T> for Result<T, E>
112where
113 E: std::fmt::Display,
114{
115 fn with_context<F>(self, f: F) -> Result<T, Error>
116 where
117 F: FnOnce() -> String,
118 {
119 self.map_err(|e| Error::Internal(format!("{}: {}", f(), e)))
120 }
121
122 fn table_context(self, table: &str) -> Result<T, Error> {
123 self.map_err(|e| Error::Internal(format!("[{}] {}", table, e)))
124 }
125
126 fn column_context(self, table: &str, column: &str) -> Result<T, Error> {
127 self.map_err(|e| Error::Internal(format!("[{}.{}] {}", table, column, e)))
128 }
129}
130
131#[macro_export]
150macro_rules! bail {
151 ($err:expr) => {
152 return Err($err.into());
153 };
154}
155
156#[macro_export]
173macro_rules! ensure {
174 ($cond:expr, $err:expr) => {
175 if !$cond {
176 return Err($err.into());
177 }
178 };
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184
185 #[test]
186 fn test_error_response_serialization() {
187 let err = Error::InvalidQueryParam {
188 param: "select".to_string(),
189 message: "Unknown column".to_string(),
190 };
191
192 let response = ErrorResponse::from(&err);
193 let json = serde_json::to_string(&response).unwrap();
194
195 assert!(json.contains("DBRST100"));
196 assert!(json.contains("select"));
197 }
198
199 #[test]
200 fn test_error_response_skips_null_fields() {
201 let err = Error::MissingPayload;
202 let response = ErrorResponse::from(&err);
203 let json = serde_json::to_string(&response).unwrap();
204
205 assert!(!json.contains("details"));
207 assert!(!json.contains("hint"));
208 }
209
210 #[test]
211 fn test_error_response_includes_hint() {
212 let err = Error::TableNotFound {
213 name: "usrs".to_string(),
214 suggestion: Some("users".to_string()),
215 };
216
217 let response = ErrorResponse::from(&err);
218 let json = serde_json::to_string(&response).unwrap();
219
220 assert!(json.contains("hint"));
221 assert!(json.contains("users"));
222 }
223
224 #[test]
225 fn test_www_authenticate_header_propagation() {
226 use crate::auth::error::{JwtDecodeError, JwtError};
227
228 let jwt_err = JwtError::TokenRequired;
230 let err = Error::JwtAuth(jwt_err);
231 let response = err.into_response();
232
233 assert!(response.headers().contains_key(header::WWW_AUTHENTICATE));
234 let www_auth = response.headers().get(header::WWW_AUTHENTICATE).unwrap();
235 assert_eq!(www_auth, "Bearer");
236
237 let jwt_err = JwtError::Decode(JwtDecodeError::BadCrypto);
239 let err = Error::JwtAuth(jwt_err);
240 let response = err.into_response();
241
242 assert!(response.headers().contains_key(header::WWW_AUTHENTICATE));
243 let www_auth = response.headers().get(header::WWW_AUTHENTICATE).unwrap();
244 assert!(www_auth.to_str().unwrap().contains("invalid_token"));
245 }
246
247 #[test]
248 fn test_result_ext_column_context() {
249 let result: Result<i32, String> = Err("test error".to_string());
250 let err = result.column_context("users", "email").unwrap_err();
251
252 match err {
253 Error::Internal(msg) => {
254 assert!(msg.contains("users"));
255 assert!(msg.contains("email"));
256 assert!(msg.contains("test error"));
257 }
258 _ => panic!("Expected Internal error"),
259 }
260 }
261
262 #[test]
263 fn test_bail_macro() {
264 fn test_bail() -> Result<(), Error> {
265 crate::bail!(Error::InvalidQueryParam {
266 param: "test".to_string(),
267 message: "bail test".to_string(),
268 });
269 }
270
271 let err = test_bail().unwrap_err();
272 assert!(matches!(err, Error::InvalidQueryParam { .. }));
273 }
274
275 #[test]
276 fn test_ensure_macro() {
277 fn test_ensure(x: i32) -> Result<(), Error> {
278 crate::ensure!(
279 x >= 0,
280 Error::InvalidQueryParam {
281 param: "x".to_string(),
282 message: "must be non-negative".to_string(),
283 }
284 );
285 Ok(())
286 }
287
288 assert!(test_ensure(5).is_ok());
289 let err = test_ensure(-1).unwrap_err();
290 assert!(matches!(err, Error::InvalidQueryParam { .. }));
291 }
292}