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, Value};
7use std::collections::HashMap;
8use std::fmt;
9use std::fmt::{Debug, Formatter};
10use thiserror::Error;
11use utoipa::ToSchema;
12use validator::{ValidationError, ValidationErrors};
13
14#[derive(Debug)]
15pub struct HttpErrorDetails {
16    pub message: String,
17    pub status_code: StatusCode,
18    pub headers: Vec<(String, String)>,
19}
20
21#[derive(Debug, Error, Display)]
22pub enum HttpError {
23    #[error(transparent)]
24    SqlxError(#[from] sqlx::Error),
25    WithDetails(HttpErrorDetails),
26    ValidationError(ValidationErrorResponse),
27}
28
29#[derive(Debug, Serialize, Deserialize, Clone)]
30pub struct ValidationErrorResponse {
31    pub validation_errors: Vec<ValidationError>,
32}
33
34impl ValidationErrorResponse {
35    pub fn from(validation_errors: ValidationErrors) -> ValidationErrorResponse {
36        let validation_errors = validation_errors
37            .field_errors()
38            .into_values()
39            .flat_map(|v| v.clone())
40            .collect();
41
42        ValidationErrorResponse { validation_errors }
43    }
44}
45
46impl Display for ValidationErrorResponse {
47    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
48        write!(f, "{:?}", self.validation_errors)
49    }
50}
51
52impl From<ValidationErrors> for HttpError {
53    fn from(validation_errors: ValidationErrors) -> Self {
54        HttpError::ValidationError(ValidationErrorResponse::from(validation_errors))
55    }
56}
57
58impl Display for HttpErrorDetails {
59    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
60        let headers = self
61            .headers
62            .iter()
63            .map(|(k, v)| format!("{}: {}", k, v))
64            .collect::<Vec<String>>()
65            .join(", ");
66        write!(
67            f,
68            "{:?}: {:?} ({:?})",
69            self.status_code, self.message, headers
70        )
71    }
72}
73
74impl IntoResponse for HttpError {
75    fn into_response(self) -> axum::response::Response {
76        let (status_code, body) = match self {
77            Self::SqlxError(sqlx_error) => {
78                let message = format!("{:?}", sqlx_error);
79                (
80                    StatusCode::INTERNAL_SERVER_ERROR,
81                    json!(InternalServerErrorResponse {
82                        message,
83                        status_code: StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
84                    }),
85                )
86            }
87            Self::WithDetails(details) => {
88                let message = details.message;
89                let status_code_enum = details.status_code;
90                let status_code = status_code_enum.as_u16();
91                (
92                    status_code_enum,
93                    match status_code_enum {
94                        StatusCode::CONFLICT => json!(ConflictResponse {
95                            message,
96                            status_code,
97                        }),
98                        StatusCode::UNAUTHORIZED => json!(UnauthorizedResponse {
99                            message,
100                            status_code,
101                        }),
102                        StatusCode::NOT_FOUND => json!(NotFoundResponse {
103                            message,
104                            status_code,
105                        }),
106                        StatusCode::INTERNAL_SERVER_ERROR => {
107                            json!(InternalServerErrorResponse {
108                                message,
109                                status_code,
110                            })
111                        }
112                        StatusCode::BAD_REQUEST => json!(BadRequestResponse {
113                            message,
114                            status_code,
115                        }),
116                        _ => json!(UnprocessableEntityResponse {
117                            message,
118                            status_code,
119                        }),
120                    },
121                )
122            }
123            Self::ValidationError(validation_error_response) => {
124                let messages = validation_error_response
125                    .validation_errors
126                    .iter()
127                    .map(|error| {
128                        let code = error.code.to_string();
129                        let message = error.message.clone().map(|m| m.to_string());
130                        let params = error
131                            .params
132                            .iter()
133                            .map(|(k, v)| (k.to_string(), v.clone()))
134                            .collect::<HashMap<_, _>>();
135                        BadRequestValidationErrorItemResponse {
136                            code,
137                            message,
138                            params,
139                        }
140                    })
141                    .collect::<Vec<_>>();
142                let json_value = json!(BadRequestValidationErrorResponse {
143                    messages,
144                    status_code: StatusCode::BAD_REQUEST.as_u16(),
145                });
146                (StatusCode::BAD_REQUEST, json_value)
147            }
148        };
149
150        (status_code, Json(body)).into_response()
151    }
152}
153
154#[derive(Serialize, Deserialize, ToSchema)]
155pub struct BadRequestValidationErrorItemResponse {
156    pub code: String,
157    pub message: Option<String>,
158    pub params: HashMap<String, Value>,
159}
160
161#[derive(Serialize, Deserialize, ToSchema)]
162pub struct BadRequestValidationErrorResponse {
163    pub messages: Vec<BadRequestValidationErrorItemResponse>,
164    pub status_code: u16,
165}
166
167macro_rules! http_error_struct {
168    ($name:ident) => {
169        #[derive(Serialize, Deserialize, ToSchema)]
170        pub struct $name {
171            pub message: String,
172            #[serde(rename = "statusCode")]
173            pub status_code: u16,
174        }
175    };
176}
177
178http_error_struct!(ConflictResponse);
179http_error_struct!(NotFoundResponse);
180http_error_struct!(InternalServerErrorResponse);
181http_error_struct!(BadRequestResponse);
182http_error_struct!(UnauthorizedResponse);
183http_error_struct!(UnprocessableEntityResponse);
184
185macro_rules! http_error {
186    ($name:ident,$status_code:expr) => {
187        #[allow(missing_docs, unused)]
188        pub fn $name<T>(message: impl Into<String>) -> Result<T, HttpError> {
189            Err(HttpError::WithDetails(HttpErrorDetails {
190                message: message.into(),
191                status_code: $status_code,
192                headers: vec![],
193            }))
194        }
195    };
196}
197
198http_error!(unprocessable_entity, StatusCode::UNPROCESSABLE_ENTITY);
199
200http_error!(conflict, StatusCode::CONFLICT);
201
202http_error!(unauthorized, StatusCode::UNAUTHORIZED);
203
204http_error!(bad_request, StatusCode::BAD_REQUEST);
205
206http_error!(not_found, StatusCode::NOT_FOUND);
207
208http_error!(internal_server_error, StatusCode::INTERNAL_SERVER_ERROR);
209
210macro_rules! http_response {
211    ($name:ident,$status:expr) => {
212        #[allow(non_snake_case, missing_docs)]
213        pub fn $name(
214            value: impl Serialize + 'static,
215        ) -> Result<axum::response::Response, HttpError> {
216            Ok(($status, Json(value)).into_response())
217        }
218    };
219}
220
221http_response!(ok, StatusCode::OK);
222http_response!(created, StatusCode::CREATED);
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227    use axum::http::StatusCode;
228    use axum::response::IntoResponse;
229    use validator::Validate;
230
231    #[derive(Debug, Validate)]
232    struct Test {
233        #[validate(length(min = 5))]
234        name: String,
235    }
236
237    #[tokio::test]
238    async fn test_validation_error_into_response() {
239        let test = Test {
240            name: "test".to_string(),
241        };
242
243        let validation_errors = test.validate().unwrap_err();
244
245        let http_error: HttpError = validation_errors.into();
246
247        let response = http_error.into_response();
248
249        assert_eq!(response.status(), StatusCode::BAD_REQUEST);
250        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
251            .await
252            .unwrap();
253        assert_eq!(
254            body,
255            "{\"messages\":[{\"code\":\"length\",\"message\":null,\"params\":{\"min\":5,\"value\":\"test\"}}],\"status_code\":400}"
256        );
257    }
258}