Skip to main content

oxide_framework_core/
response.rs

1use axum::{
2    Json,
3    http::StatusCode,
4    response::{IntoResponse, Response},
5};
6use serde::Serialize;
7
8/// Standardized success envelope returned by all handlers that use [`ApiResponse`].
9///
10/// ```json
11/// { "status": 200, "data": { ... } }
12/// ```
13#[derive(Debug, Serialize)]
14pub struct SuccessBody<T: Serialize> {
15    pub status: u16,
16    pub data: T,
17}
18
19/// Standardized error envelope.
20///
21/// ```json
22/// { "status": 404, "error": "not found" }
23/// ```
24#[derive(Debug, Serialize)]
25pub struct ErrorBody {
26    pub status: u16,
27    pub error: String,
28}
29
30/// A unified response type that handlers can return.
31///
32/// Converts into a properly-typed axum `Response` with the correct status code
33/// and a JSON body using either [`SuccessBody`] or [`ErrorBody`].
34///
35/// # Usage
36///
37/// ```rust,ignore
38/// async fn get_user() -> ApiResponse<User> {
39///     let user = User { name: "Alice".into() };
40///     ApiResponse::ok(user)
41/// }
42///
43/// async fn not_found() -> ApiResponse<()> {
44///     ApiResponse::error(StatusCode::NOT_FOUND, "resource not found")
45/// }
46/// ```
47pub enum ApiResponse<T: Serialize> {
48    Success(StatusCode, T),
49    Error(StatusCode, String),
50}
51
52impl<T: Serialize> ApiResponse<T> {
53    /// 200 OK with a data payload.
54    pub fn ok(data: T) -> Self {
55        Self::Success(StatusCode::OK, data)
56    }
57
58    /// 201 Created with a data payload.
59    pub fn created(data: T) -> Self {
60        Self::Success(StatusCode::CREATED, data)
61    }
62
63    /// Arbitrary success status with a data payload.
64    pub fn success(status: StatusCode, data: T) -> Self {
65        Self::Success(status, data)
66    }
67
68    /// Error response with a status code and message.
69    pub fn error(status: StatusCode, message: impl Into<String>) -> Self {
70        Self::Error(status, message.into())
71    }
72
73    /// 400 Bad Request.
74    pub fn bad_request(message: impl Into<String>) -> Self {
75        Self::error(StatusCode::BAD_REQUEST, message)
76    }
77
78    /// 404 Not Found.
79    pub fn not_found(message: impl Into<String>) -> Self {
80        Self::error(StatusCode::NOT_FOUND, message)
81    }
82
83    /// 401 Unauthorized.
84    pub fn unauthorized(message: impl Into<String>) -> Self {
85        Self::error(StatusCode::UNAUTHORIZED, message)
86    }
87
88    /// 403 Forbidden.
89    pub fn forbidden(message: impl Into<String>) -> Self {
90        Self::error(StatusCode::FORBIDDEN, message)
91    }
92
93    /// 500 Internal Server Error.
94    pub fn internal_error(message: impl Into<String>) -> Self {
95        Self::error(StatusCode::INTERNAL_SERVER_ERROR, message)
96    }
97}
98
99impl<T: Serialize> IntoResponse for ApiResponse<T> {
100    fn into_response(self) -> Response {
101        match self {
102            ApiResponse::Success(status, data) => {
103                let body = SuccessBody {
104                    status: status.as_u16(),
105                    data,
106                };
107                (status, Json(body)).into_response()
108            }
109            ApiResponse::Error(status, message) => {
110                let body = ErrorBody {
111                    status: status.as_u16(),
112                    error: message,
113                };
114                (status, Json(body)).into_response()
115            }
116        }
117    }
118}
119