axum_webtools/http/
response.rs

1use axum::http::StatusCode;
2use axum::response::IntoResponse;
3use axum::Json;
4use derive_more::with_trait::Display;
5use serde::{Deserialize, Serialize};
6use serde_json::json;
7use std::fmt;
8use std::fmt::{Debug, Formatter};
9use thiserror::Error;
10use validator::{ValidationError, ValidationErrors};
11
12#[derive(Debug)]
13pub struct HttpErrorDetails {
14    pub message: String,
15    pub status_code: StatusCode,
16    pub headers: Vec<(String, String)>,
17}
18
19#[derive(Debug, Error, Display)]
20pub enum HttpError {
21    #[error(transparent)]
22    SqlxError(#[from] sqlx::Error),
23    WithDetails(HttpErrorDetails),
24    ValidationError(ValidationErrorResponse),
25}
26
27#[derive(Debug, Serialize, Deserialize, Clone)]
28pub struct ValidationErrorResponse {
29    pub validation_errors: Vec<ValidationError>,
30}
31
32impl ValidationErrorResponse {
33    pub fn from(validation_errors: ValidationErrors) -> ValidationErrorResponse {
34        let validation_errors = validation_errors
35            .field_errors()
36            .into_values()
37            .flat_map(|v| v.clone())
38            .collect();
39
40        ValidationErrorResponse { validation_errors }
41    }
42}
43
44impl Display for ValidationErrorResponse {
45    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
46        write!(f, "{:?}", self.validation_errors)
47    }
48}
49
50impl From<ValidationErrors> for HttpError {
51    fn from(validation_errors: ValidationErrors) -> Self {
52        HttpError::ValidationError(ValidationErrorResponse::from(validation_errors))
53    }
54}
55
56impl Display for HttpErrorDetails {
57    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
58        let headers = self
59            .headers
60            .iter()
61            .map(|(k, v)| format!("{}: {}", k, v))
62            .collect::<Vec<String>>()
63            .join(", ");
64        write!(
65            f,
66            "{:?}: {:?} ({:?})",
67            self.status_code, self.message, headers
68        )
69    }
70}
71
72impl IntoResponse for HttpError {
73    fn into_response(self) -> axum::response::Response {
74        let (status_code, body) = match self {
75            Self::SqlxError(sqlx_error) => {
76                let message = format!("{:?}", sqlx_error);
77                (
78                    StatusCode::INTERNAL_SERVER_ERROR,
79                    json!({
80                        "message": message
81                    }),
82                )
83            }
84            Self::WithDetails(details) => (
85                details.status_code,
86                json!({
87                    "message": details.message
88                }),
89            ),
90            Self::ValidationError(validation_error_response) => {
91                let json_value = json!({
92                    "errors": validation_error_response.validation_errors
93                });
94                (StatusCode::BAD_REQUEST, json_value)
95            }
96        };
97
98        (status_code, Json(body)).into_response()
99    }
100}
101
102macro_rules! http_error {
103    ($name:ident,$status_code:expr) => {
104        #[allow(missing_docs, unused)]
105        pub fn $name<T>(message: impl Into<String>) -> Result<T, HttpError> {
106            Err(HttpError::WithDetails(HttpErrorDetails {
107                message: message.into(),
108                status_code: $status_code,
109                headers: vec![],
110            }))
111        }
112    };
113}
114
115http_error!(conflict, StatusCode::CONFLICT);
116
117http_error!(unauthorized, StatusCode::UNAUTHORIZED);
118
119http_error!(bad_request, StatusCode::BAD_REQUEST);
120
121http_error!(not_found, StatusCode::NOT_FOUND);
122
123http_error!(internal_server_error, StatusCode::INTERNAL_SERVER_ERROR);
124
125macro_rules! http_response {
126    ($name:ident,$status:expr) => {
127        #[allow(non_snake_case, missing_docs)]
128        pub fn $name(
129            value: impl Serialize + 'static,
130        ) -> Result<axum::response::Response, HttpError> {
131            Ok(($status, Json(value)).into_response())
132        }
133    };
134}
135
136http_response!(ok, StatusCode::OK);
137http_response!(created, StatusCode::CREATED);
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142    use axum::http::StatusCode;
143    use axum::response::IntoResponse;
144    use validator::Validate;
145
146    #[derive(Debug, Validate)]
147    struct Test {
148        #[validate(length(min = 5))]
149        name: String,
150    }
151
152    #[tokio::test]
153    async fn test_validation_error_into_response() {
154        let test = Test {
155            name: "test".to_string(),
156        };
157
158        let validation_errors = test.validate().unwrap_err();
159
160        let http_error: HttpError = validation_errors.into();
161
162        let response = http_error.into_response();
163
164        assert_eq!(response.status(), StatusCode::BAD_REQUEST);
165        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
166            .await
167            .unwrap();
168        assert_eq!(
169            body,
170            "{\"errors\":[{\"code\":\"length\",\"message\":null,\"params\":{\"min\":5,\"value\":\"test\"}}]}"
171        );
172    }
173}