api_tools/server/axum/
response.rs

1//! API response module
2
3use axum::Json;
4use axum::http::StatusCode;
5use axum::response::{IntoResponse, Response};
6use serde::Serialize;
7use thiserror::Error;
8
9/// API response success
10#[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/// Generic response structure shared by all API responses.
35#[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/// API error
51#[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}