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 let mut response = (status, Json(body)).into_response();
62
63 if let Error::JwtAuth(jwt_err) = &self
65 && let Some(www_auth) = jwt_err.www_authenticate()
66 && let Ok(header_value) = http::HeaderValue::from_str(&www_auth)
67 {
68 response
69 .headers_mut()
70 .insert(header::WWW_AUTHENTICATE, header_value);
71 }
72
73 response
74 }
75}
76
77pub type AppResult<T> = Result<T, Error>;
79
80pub trait ResultExt<T> {
82 fn with_context<F>(self, f: F) -> Result<T, Error>
84 where
85 F: FnOnce() -> String;
86
87 fn table_context(self, table: &str) -> Result<T, Error>;
89
90 fn column_context(self, table: &str, column: &str) -> Result<T, Error>;
92}
93
94impl<T, E> ResultExt<T> for Result<T, E>
95where
96 E: std::fmt::Display,
97{
98 fn with_context<F>(self, f: F) -> Result<T, Error>
99 where
100 F: FnOnce() -> String,
101 {
102 self.map_err(|e| Error::Internal(format!("{}: {}", f(), e)))
103 }
104
105 fn table_context(self, table: &str) -> Result<T, Error> {
106 self.map_err(|e| Error::Internal(format!("[{}] {}", table, e)))
107 }
108
109 fn column_context(self, table: &str, column: &str) -> Result<T, Error> {
110 self.map_err(|e| Error::Internal(format!("[{}.{}] {}", table, column, e)))
111 }
112}
113
114#[macro_export]
133macro_rules! bail {
134 ($err:expr) => {
135 return Err($err.into());
136 };
137}
138
139#[macro_export]
156macro_rules! ensure {
157 ($cond:expr, $err:expr) => {
158 if !$cond {
159 return Err($err.into());
160 }
161 };
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 #[test]
169 fn test_error_response_serialization() {
170 let err = Error::InvalidQueryParam {
171 param: "select".to_string(),
172 message: "Unknown column".to_string(),
173 };
174
175 let response = ErrorResponse::from(&err);
176 let json = serde_json::to_string(&response).unwrap();
177
178 assert!(json.contains("DBRST100"));
179 assert!(json.contains("select"));
180 }
181
182 #[test]
183 fn test_error_response_skips_null_fields() {
184 let err = Error::MissingPayload;
185 let response = ErrorResponse::from(&err);
186 let json = serde_json::to_string(&response).unwrap();
187
188 assert!(!json.contains("details"));
190 assert!(!json.contains("hint"));
191 }
192
193 #[test]
194 fn test_error_response_includes_hint() {
195 let err = Error::TableNotFound {
196 name: "usrs".to_string(),
197 suggestion: Some("users".to_string()),
198 };
199
200 let response = ErrorResponse::from(&err);
201 let json = serde_json::to_string(&response).unwrap();
202
203 assert!(json.contains("hint"));
204 assert!(json.contains("users"));
205 }
206
207 #[test]
208 fn test_www_authenticate_header_propagation() {
209 use crate::auth::error::{JwtDecodeError, JwtError};
210
211 let jwt_err = JwtError::TokenRequired;
213 let err = Error::JwtAuth(jwt_err);
214 let response = err.into_response();
215
216 assert!(response.headers().contains_key(header::WWW_AUTHENTICATE));
217 let www_auth = response.headers().get(header::WWW_AUTHENTICATE).unwrap();
218 assert_eq!(www_auth, "Bearer");
219
220 let jwt_err = JwtError::Decode(JwtDecodeError::BadCrypto);
222 let err = Error::JwtAuth(jwt_err);
223 let response = err.into_response();
224
225 assert!(response.headers().contains_key(header::WWW_AUTHENTICATE));
226 let www_auth = response.headers().get(header::WWW_AUTHENTICATE).unwrap();
227 assert!(www_auth.to_str().unwrap().contains("invalid_token"));
228 }
229
230 #[test]
231 fn test_result_ext_column_context() {
232 let result: Result<i32, String> = Err("test error".to_string());
233 let err = result.column_context("users", "email").unwrap_err();
234
235 match err {
236 Error::Internal(msg) => {
237 assert!(msg.contains("users"));
238 assert!(msg.contains("email"));
239 assert!(msg.contains("test error"));
240 }
241 _ => panic!("Expected Internal error"),
242 }
243 }
244
245 #[test]
246 fn test_bail_macro() {
247 fn test_bail() -> Result<(), Error> {
248 crate::bail!(Error::InvalidQueryParam {
249 param: "test".to_string(),
250 message: "bail test".to_string(),
251 });
252 }
253
254 let err = test_bail().unwrap_err();
255 assert!(matches!(err, Error::InvalidQueryParam { .. }));
256 }
257
258 #[test]
259 fn test_ensure_macro() {
260 fn test_ensure(x: i32) -> Result<(), Error> {
261 crate::ensure!(
262 x >= 0,
263 Error::InvalidQueryParam {
264 param: "x".to_string(),
265 message: "must be non-negative".to_string(),
266 }
267 );
268 Ok(())
269 }
270
271 assert!(test_ensure(5).is_ok());
272 let err = test_ensure(-1).unwrap_err();
273 assert!(matches!(err, Error::InvalidQueryParam { .. }));
274 }
275}