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, message) = match self {
75            Self::SqlxError(sqlx_error) => (
76                StatusCode::INTERNAL_SERVER_ERROR,
77                format!("{:?}", sqlx_error),
78            ),
79            Self::WithDetails(details) => (details.status_code, details.message),
80            Self::ValidationError(validation_error_response) => (
81                StatusCode::BAD_REQUEST,
82                format!("{:?}", validation_error_response),
83            ),
84        };
85
86        let body = Json(json!({
87            "message": message
88        }));
89        (status_code, body).into_response()
90    }
91}
92
93macro_rules! http_error {
94    ($name:ident,$status_code:expr) => {
95        #[allow(missing_docs, unused)]
96        pub fn $name<T>(message: impl Into<String>) -> Result<T, HttpError> {
97            Err(HttpError::WithDetails(HttpErrorDetails {
98                message: message.into(),
99                status_code: $status_code,
100                headers: vec![],
101            }))
102        }
103    };
104}
105
106http_error!(conflict, StatusCode::CONFLICT);
107
108http_error!(unauthorized, StatusCode::UNAUTHORIZED);
109
110http_error!(bad_request, StatusCode::BAD_REQUEST);
111
112http_error!(not_found, StatusCode::NOT_FOUND);
113
114http_error!(internal_server_error, StatusCode::INTERNAL_SERVER_ERROR);
115
116macro_rules! http_response {
117    ($name:ident,$status:expr) => {
118        #[allow(non_snake_case, missing_docs)]
119        pub fn $name(
120            value: impl Serialize + 'static,
121        ) -> Result<axum::response::Response, HttpError> {
122            Ok(($status, Json(value)).into_response())
123        }
124    };
125}
126
127http_response!(ok, StatusCode::OK);