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        let mut response = (status, Json(body)).into_response();
62
63        // Propagate WWW-Authenticate header for JWT errors
64        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
77/// Result type alias for handlers returning potential errors
78pub type AppResult<T> = Result<T, Error>;
79
80/// Extension trait for adding context to Results
81pub trait ResultExt<T> {
82    /// Add context message on error
83    fn with_context<F>(self, f: F) -> Result<T, Error>
84    where
85        F: FnOnce() -> String;
86
87    /// Add table context to error message
88    fn table_context(self, table: &str) -> Result<T, Error>;
89
90    /// Add table and column context to error message
91    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/// Early return with an error.
115///
116/// # Example
117///
118/// ```rust
119/// use dbrest::bail;
120/// use dbrest::Error;
121///
122/// fn validate(x: i32) -> Result<(), Error> {
123///     if x < 0 {
124///         bail!(Error::InvalidQueryParam {
125///             param: "x".to_string(),
126///             message: "must be non-negative".to_string(),
127///         });
128///     }
129///     Ok(())
130/// }
131/// ```
132#[macro_export]
133macro_rules! bail {
134    ($err:expr) => {
135        return Err($err.into());
136    };
137}
138
139/// Ensure a condition is true, otherwise return an error.
140///
141/// # Example
142///
143/// ```rust
144/// use dbrest::ensure;
145/// use dbrest::Error;
146///
147/// fn validate(x: i32) -> Result<(), Error> {
148///     ensure!(x >= 0, Error::InvalidQueryParam {
149///         param: "x".to_string(),
150///         message: "must be non-negative".to_string(),
151///     });
152///     Ok(())
153/// }
154/// ```
155#[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        // details and hint should be omitted when None
189        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        // Test that JwtAuth errors include WWW-Authenticate header
212        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        // Test decode error
221        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}