auth_framework/api/responses.rs
1//! API Response Types
2//!
3//! Common response types for the REST API
4
5use axum::{
6 Json,
7 http::StatusCode,
8 response::{IntoResponse, Response},
9};
10use serde::Serialize;
11
12/// Standard API response wrapper.
13///
14/// Encapsulates success/error status, optional data payload,
15/// optional error details, and an optional human-readable message.
16///
17/// # Example
18/// ```rust
19/// use auth_framework::api::responses::ApiResponse;
20///
21/// let resp = ApiResponse::success("hello");
22/// assert!(resp.success);
23/// assert_eq!(resp.data, Some("hello"));
24/// ```
25#[derive(Debug, Serialize)]
26pub struct ApiResponse<T> {
27 pub success: bool,
28 #[serde(skip_serializing_if = "Option::is_none")]
29 pub data: Option<T>,
30 #[serde(skip_serializing_if = "Option::is_none")]
31 pub error: Option<ApiError>,
32 #[serde(skip_serializing_if = "Option::is_none")]
33 pub message: Option<String>,
34}
35
36/// API error details attached to a failed [`ApiResponse`].
37///
38/// Contains a machine-readable `code`, human-readable `message`,
39/// and optional structured `details`.
40///
41/// # Example
42/// ```rust
43/// use auth_framework::api::responses::ApiResponse;
44///
45/// let resp = ApiResponse::<()>::error("BAD_INPUT", "missing field");
46/// let err = resp.error.unwrap();
47/// assert_eq!(err.code, "BAD_INPUT");
48/// ```
49#[derive(Debug, Serialize)]
50pub struct ApiError {
51 pub code: String,
52 pub message: String,
53 #[serde(skip_serializing_if = "Option::is_none")]
54 pub details: Option<serde_json::Value>,
55}
56
57/// Pagination metadata for list endpoints.
58///
59/// # Example
60/// ```rust
61/// use auth_framework::api::responses::Pagination;
62///
63/// let page = Pagination { page: 1, limit: 25, total: 100, pages: 4 };
64/// assert_eq!(page.pages, 4);
65/// ```
66#[derive(Debug, Serialize)]
67pub struct Pagination {
68 pub page: u32,
69 pub limit: u32,
70 pub total: u64,
71 pub pages: u32,
72}
73
74/// API result type
75pub type ApiResult<T> = Result<ApiResponse<T>, ApiResponse<()>>;
76
77impl<T> ApiResponse<T> {
78 /// Create a successful response carrying `data`.
79 ///
80 /// # Example
81 /// ```rust
82 /// use auth_framework::api::responses::ApiResponse;
83 ///
84 /// let resp = ApiResponse::success(42u32);
85 /// assert!(resp.success);
86 /// assert_eq!(resp.data, Some(42));
87 /// ```
88 pub fn success(data: T) -> Self {
89 Self {
90 success: true,
91 data: Some(data),
92 error: None,
93 message: None,
94 }
95 }
96
97 /// Convert this response to another data type, discarding the payload.
98 ///
99 /// Useful for propagating error responses where the data type differs.
100 ///
101 /// # Example
102 /// ```rust
103 /// use auth_framework::api::responses::ApiResponse;
104 ///
105 /// let err = ApiResponse::<()>::error("FAIL", "oops");
106 /// let typed: ApiResponse<String> = err.cast();
107 /// assert!(!typed.success);
108 /// ```
109 pub fn cast<U>(self) -> ApiResponse<U> {
110 ApiResponse {
111 success: self.success,
112 data: None,
113 error: self.error,
114 message: self.message,
115 }
116 }
117
118 /// Create a forbidden (403) response for any data type `T`.
119 ///
120 /// # Example
121 /// ```rust
122 /// use auth_framework::api::responses::ApiResponse;
123 ///
124 /// let resp: ApiResponse<String> = ApiResponse::forbidden_typed();
125 /// assert!(!resp.success);
126 /// ```
127 pub fn forbidden_typed() -> ApiResponse<T> {
128 ApiResponse::<()>::forbidden().cast()
129 }
130
131 /// Create an unauthorized (401) response for any data type `T`.
132 ///
133 /// # Example
134 /// ```rust
135 /// use auth_framework::api::responses::ApiResponse;
136 ///
137 /// let resp: ApiResponse<Vec<u8>> = ApiResponse::unauthorized_typed();
138 /// assert!(!resp.success);
139 /// ```
140 pub fn unauthorized_typed() -> ApiResponse<T> {
141 ApiResponse::<()>::unauthorized().cast()
142 }
143
144 /// Create an error response for any data type `T`.
145 ///
146 /// # Example
147 /// ```rust
148 /// use auth_framework::api::responses::ApiResponse;
149 ///
150 /// let resp: ApiResponse<i32> = ApiResponse::error_typed("FAIL", "bad input");
151 /// assert!(!resp.success);
152 /// ```
153 pub fn error_typed(code: &str, message: impl Into<String>) -> ApiResponse<T> {
154 ApiResponse::<()>::error(code, message).cast()
155 }
156
157 /// Create a validation error (400) response for any data type `T`.
158 ///
159 /// # Example
160 /// ```rust
161 /// use auth_framework::api::responses::ApiResponse;
162 ///
163 /// let resp: ApiResponse<()> = ApiResponse::validation_error_typed("bad field");
164 /// assert!(!resp.success);
165 /// ```
166 pub fn validation_error_typed(message: impl Into<String>) -> ApiResponse<T> {
167 ApiResponse::<()>::validation_error(message).cast()
168 }
169
170 /// Create a not-found (404) response for any data type `T`.
171 ///
172 /// # Example
173 /// ```rust
174 /// use auth_framework::api::responses::ApiResponse;
175 ///
176 /// let resp: ApiResponse<String> = ApiResponse::not_found_typed("user");
177 /// assert!(!resp.success);
178 /// ```
179 pub fn not_found_typed(message: impl Into<String>) -> ApiResponse<T> {
180 ApiResponse::<()>::not_found(message).cast()
181 }
182
183 /// Create a forbidden (403) response with a custom message for any data type `T`.
184 ///
185 /// # Example
186 /// ```rust
187 /// use auth_framework::api::responses::ApiResponse;
188 ///
189 /// let resp: ApiResponse<()> = ApiResponse::forbidden_with_message_typed("admin only");
190 /// assert!(!resp.success);
191 /// ```
192 pub fn forbidden_with_message_typed(message: impl Into<String>) -> ApiResponse<T> {
193 ApiResponse::<()>::forbidden_with_message(message).cast()
194 }
195
196 /// Create an error response with a custom code and message for any data type `T`.
197 ///
198 /// # Example
199 /// ```rust
200 /// use auth_framework::api::responses::ApiResponse;
201 ///
202 /// let resp: ApiResponse<()> = ApiResponse::error_with_message_typed("QUOTA", "exceeded");
203 /// assert_eq!(resp.error.unwrap().code, "QUOTA");
204 /// ```
205 pub fn error_with_message_typed(code: &str, message: impl Into<String>) -> ApiResponse<T> {
206 ApiResponse::<()>::error_with_message(code, message).cast()
207 }
208
209 /// Create a not-found (404) response with a custom message for any data type `T`.
210 ///
211 /// # Example
212 /// ```rust
213 /// use auth_framework::api::responses::ApiResponse;
214 ///
215 /// let resp: ApiResponse<()> = ApiResponse::not_found_with_message_typed("gone");
216 /// assert!(!resp.success);
217 /// ```
218 pub fn not_found_with_message_typed(message: impl Into<String>) -> ApiResponse<T> {
219 ApiResponse::<()>::not_found_with_message(message).cast()
220 }
221
222 /// Create an internal server error (500) response for any data type `T`.
223 ///
224 /// # Example
225 /// ```rust
226 /// use auth_framework::api::responses::ApiResponse;
227 ///
228 /// let resp: ApiResponse<()> = ApiResponse::internal_error_typed();
229 /// assert!(!resp.success);
230 /// ```
231 pub fn internal_error_typed() -> ApiResponse<T> {
232 ApiResponse::<()>::internal_error().cast()
233 }
234
235 /// Create a successful response with data and a human-readable message.
236 ///
237 /// # Example
238 /// ```rust
239 /// use auth_framework::api::responses::ApiResponse;
240 ///
241 /// let resp = ApiResponse::success_with_message("done", "operation complete");
242 /// assert!(resp.success);
243 /// assert_eq!(resp.message, Some("operation complete".into()));
244 /// ```
245 pub fn success_with_message(data: T, message: impl Into<String>) -> Self {
246 Self {
247 success: true,
248 data: Some(data),
249 error: None,
250 message: Some(message.into()),
251 }
252 }
253
254 /// Create a simple success response with no data.
255 ///
256 /// # Example
257 /// ```rust
258 /// use auth_framework::api::responses::ApiResponse;
259 ///
260 /// let resp = ApiResponse::<()>::ok();
261 /// assert!(resp.success);
262 /// assert!(resp.data.is_none());
263 /// ```
264 pub fn ok() -> ApiResponse<()> {
265 ApiResponse {
266 success: true,
267 data: None,
268 error: None,
269 message: None,
270 }
271 }
272
273 /// Create a success response with a message but no data.
274 ///
275 /// # Example
276 /// ```rust
277 /// use auth_framework::api::responses::ApiResponse;
278 ///
279 /// let resp = ApiResponse::<()>::ok_with_message("saved");
280 /// assert!(resp.success);
281 /// assert_eq!(resp.message, Some("saved".into()));
282 /// ```
283 pub fn ok_with_message(message: impl Into<String>) -> ApiResponse<()> {
284 ApiResponse {
285 success: true,
286 data: None,
287 error: None,
288 message: Some(message.into()),
289 }
290 }
291}
292
293impl ApiResponse<()> {
294 /// Create an error response with a code and message.
295 ///
296 /// # Example
297 /// ```rust
298 /// use auth_framework::api::responses::ApiResponse;
299 ///
300 /// let resp = ApiResponse::<()>::error("BAD_REQUEST", "missing param");
301 /// assert!(!resp.success);
302 /// assert_eq!(resp.error.as_ref().unwrap().code, "BAD_REQUEST");
303 /// ```
304 pub fn error(code: impl Into<String>, message: impl Into<String>) -> Self {
305 Self {
306 success: false,
307 data: None,
308 error: Some(ApiError {
309 code: code.into(),
310 message: message.into(),
311 details: None,
312 }),
313 message: None,
314 }
315 }
316
317 /// Create an error response with structured details.
318 ///
319 /// # Example
320 /// ```rust
321 /// use auth_framework::api::responses::ApiResponse;
322 ///
323 /// let details = serde_json::json!({"fields": ["name"]});
324 /// let resp = ApiResponse::<()>::error_with_details("VALIDATION", "invalid", details);
325 /// assert!(resp.error.as_ref().unwrap().details.is_some());
326 /// ```
327 pub fn error_with_details(
328 code: impl Into<String>,
329 message: impl Into<String>,
330 details: serde_json::Value,
331 ) -> Self {
332 Self {
333 success: false,
334 data: None,
335 error: Some(ApiError {
336 code: code.into(),
337 message: message.into(),
338 details: Some(details),
339 }),
340 message: None,
341 }
342 }
343
344 /// Create a validation error (400) response.
345 ///
346 /// # Example
347 /// ```rust
348 /// use auth_framework::api::responses::ApiResponse;
349 ///
350 /// let resp = ApiResponse::<()>::validation_error("email is invalid");
351 /// assert_eq!(resp.error.as_ref().unwrap().code, "VALIDATION_ERROR");
352 /// ```
353 pub fn validation_error(message: impl Into<String>) -> Self {
354 Self::error("VALIDATION_ERROR", message)
355 }
356
357 /// Create an unauthorized (401) error response.
358 ///
359 /// # Example
360 /// ```rust
361 /// use auth_framework::api::responses::ApiResponse;
362 ///
363 /// let resp = ApiResponse::<()>::unauthorized();
364 /// assert_eq!(resp.error.as_ref().unwrap().code, "UNAUTHORIZED");
365 /// ```
366 pub fn unauthorized() -> Self {
367 Self::error("UNAUTHORIZED", "Authentication required")
368 }
369
370 /// Create a forbidden (403) error response.
371 ///
372 /// # Example
373 /// ```rust
374 /// use auth_framework::api::responses::ApiResponse;
375 ///
376 /// let resp = ApiResponse::<()>::forbidden();
377 /// assert_eq!(resp.error.as_ref().unwrap().code, "FORBIDDEN");
378 /// ```
379 pub fn forbidden() -> Self {
380 Self::error("FORBIDDEN", "Insufficient permissions")
381 }
382
383 /// Create a forbidden (403) error with a custom message.
384 ///
385 /// # Example
386 /// ```rust
387 /// use auth_framework::api::responses::ApiResponse;
388 ///
389 /// let resp = ApiResponse::<()>::forbidden_with_message("admin area");
390 /// assert_eq!(resp.error.as_ref().unwrap().message, "admin area");
391 /// ```
392 pub fn forbidden_with_message(message: impl Into<String>) -> Self {
393 Self::error("FORBIDDEN", message)
394 }
395
396 /// Create a not-found (404) error naming the missing resource.
397 ///
398 /// # Example
399 /// ```rust
400 /// use auth_framework::api::responses::ApiResponse;
401 ///
402 /// let resp = ApiResponse::<()>::not_found("User");
403 /// assert!(resp.error.as_ref().unwrap().message.contains("not found"));
404 /// ```
405 pub fn not_found(resource: impl Into<String>) -> Self {
406 Self::error("NOT_FOUND", format!("{} not found", resource.into()))
407 }
408
409 /// Create a not-found (404) error with a custom message.
410 ///
411 /// # Example
412 /// ```rust
413 /// use auth_framework::api::responses::ApiResponse;
414 ///
415 /// let resp = ApiResponse::<()>::not_found_with_message("deleted");
416 /// assert_eq!(resp.error.as_ref().unwrap().code, "NOT_FOUND");
417 /// ```
418 pub fn not_found_with_message(message: impl Into<String>) -> Self {
419 Self::error("NOT_FOUND", message)
420 }
421
422 /// Create an error response with a custom code and message (alias for [`error`](Self::error)).
423 ///
424 /// # Example
425 /// ```rust
426 /// use auth_framework::api::responses::ApiResponse;
427 ///
428 /// let resp = ApiResponse::<()>::error_with_message("LIMIT", "rate exceeded");
429 /// assert_eq!(resp.error.as_ref().unwrap().code, "LIMIT");
430 /// ```
431 pub fn error_with_message(code: impl Into<String>, message: impl Into<String>) -> Self {
432 Self::error(code, message)
433 }
434
435 /// Create an internal server error (500) response.
436 ///
437 /// # Example
438 /// ```rust
439 /// use auth_framework::api::responses::ApiResponse;
440 ///
441 /// let resp = ApiResponse::<()>::internal_error();
442 /// assert_eq!(resp.error.as_ref().unwrap().code, "SERVER_ERROR");
443 /// ```
444 pub fn internal_error() -> Self {
445 Self::error("SERVER_ERROR", "Internal server error")
446 }
447}
448
449impl<T> IntoResponse for ApiResponse<T>
450where
451 T: Serialize,
452{
453 fn into_response(self) -> Response {
454 let status = if self.success {
455 StatusCode::OK
456 } else {
457 match self.error.as_ref().map(|e| e.code.as_str()) {
458 Some("UNAUTHORIZED") => StatusCode::UNAUTHORIZED,
459 Some("FORBIDDEN") => StatusCode::FORBIDDEN,
460 Some("NOT_FOUND") => StatusCode::NOT_FOUND,
461 Some("VALIDATION_ERROR") => StatusCode::BAD_REQUEST,
462 Some("RATE_LIMITED") => StatusCode::TOO_MANY_REQUESTS,
463 // Authentication failures should be 401, not 500
464 Some(
465 "AUTHENTICATION_FAILED"
466 | "INVALID_CREDENTIALS"
467 | "AUTH_ERROR"
468 | "MFA_REQUIRED"
469 | "TOKEN_EXPIRED"
470 | "INVALID_TOKEN",
471 ) => StatusCode::UNAUTHORIZED,
472 // Client-side errors (bad input / missing resource)
473 Some("CONFLICT" | "DUPLICATE_USER") => StatusCode::CONFLICT,
474 Some("NOT_IMPLEMENTED") => StatusCode::NOT_IMPLEMENTED,
475 // RFC 6749 OAuth error codes (lowercase) and internal codes (uppercase)
476 Some(
477 "UNSUPPORTED_GRANT_TYPE"
478 | "UNSUPPORTED_RESPONSE_TYPE"
479 | "unsupported_grant_type"
480 | "unsupported_response_type"
481 | "invalid_grant"
482 | "invalid_request"
483 | "invalid_scope",
484 ) => StatusCode::BAD_REQUEST,
485 _ => StatusCode::INTERNAL_SERVER_ERROR,
486 }
487 };
488
489 (status, Json(self)).into_response()
490 }
491}
492
493/// Convert an [`AuthError`](crate::errors::AuthError) into an appropriate API error response.
494///
495/// Maps error variants to HTTP-semantic error codes:
496/// - `Token` → `INVALID_TOKEN`
497/// - `Validation` → `VALIDATION_ERROR`
498/// - `AuthMethod` → `INVALID_CREDENTIALS`
499/// - `UserNotFound` → `NOT_FOUND`
500/// - `Permission` → `FORBIDDEN`
501/// - `RateLimit` → `RATE_LIMITED`
502/// - everything else → `SERVER_ERROR`
503impl From<crate::errors::AuthError> for ApiResponse<()> {
504 fn from(error: crate::errors::AuthError) -> Self {
505 match &error {
506 crate::errors::AuthError::Token(_) => Self::error("INVALID_TOKEN", error.to_string()),
507 crate::errors::AuthError::Validation { .. } => {
508 Self::validation_error(error.to_string())
509 }
510 crate::errors::AuthError::AuthMethod { .. } => {
511 Self::error("INVALID_CREDENTIALS", error.to_string())
512 }
513 crate::errors::AuthError::UserNotFound => Self::not_found(error.to_string()),
514 crate::errors::AuthError::Permission(_) => Self::forbidden(),
515 crate::errors::AuthError::RateLimit { .. } => {
516 Self::error("RATE_LIMITED", error.to_string())
517 }
518 _ => Self::internal_error(),
519 }
520 }
521}