1use axum::{
6 Json,
7 http::StatusCode,
8 response::{IntoResponse, Response},
9};
10use serde::Serialize;
11
12#[derive(Debug, Serialize)]
14pub struct ApiResponse<T> {
15 pub success: bool,
16 #[serde(skip_serializing_if = "Option::is_none")]
17 pub data: Option<T>,
18 #[serde(skip_serializing_if = "Option::is_none")]
19 pub error: Option<ApiError>,
20 #[serde(skip_serializing_if = "Option::is_none")]
21 pub message: Option<String>,
22}
23
24#[derive(Debug, Serialize)]
26pub struct ApiError {
27 pub code: String,
28 pub message: String,
29 #[serde(skip_serializing_if = "Option::is_none")]
30 pub details: Option<serde_json::Value>,
31}
32
33#[derive(Debug, Serialize)]
35pub struct Pagination {
36 pub page: u32,
37 pub limit: u32,
38 pub total: u64,
39 pub pages: u32,
40}
41
42pub type ApiResult<T> = Result<ApiResponse<T>, ApiResponse<()>>;
44
45impl<T> ApiResponse<T> {
46 pub fn success(data: T) -> Self {
48 Self {
49 success: true,
50 data: Some(data),
51 error: None,
52 message: None,
53 }
54 }
55
56 pub fn cast<U>(self) -> ApiResponse<U> {
58 ApiResponse {
59 success: self.success,
60 data: None,
61 error: self.error,
62 message: self.message,
63 }
64 }
65
66 pub fn forbidden_typed() -> ApiResponse<T> {
68 ApiResponse::<()>::forbidden().cast()
69 }
70
71 pub fn unauthorized_typed(code: &str, message: impl Into<String>) -> ApiResponse<T> {
73 ApiResponse::<()>::error(code, message).cast()
74 }
75
76 pub fn conflict_typed(code: &str, message: impl Into<String>) -> ApiResponse<T> {
78 ApiResponse::<()>::conflict(code, message).cast()
79 }
80
81 pub fn error_typed(code: &str, message: impl Into<String>) -> ApiResponse<T> {
83 ApiResponse::<()>::error(code, message).cast()
84 }
85
86 pub fn validation_error_typed(message: impl Into<String>) -> ApiResponse<T> {
88 ApiResponse::<()>::validation_error(message).cast()
89 }
90
91 pub fn not_found_typed(message: impl Into<String>) -> ApiResponse<T> {
93 ApiResponse::<()>::not_found(message).cast()
94 }
95
96 pub fn forbidden_with_message_typed(message: impl Into<String>) -> ApiResponse<T> {
98 ApiResponse::<()>::forbidden_with_message(message).cast()
99 }
100
101 pub fn error_with_message_typed(code: &str, message: impl Into<String>) -> ApiResponse<T> {
103 ApiResponse::<()>::error_with_message(code, message).cast()
104 }
105
106 pub fn not_found_with_message_typed(message: impl Into<String>) -> ApiResponse<T> {
108 ApiResponse::<()>::not_found_with_message(message).cast()
109 }
110
111 pub fn internal_error_typed() -> ApiResponse<T> {
113 ApiResponse::<()>::internal_error().cast()
114 }
115
116 pub fn success_with_message(data: T, message: impl Into<String>) -> Self {
118 Self {
119 success: true,
120 data: Some(data),
121 error: None,
122 message: Some(message.into()),
123 }
124 }
125
126 pub fn ok() -> ApiResponse<()> {
128 ApiResponse {
129 success: true,
130 data: None,
131 error: None,
132 message: None,
133 }
134 }
135
136 pub fn ok_with_message(message: impl Into<String>) -> ApiResponse<()> {
138 ApiResponse {
139 success: true,
140 data: None,
141 error: None,
142 message: Some(message.into()),
143 }
144 }
145}
146
147impl ApiResponse<()> {
148 pub fn error(code: impl Into<String>, message: impl Into<String>) -> Self {
150 Self {
151 success: false,
152 data: None,
153 error: Some(ApiError {
154 code: code.into(),
155 message: message.into(),
156 details: None,
157 }),
158 message: None,
159 }
160 }
161
162 pub fn error_with_details(
164 code: impl Into<String>,
165 message: impl Into<String>,
166 details: serde_json::Value,
167 ) -> Self {
168 Self {
169 success: false,
170 data: None,
171 error: Some(ApiError {
172 code: code.into(),
173 message: message.into(),
174 details: Some(details),
175 }),
176 message: None,
177 }
178 }
179
180 pub fn validation_error(message: impl Into<String>) -> Self {
182 Self::error("VALIDATION_ERROR", message)
183 }
184
185 pub fn unauthorized() -> Self {
187 Self::error("UNAUTHORIZED", "Authentication required")
188 }
189
190 pub fn forbidden() -> Self {
192 Self::error("FORBIDDEN", "Insufficient permissions")
193 }
194
195 pub fn forbidden_with_message(message: impl Into<String>) -> Self {
197 Self::error("FORBIDDEN", message)
198 }
199
200 pub fn not_found(resource: impl Into<String>) -> Self {
202 Self::error("NOT_FOUND", format!("{} not found", resource.into()))
203 }
204
205 pub fn not_found_with_message(message: impl Into<String>) -> Self {
207 Self::error("NOT_FOUND", message)
208 }
209
210 pub fn conflict(code: impl Into<String>, message: impl Into<String>) -> Self {
212 Self::error(code, message)
213 }
214
215 pub fn error_with_message(code: impl Into<String>, message: impl Into<String>) -> Self {
217 Self::error(code, message)
218 }
219
220 pub fn internal_error() -> Self {
222 Self::error("SERVER_ERROR", "Internal server error")
223 }
224}
225
226impl<T> IntoResponse for ApiResponse<T>
227where
228 T: Serialize,
229{
230 fn into_response(self) -> Response {
231 let status = if self.success {
232 StatusCode::OK
233 } else {
234 match self.error.as_ref().map(|e| e.code.as_str()) {
235 Some("UNAUTHORIZED") | Some("INVALID_CREDENTIALS") | Some("AUTH_ERROR") | Some("INVALID_TOKEN") => StatusCode::UNAUTHORIZED,
236 Some("FORBIDDEN") => StatusCode::FORBIDDEN,
237 Some("NOT_FOUND") => StatusCode::NOT_FOUND,
238 Some("VALIDATION_ERROR") => StatusCode::BAD_REQUEST,
239 Some("USERNAME_EXISTS") | Some("EMAIL_EXISTS") => StatusCode::CONFLICT,
240 Some("RATE_LIMITED") => StatusCode::TOO_MANY_REQUESTS,
241 _ => StatusCode::INTERNAL_SERVER_ERROR,
242 }
243 };
244
245 (status, Json(self)).into_response()
246 }
247}
248
249impl From<crate::errors::AuthError> for ApiResponse<()> {
251 fn from(error: crate::errors::AuthError) -> Self {
252 match &error {
253 crate::errors::AuthError::Token(_) => Self::error("INVALID_TOKEN", error.to_string()),
254 crate::errors::AuthError::Validation { .. } => {
255 Self::validation_error(error.to_string())
256 }
257 crate::errors::AuthError::AuthMethod { .. } => {
258 Self::error("INVALID_CREDENTIALS", error.to_string())
259 }
260 crate::errors::AuthError::UserNotFound => Self::not_found(error.to_string()),
261 crate::errors::AuthError::Permission(_) => Self::forbidden(),
262 crate::errors::AuthError::RateLimit { .. } => {
263 Self::error("RATE_LIMITED", error.to_string())
264 }
265 _ => Self::internal_error(),
266 }
267 }
268}
269