Skip to main content

dbrest_core/error/
response.rs

1//! Error response types for HTTP responses
2//!
3//! JSON error response formatting.
4
5use axum::{
6    Json,
7    response::{IntoResponse, Response},
8};
9use http::header;
10use serde::Serialize;
11
12use super::Error;
13
14/// JSON error response format
15///
16/// # Example Response
17///
18/// ```json
19/// {
20///   "code": "DBRST200",
21///   "message": "Table not found: users",
22///   "details": null,
23///   "hint": "Did you mean 'user'?"
24/// }
25/// ```
26#[derive(Debug, Serialize)]
27pub struct ErrorResponse {
28    /// DBRST error code (e.g., "DBRST200")
29    pub code: &'static str,
30
31    /// Human-readable error message
32    pub message: String,
33
34    /// Additional details about the error (optional)
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub details: Option<String>,
37
38    /// Hint for resolution (optional)
39    #[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        // Propagate WWW-Authenticate header for JWT errors
81        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
94/// Result type alias for handlers returning potential errors
95pub type AppResult<T> = Result<T, Error>;
96
97/// Extension trait for adding context to Results
98pub trait ResultExt<T> {
99    /// Add context message on error
100    fn with_context<F>(self, f: F) -> Result<T, Error>
101    where
102        F: FnOnce() -> String;
103
104    /// Add table context to error message
105    fn table_context(self, table: &str) -> Result<T, Error>;
106
107    /// Add table and column context to error message
108    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/// Early return with an error.
132///
133/// # Example
134///
135/// ```rust
136/// use dbrest::bail;
137/// use dbrest::Error;
138///
139/// fn validate(x: i32) -> Result<(), Error> {
140///     if x < 0 {
141///         bail!(Error::InvalidQueryParam {
142///             param: "x".to_string(),
143///             message: "must be non-negative".to_string(),
144///         });
145///     }
146///     Ok(())
147/// }
148/// ```
149#[macro_export]
150macro_rules! bail {
151    ($err:expr) => {
152        return Err($err.into());
153    };
154}
155
156/// Ensure a condition is true, otherwise return an error.
157///
158/// # Example
159///
160/// ```rust
161/// use dbrest::ensure;
162/// use dbrest::Error;
163///
164/// fn validate(x: i32) -> Result<(), Error> {
165///     ensure!(x >= 0, Error::InvalidQueryParam {
166///         param: "x".to_string(),
167///         message: "must be non-negative".to_string(),
168///     });
169///     Ok(())
170/// }
171/// ```
172#[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        // details and hint should be omitted when None
206        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        // Test that JwtAuth errors include WWW-Authenticate header
229        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        // Test decode error
238        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}