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}