1use axum::Json;
4use axum::http::StatusCode;
5use axum::response::{IntoResponse, Response};
6use serde::Serialize;
7use thiserror::Error;
8
9#[derive(Debug, Clone)]
11pub struct ApiSuccess<T: Serialize + PartialEq>(StatusCode, Json<T>);
12
13impl<T> PartialEq for ApiSuccess<T>
14where
15 T: Serialize + PartialEq,
16{
17 fn eq(&self, other: &Self) -> bool {
18 self.0 == other.0 && self.1.0 == other.1.0
19 }
20}
21
22impl<T: Serialize + PartialEq> ApiSuccess<T> {
23 pub fn new(status: StatusCode, data: T) -> Self {
24 ApiSuccess(status, Json(data))
25 }
26}
27
28impl<T: Serialize + PartialEq> IntoResponse for ApiSuccess<T> {
29 fn into_response(self) -> Response {
30 (self.0, self.1).into_response()
31 }
32}
33
34#[derive(Debug, Clone, PartialEq, Serialize)]
36pub struct ApiErrorResponse<T: Serialize + PartialEq> {
37 code: u16,
38 message: T,
39}
40
41impl<T: Serialize + PartialEq> ApiErrorResponse<T> {
42 pub fn new(status_code: StatusCode, message: T) -> Self {
43 Self {
44 code: status_code.as_u16(),
45 message,
46 }
47 }
48}
49
50#[derive(Debug, Clone, PartialEq, Error)]
52pub enum ApiError {
53 #[error("Bad request: {0}")]
54 BadRequest(String),
55
56 #[error("Unauthorized: {0}")]
57 Unauthorized(String),
58
59 #[error("Forbidden: {0}")]
60 Forbidden(String),
61
62 #[error("Not found: {0}")]
63 NotFound(String),
64
65 #[error("Unprocessable entity: {0}")]
66 UnprocessableEntity(String),
67
68 #[error("Internal server error: {0}")]
69 InternalServerError(String),
70
71 #[error("Timeout")]
72 Timeout,
73
74 #[error("Too many requests")]
75 TooManyRequests,
76
77 #[error("Method not allowed")]
78 MethodNotAllowed,
79
80 #[error("Payload too large")]
81 PayloadTooLarge,
82
83 #[error("Service unavailable")]
84 ServiceUnavailable,
85}
86
87impl ApiError {
88 fn response(code: StatusCode, message: &str) -> impl IntoResponse + '_ {
89 match code {
90 StatusCode::REQUEST_TIMEOUT => (
91 StatusCode::REQUEST_TIMEOUT,
92 Json(ApiErrorResponse::new(StatusCode::REQUEST_TIMEOUT, message)),
93 ),
94 StatusCode::TOO_MANY_REQUESTS => (
95 StatusCode::TOO_MANY_REQUESTS,
96 Json(ApiErrorResponse::new(StatusCode::TOO_MANY_REQUESTS, message)),
97 ),
98 StatusCode::METHOD_NOT_ALLOWED => (
99 StatusCode::METHOD_NOT_ALLOWED,
100 Json(ApiErrorResponse::new(StatusCode::METHOD_NOT_ALLOWED, message)),
101 ),
102 StatusCode::PAYLOAD_TOO_LARGE => (
103 StatusCode::PAYLOAD_TOO_LARGE,
104 Json(ApiErrorResponse::new(StatusCode::PAYLOAD_TOO_LARGE, message)),
105 ),
106 StatusCode::BAD_REQUEST => (
107 StatusCode::BAD_REQUEST,
108 Json(ApiErrorResponse::new(StatusCode::BAD_REQUEST, message)),
109 ),
110 StatusCode::UNAUTHORIZED => (
111 StatusCode::UNAUTHORIZED,
112 Json(ApiErrorResponse::new(StatusCode::UNAUTHORIZED, message)),
113 ),
114 StatusCode::FORBIDDEN => (
115 StatusCode::FORBIDDEN,
116 Json(ApiErrorResponse::new(StatusCode::FORBIDDEN, message)),
117 ),
118 StatusCode::NOT_FOUND => (
119 StatusCode::NOT_FOUND,
120 Json(ApiErrorResponse::new(StatusCode::NOT_FOUND, message)),
121 ),
122 StatusCode::SERVICE_UNAVAILABLE => (
123 StatusCode::SERVICE_UNAVAILABLE,
124 Json(ApiErrorResponse::new(StatusCode::SERVICE_UNAVAILABLE, message)),
125 ),
126 StatusCode::UNPROCESSABLE_ENTITY => (
127 StatusCode::UNPROCESSABLE_ENTITY,
128 Json(ApiErrorResponse::new(StatusCode::UNPROCESSABLE_ENTITY, message)),
129 ),
130 _ => (
131 StatusCode::INTERNAL_SERVER_ERROR,
132 Json(ApiErrorResponse::new(StatusCode::INTERNAL_SERVER_ERROR, message)),
133 ),
134 }
135 }
136}
137
138impl IntoResponse for ApiError {
139 fn into_response(self) -> Response {
140 match self {
141 ApiError::Timeout => Self::response(StatusCode::REQUEST_TIMEOUT, "Request timeout").into_response(),
142 ApiError::TooManyRequests => {
143 Self::response(StatusCode::TOO_MANY_REQUESTS, "Too many requests").into_response()
144 }
145 ApiError::MethodNotAllowed => {
146 Self::response(StatusCode::METHOD_NOT_ALLOWED, "Method not allowed").into_response()
147 }
148 ApiError::PayloadTooLarge => {
149 Self::response(StatusCode::PAYLOAD_TOO_LARGE, "Payload too large").into_response()
150 }
151 ApiError::ServiceUnavailable => {
152 Self::response(StatusCode::SERVICE_UNAVAILABLE, "Service unavailable").into_response()
153 }
154 ApiError::BadRequest(message) => Self::response(StatusCode::BAD_REQUEST, &message).into_response(),
155 ApiError::Unauthorized(message) => Self::response(StatusCode::UNAUTHORIZED, &message).into_response(),
156 ApiError::Forbidden(message) => Self::response(StatusCode::FORBIDDEN, &message).into_response(),
157 ApiError::NotFound(message) => Self::response(StatusCode::NOT_FOUND, &message).into_response(),
158 ApiError::UnprocessableEntity(message) => {
159 Self::response(StatusCode::UNPROCESSABLE_ENTITY, &message).into_response()
160 }
161 ApiError::InternalServerError(message) => {
162 Self::response(StatusCode::INTERNAL_SERVER_ERROR, &message).into_response()
163 }
164 }
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171 use serde_json::json;
172
173 #[test]
174 fn test_api_success_partial_eq() {
175 let success1 = ApiSuccess::new(StatusCode::OK, json!({"data": "test"}));
176 let success2 = ApiSuccess::new(StatusCode::OK, json!({"data": "test"}));
177 assert_eq!(success1, success2);
178
179 let success3 = ApiSuccess::new(StatusCode::BAD_REQUEST, json!({"data": "test"}));
180 assert_ne!(success1, success3);
181 }
182
183 #[tokio::test]
184 async fn test_api_success_into_response() {
185 let data = json!({"hello": "world"});
186 let api_success = ApiSuccess::new(StatusCode::OK, data.clone());
187 let response = api_success.into_response();
188 assert_eq!(response.status(), StatusCode::OK);
189
190 let body = response.into_body();
191 let body_bytes = axum::body::to_bytes(body, 1_024).await.unwrap();
192 let body_str = String::from_utf8(body_bytes.to_vec()).unwrap();
193 assert_eq!(body_str, data.to_string());
194 }
195
196 #[test]
197 fn test_new_api_error_response() {
198 let error = ApiErrorResponse::new(StatusCode::BAD_REQUEST, "Bad request");
199 assert_eq!(error.code, 400);
200 assert_eq!(error.message, "Bad request");
201 }
202
203 #[tokio::test]
204 async fn test_api_error_into_response_bad_request() {
205 let error = ApiError::BadRequest("Invalid input".to_string());
206 assert_eq!(error.to_string(), "Bad request: Invalid input");
207
208 let response = error.into_response();
209 assert_eq!(response.status(), StatusCode::BAD_REQUEST);
210
211 let body = response.into_body();
212 let body_bytes = axum::body::to_bytes(body, 1_024).await.unwrap();
213 let body_str = String::from_utf8(body_bytes.to_vec()).unwrap();
214 assert_eq!(body_str, json!({ "code": 400, "message": "Invalid input" }).to_string());
215 }
216
217 #[tokio::test]
218 async fn test_api_error_into_response_unauthorized() {
219 let error = ApiError::Unauthorized("Not authorized".to_string());
220 assert_eq!(error.to_string(), "Unauthorized: Not authorized");
221
222 let response = error.into_response();
223 assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
224
225 let body = response.into_body();
226 let body_bytes = axum::body::to_bytes(body, 1_024).await.unwrap();
227 let body_str = String::from_utf8(body_bytes.to_vec()).unwrap();
228 assert_eq!(
229 body_str,
230 json!({ "code": 401, "message": "Not authorized" }).to_string()
231 );
232 }
233
234 #[tokio::test]
235 async fn test_api_error_into_response_forbidden() {
236 let error = ApiError::Forbidden("Access denied".to_string());
237 assert_eq!(error.to_string(), "Forbidden: Access denied");
238
239 let response = error.into_response();
240 assert_eq!(response.status(), StatusCode::FORBIDDEN);
241
242 let body = response.into_body();
243 let body_bytes = axum::body::to_bytes(body, 1_024).await.unwrap();
244 let body_str = String::from_utf8(body_bytes.to_vec()).unwrap();
245 assert_eq!(body_str, json!({ "code": 403, "message": "Access denied" }).to_string());
246 }
247
248 #[tokio::test]
249 async fn test_api_error_into_response_not_found() {
250 let error = ApiError::NotFound("Resource missing".to_string());
251 assert_eq!(error.to_string(), "Not found: Resource missing");
252
253 let response = error.into_response();
254 assert_eq!(response.status(), StatusCode::NOT_FOUND);
255
256 let body = response.into_body();
257 let body_bytes = axum::body::to_bytes(body, 1_024).await.unwrap();
258 let body_str = String::from_utf8(body_bytes.to_vec()).unwrap();
259 assert_eq!(
260 body_str,
261 json!({ "code": 404, "message": "Resource missing" }).to_string()
262 );
263 }
264
265 #[tokio::test]
266 async fn test_api_error_into_response_unprocessable_entity() {
267 let error = ApiError::UnprocessableEntity("Invalid data".to_string());
268 assert_eq!(error.to_string(), "Unprocessable entity: Invalid data");
269
270 let response = error.into_response();
271 assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);
272
273 let body = response.into_body();
274 let body_bytes = axum::body::to_bytes(body, 1_024).await.unwrap();
275 let body_str = String::from_utf8(body_bytes.to_vec()).unwrap();
276 assert_eq!(body_str, json!({ "code": 422, "message": "Invalid data" }).to_string());
277 }
278
279 #[tokio::test]
280 async fn test_api_error_into_response_internal_server_error() {
281 let error = ApiError::InternalServerError("Unexpected".to_string());
282 assert_eq!(error.to_string(), "Internal server error: Unexpected");
283
284 let response = error.into_response();
285 assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
286
287 let body = response.into_body();
288 let body_bytes = axum::body::to_bytes(body, 1_024).await.unwrap();
289 let body_str = String::from_utf8(body_bytes.to_vec()).unwrap();
290 assert_eq!(body_str, json!({ "code": 500, "message": "Unexpected" }).to_string());
291 }
292
293 #[tokio::test]
294 async fn test_api_error_into_response_timeout() {
295 let error = ApiError::Timeout;
296 assert_eq!(error.to_string(), "Timeout");
297
298 let response = error.into_response();
299 assert_eq!(response.status(), StatusCode::REQUEST_TIMEOUT);
300
301 let body = response.into_body();
302 let body_bytes = axum::body::to_bytes(body, 1_024).await.unwrap();
303 let body_str = String::from_utf8(body_bytes.to_vec()).unwrap();
304 assert_eq!(
305 body_str,
306 json!({ "code": 408, "message": "Request timeout" }).to_string()
307 );
308 }
309
310 #[tokio::test]
311 async fn test_api_error_into_response_too_many_requests() {
312 let error = ApiError::TooManyRequests;
313 assert_eq!(error.to_string(), "Too many requests");
314
315 let response = error.into_response();
316 assert_eq!(response.status(), StatusCode::TOO_MANY_REQUESTS);
317
318 let body = response.into_body();
319 let body_bytes = axum::body::to_bytes(body, 1_024).await.unwrap();
320 let body_str = String::from_utf8(body_bytes.to_vec()).unwrap();
321 assert_eq!(
322 body_str,
323 json!({ "code": 429, "message": "Too many requests" }).to_string()
324 );
325 }
326
327 #[tokio::test]
328 async fn test_api_error_into_response_method_not_allowed() {
329 let error = ApiError::MethodNotAllowed;
330 assert_eq!(error.to_string(), "Method not allowed");
331
332 let response = error.into_response();
333 assert_eq!(response.status(), StatusCode::METHOD_NOT_ALLOWED);
334
335 let body = response.into_body();
336 let body_bytes = axum::body::to_bytes(body, 1_024).await.unwrap();
337 let body_str = String::from_utf8(body_bytes.to_vec()).unwrap();
338 assert_eq!(
339 body_str,
340 json!({ "code": 405, "message": "Method not allowed" }).to_string()
341 );
342 }
343
344 #[tokio::test]
345 async fn test_api_error_into_response_payload_too_large() {
346 let error = ApiError::PayloadTooLarge;
347 assert_eq!(error.to_string(), "Payload too large");
348
349 let response = error.into_response();
350 assert_eq!(response.status(), StatusCode::PAYLOAD_TOO_LARGE);
351
352 let body = response.into_body();
353 let body_bytes = axum::body::to_bytes(body, 1_024).await.unwrap();
354 let body_str = String::from_utf8(body_bytes.to_vec()).unwrap();
355 assert_eq!(
356 body_str,
357 json!({ "code": 413, "message": "Payload too large" }).to_string()
358 );
359 }
360
361 #[tokio::test]
362 async fn test_api_error_into_response_service_unavailable() {
363 let error = ApiError::ServiceUnavailable;
364 assert_eq!(error.to_string(), "Service unavailable");
365
366 let response = error.into_response();
367 assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
368
369 let body = response.into_body();
370 let body_bytes = axum::body::to_bytes(body, 1_024).await.unwrap();
371 let body_str = String::from_utf8(body_bytes.to_vec()).unwrap();
372 assert_eq!(
373 body_str,
374 json!({ "code": 503, "message": "Service unavailable" }).to_string()
375 );
376 }
377
378 #[tokio::test]
379 async fn test_api_error_response() {
380 let response = ApiError::response(StatusCode::INTERNAL_SERVER_ERROR, "Internal server error");
381 let response = response.into_response();
382 assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
383
384 let body = response.into_body();
385 let body_bytes = axum::body::to_bytes(body, 1_024).await.unwrap();
386 let body_str = String::from_utf8(body_bytes.to_vec()).unwrap();
387 assert_eq!(
388 body_str,
389 json!({ "code": 500, "message": "Internal server error" }).to_string()
390 );
391 }
392}