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() -> ApiResponse<T> {
73 ApiResponse::<()>::unauthorized().cast()
74 }
75
76 pub fn error_typed(code: &str, message: impl Into<String>) -> ApiResponse<T> {
78 ApiResponse::<()>::error(code, message).cast()
79 }
80
81 pub fn validation_error_typed(message: impl Into<String>) -> ApiResponse<T> {
83 ApiResponse::<()>::validation_error(message).cast()
84 }
85
86 pub fn not_found_typed(message: impl Into<String>) -> ApiResponse<T> {
88 ApiResponse::<()>::not_found(message).cast()
89 }
90
91 pub fn forbidden_with_message_typed(message: impl Into<String>) -> ApiResponse<T> {
93 ApiResponse::<()>::forbidden_with_message(message).cast()
94 }
95
96 pub fn error_with_message_typed(code: &str, message: impl Into<String>) -> ApiResponse<T> {
98 ApiResponse::<()>::error_with_message(code, message).cast()
99 }
100
101 pub fn not_found_with_message_typed(message: impl Into<String>) -> ApiResponse<T> {
103 ApiResponse::<()>::not_found_with_message(message).cast()
104 }
105
106 pub fn internal_error_typed() -> ApiResponse<T> {
108 ApiResponse::<()>::internal_error().cast()
109 }
110
111 pub fn success_with_message(data: T, message: impl Into<String>) -> Self {
113 Self {
114 success: true,
115 data: Some(data),
116 error: None,
117 message: Some(message.into()),
118 }
119 }
120
121 pub fn ok() -> ApiResponse<()> {
123 ApiResponse {
124 success: true,
125 data: None,
126 error: None,
127 message: None,
128 }
129 }
130
131 pub fn ok_with_message(message: impl Into<String>) -> ApiResponse<()> {
133 ApiResponse {
134 success: true,
135 data: None,
136 error: None,
137 message: Some(message.into()),
138 }
139 }
140}
141
142impl ApiResponse<()> {
143 pub fn error(code: impl Into<String>, message: impl Into<String>) -> Self {
145 Self {
146 success: false,
147 data: None,
148 error: Some(ApiError {
149 code: code.into(),
150 message: message.into(),
151 details: None,
152 }),
153 message: None,
154 }
155 }
156
157 pub fn error_with_details(
159 code: impl Into<String>,
160 message: impl Into<String>,
161 details: serde_json::Value,
162 ) -> Self {
163 Self {
164 success: false,
165 data: None,
166 error: Some(ApiError {
167 code: code.into(),
168 message: message.into(),
169 details: Some(details),
170 }),
171 message: None,
172 }
173 }
174
175 pub fn validation_error(message: impl Into<String>) -> Self {
177 Self::error("VALIDATION_ERROR", message)
178 }
179
180 pub fn unauthorized() -> Self {
182 Self::error("UNAUTHORIZED", "Authentication required")
183 }
184
185 pub fn forbidden() -> Self {
187 Self::error("FORBIDDEN", "Insufficient permissions")
188 }
189
190 pub fn forbidden_with_message(message: impl Into<String>) -> Self {
192 Self::error("FORBIDDEN", message)
193 }
194
195 pub fn not_found(resource: impl Into<String>) -> Self {
197 Self::error("NOT_FOUND", format!("{} not found", resource.into()))
198 }
199
200 pub fn not_found_with_message(message: impl Into<String>) -> Self {
202 Self::error("NOT_FOUND", message)
203 }
204
205 pub fn error_with_message(code: impl Into<String>, message: impl Into<String>) -> Self {
207 Self::error(code, message)
208 }
209
210 pub fn internal_error() -> Self {
212 Self::error("SERVER_ERROR", "Internal server error")
213 }
214}
215
216impl<T> IntoResponse for ApiResponse<T>
217where
218 T: Serialize,
219{
220 fn into_response(self) -> Response {
221 let status = if self.success {
222 StatusCode::OK
223 } else {
224 match self.error.as_ref().map(|e| e.code.as_str()) {
225 Some("UNAUTHORIZED") => StatusCode::UNAUTHORIZED,
226 Some("FORBIDDEN") => StatusCode::FORBIDDEN,
227 Some("NOT_FOUND") => StatusCode::NOT_FOUND,
228 Some("VALIDATION_ERROR") => StatusCode::BAD_REQUEST,
229 Some("RATE_LIMITED") => StatusCode::TOO_MANY_REQUESTS,
230 _ => StatusCode::INTERNAL_SERVER_ERROR,
231 }
232 };
233
234 (status, Json(self)).into_response()
235 }
236}
237
238impl From<crate::errors::AuthError> for ApiResponse<()> {
240 fn from(error: crate::errors::AuthError) -> Self {
241 match &error {
242 crate::errors::AuthError::Token(_) => Self::error("INVALID_TOKEN", error.to_string()),
243 crate::errors::AuthError::Validation { .. } => {
244 Self::validation_error(error.to_string())
245 }
246 crate::errors::AuthError::AuthMethod { .. } => {
247 Self::error("INVALID_CREDENTIALS", error.to_string())
248 }
249 crate::errors::AuthError::UserNotFound => Self::not_found(error.to_string()),
250 crate::errors::AuthError::Permission(_) => Self::forbidden(),
251 crate::errors::AuthError::RateLimit { .. } => {
252 Self::error("RATE_LIMITED", error.to_string())
253 }
254 _ => Self::internal_error(),
255 }
256 }
257}
258
259