Skip to main content

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}