axum_webtools/http/
response.rs1use 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}