anycms_core/
result.rs

1use crate::pagination::ResultPagination;
2use serde::{Deserialize, Serialize};
3use serde_json::Value;
4use thiserror::Error;
5
6// ============================================================
7// Error Types & Codes
8// ============================================================
9
10/// Standard error codes for API responses
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12#[repr(i32)]
13pub enum ErrorCode {
14    Success = 0,
15    BadRequest = 400,
16    Unauthorized = 401,
17    Forbidden = 403,
18    NotFound = 404,
19    Conflict = 409,
20    ValidationError = 422,
21    InternalError = 500,
22    NotImplemented = 501,
23    BadGateway = 502,
24    ServiceUnavailable = 503,
25}
26
27impl ErrorCode {
28    /// Get the integer representation of the error code
29    pub fn as_i32(self) -> i32 {
30        self as i32
31    }
32
33    /// Convert error code to HTTP status message
34    pub fn as_str(self) -> &'static str {
35        match self {
36            ErrorCode::Success => "Success",
37            ErrorCode::BadRequest => "Bad Request",
38            ErrorCode::Unauthorized => "Unauthorized",
39            ErrorCode::Forbidden => "Forbidden",
40            ErrorCode::NotFound => "Not Found",
41            ErrorCode::Conflict => "Conflict",
42            ErrorCode::ValidationError => "Unprocessable Entity",
43            ErrorCode::InternalError => "Internal Server Error",
44            ErrorCode::NotImplemented => "Not Implemented",
45            ErrorCode::BadGateway => "Bad Gateway",
46            ErrorCode::ServiceUnavailable => "Service Unavailable",
47        }
48    }
49
50    /// Try to convert an integer to an ErrorCode
51    pub fn from_i32(value: i32) -> Option<Self> {
52        match value {
53            0 => Some(ErrorCode::Success),
54            400 => Some(ErrorCode::BadRequest),
55            401 => Some(ErrorCode::Unauthorized),
56            403 => Some(ErrorCode::Forbidden),
57            404 => Some(ErrorCode::NotFound),
58            409 => Some(ErrorCode::Conflict),
59            422 => Some(ErrorCode::ValidationError),
60            500 => Some(ErrorCode::InternalError),
61            501 => Some(ErrorCode::NotImplemented),
62            502 => Some(ErrorCode::BadGateway),
63            503 => Some(ErrorCode::ServiceUnavailable),
64            _ => None,
65        }
66    }
67
68    /// Convert to Axum StatusCode (requires axum feature)
69    #[cfg(feature = "axum")]
70    pub fn to_axum_status(self) -> axum::http::StatusCode {
71        match self {
72            ErrorCode::Success => axum::http::StatusCode::OK,
73            ErrorCode::BadRequest => axum::http::StatusCode::BAD_REQUEST,
74            ErrorCode::Unauthorized => axum::http::StatusCode::UNAUTHORIZED,
75            ErrorCode::Forbidden => axum::http::StatusCode::FORBIDDEN,
76            ErrorCode::NotFound => axum::http::StatusCode::NOT_FOUND,
77            ErrorCode::Conflict => axum::http::StatusCode::CONFLICT,
78            ErrorCode::ValidationError => axum::http::StatusCode::UNPROCESSABLE_ENTITY,
79            ErrorCode::InternalError => axum::http::StatusCode::INTERNAL_SERVER_ERROR,
80            ErrorCode::NotImplemented => axum::http::StatusCode::NOT_IMPLEMENTED,
81            ErrorCode::BadGateway => axum::http::StatusCode::BAD_GATEWAY,
82            ErrorCode::ServiceUnavailable => axum::http::StatusCode::SERVICE_UNAVAILABLE,
83        }
84    }
85
86    /// Convert to Actix-web StatusCode (requires actix feature)
87    #[cfg(feature = "actix")]
88    pub fn to_actix_status(self) -> actix_web::http::StatusCode {
89        match self {
90            ErrorCode::Success => actix_web::http::StatusCode::OK,
91            ErrorCode::BadRequest => actix_web::http::StatusCode::BAD_REQUEST,
92            ErrorCode::Unauthorized => actix_web::http::StatusCode::UNAUTHORIZED,
93            ErrorCode::Forbidden => actix_web::http::StatusCode::FORBIDDEN,
94            ErrorCode::NotFound => actix_web::http::StatusCode::NOT_FOUND,
95            ErrorCode::Conflict => actix_web::http::StatusCode::CONFLICT,
96            ErrorCode::ValidationError => actix_web::http::StatusCode::UNPROCESSABLE_ENTITY,
97            ErrorCode::InternalError => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR,
98            ErrorCode::NotImplemented => actix_web::http::StatusCode::NOT_IMPLEMENTED,
99            ErrorCode::BadGateway => actix_web::http::StatusCode::BAD_GATEWAY,
100            ErrorCode::ServiceUnavailable => actix_web::http::StatusCode::SERVICE_UNAVAILABLE,
101        }
102    }
103}
104
105/// Standard API error with error code and message
106///
107/// This error type can be used directly or as a base for custom error types.
108///
109/// # Example
110/// ```rust
111/// use anycms_core::{ApiError, ErrorCode};
112///
113/// // Create error directly
114/// let err = ApiError::new(ErrorCode::NotFound, "User not found");
115///
116/// // Use convenience methods
117/// let err = ApiError::not_found("User not found");
118/// let err = ApiError::bad_request("Invalid email format");
119/// ```
120#[derive(Error, Debug)]
121pub struct ApiError {
122    pub code: ErrorCode,
123    pub message: String,
124}
125
126impl ApiError {
127    /// Create a new ApiError with the given code and message
128    pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
129        Self {
130            code,
131            message: message.into(),
132        }
133    }
134
135    /// Create a NotFound error (404)
136    pub fn not_found(message: impl Into<String>) -> Self {
137        Self::new(ErrorCode::NotFound, message)
138    }
139
140    /// Create a BadRequest error (400)
141    pub fn bad_request(message: impl Into<String>) -> Self {
142        Self::new(ErrorCode::BadRequest, message)
143    }
144
145    /// Create an Unauthorized error (401)
146    pub fn unauthorized(message: impl Into<String>) -> Self {
147        Self::new(ErrorCode::Unauthorized, message)
148    }
149
150    /// Create a Forbidden error (403)
151    pub fn forbidden(message: impl Into<String>) -> Self {
152        Self::new(ErrorCode::Forbidden, message)
153    }
154
155    /// Create a ValidationError error (422)
156    pub fn validation(message: impl Into<String>) -> Self {
157        Self::new(ErrorCode::ValidationError, message)
158    }
159
160    /// Create a Conflict error (409)
161    pub fn conflict(message: impl Into<String>) -> Self {
162        Self::new(ErrorCode::Conflict, message)
163    }
164
165    /// Create an InternalError (500)
166    pub fn internal(message: impl Into<String>) -> Self {
167        Self::new(ErrorCode::InternalError, message)
168    }
169
170    /// Get the error code
171    pub fn code(&self) -> ErrorCode {
172        self.code
173    }
174
175    /// Get reference to the error message
176    pub fn message(&self) -> &str {
177        &self.message
178    }
179}
180
181impl std::fmt::Display for ApiError {
182    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
183        write!(f, "[{}] {}", self.code.as_str(), self.message)
184    }
185}
186
187/// Generic application error that can represent any error type
188///
189/// This is a catch-all error type that can wrap any error that implements
190/// `std::error::Error + Send + Sync + 'static`.
191#[derive(Error, Debug)]
192pub enum AppError {
193    #[error("Resource not found: {0}")]
194    NotFound(String),
195
196    #[error("Bad request: {0}")]
197    BadRequest(String),
198
199    #[error("Unauthorized: {0}")]
200    Unauthorized(String),
201
202    #[error("Forbidden: {0}")]
203    Forbidden(String),
204
205    #[error("Conflict: {0}")]
206    Conflict(String),
207
208    #[error("Validation error: {0}")]
209    Validation(String),
210
211    #[error("Internal error: {0}")]
212    Internal(#[from] anyhow::Error),
213
214    #[error("Unknown error: {0}")]
215    Unknown(String),
216}
217
218impl AppError {
219    /// Convert AppError to ErrorCode
220    pub fn to_error_code(&self) -> ErrorCode {
221        match self {
222            AppError::NotFound(_) => ErrorCode::NotFound,
223            AppError::BadRequest(_) => ErrorCode::BadRequest,
224            AppError::Unauthorized(_) => ErrorCode::Unauthorized,
225            AppError::Forbidden(_) => ErrorCode::Forbidden,
226            AppError::Conflict(_) => ErrorCode::Conflict,
227            AppError::Validation(_) => ErrorCode::ValidationError,
228            AppError::Internal(_) => ErrorCode::InternalError,
229            AppError::Unknown(_) => ErrorCode::InternalError,
230        }
231    }
232}
233
234impl From<ApiError> for AppError {
235    fn from(err: ApiError) -> Self {
236        match err.code {
237            ErrorCode::NotFound => AppError::NotFound(err.message),
238            ErrorCode::BadRequest => AppError::BadRequest(err.message),
239            ErrorCode::Unauthorized => AppError::Unauthorized(err.message),
240            ErrorCode::Forbidden => AppError::Forbidden(err.message),
241            ErrorCode::Conflict => AppError::Conflict(err.message),
242            ErrorCode::ValidationError => AppError::Validation(err.message),
243            _ => AppError::Internal(anyhow::anyhow!(err.message)),
244        }
245    }
246}
247
248// ============================================================
249// Response Data Types
250// ============================================================
251
252/// Response data wrapper that can hold either a single value or a list
253///
254/// This enum uses `#[serde(untagged)]` to serialize directly as the contained value,
255/// allowing a single `data` field to represent both single values and lists.
256#[derive(Debug, Clone, Serialize, Deserialize)]
257#[serde(untagged)]
258pub enum ResponseData<T> {
259    /// A single value
260    Single(T),
261    /// A list of values
262    Multiple(Vec<T>),
263}
264
265impl<T> ResponseData<T> {
266    /// Create a single value response data
267    pub fn single(v: T) -> Self {
268        ResponseData::Single(v)
269    }
270
271    /// Create a multiple values response data
272    pub fn multiple(v: Vec<T>) -> Self {
273        ResponseData::Multiple(v)
274    }
275
276    /// Check if this is a single value
277    pub fn is_single(&self) -> bool {
278        matches!(self, ResponseData::Single(_))
279    }
280
281    /// Check if this is multiple values
282    pub fn is_multiple(&self) -> bool {
283        matches!(self, ResponseData::Multiple(_))
284    }
285
286    /// Get reference to the single value, if present
287    pub fn as_single(&self) -> Option<&T> {
288        match self {
289            ResponseData::Single(v) => Some(v),
290            ResponseData::Multiple(_) => None,
291        }
292    }
293
294    /// Get reference to the multiple values, if present
295    pub fn as_multiple(&self) -> Option<&Vec<T>> {
296        match self {
297            ResponseData::Single(_) => None,
298            ResponseData::Multiple(v) => Some(v),
299        }
300    }
301}
302
303// ============================================================
304// API Response Types
305// ============================================================
306
307/// API response wrapper with unified structure
308///
309/// # Fields
310/// - `success`: Indicates if the request was successful
311/// - `data`: Contains either a single value or a list (via `ResponseData`)
312/// - `message`: Optional message or error description
313/// - `code`: Optional error code
314/// - `pagination`: Optional pagination metadata for list responses
315/// - `extra`: Additional metadata as key-value pairs
316///
317/// # JSON Serialization Examples
318/// ```json
319/// // Single value
320/// { "success": true, "data": { "id": 1, "name": "Alice" } }
321///
322/// // List
323/// { "success": true, "data": [{ "id": 1 }, { "id": 2 }], "pagination": {...} }
324///
325/// // Empty success
326/// { "success": true }
327///
328/// // Error
329/// { "success": false, "message": "Not found", "code": 404 }
330/// ```
331#[derive(Debug, Serialize, Deserialize)]
332#[serde(rename_all = "camelCase")]
333pub struct ApiResult<T> {
334    pub success: bool,
335    #[serde(skip_serializing_if = "Option::is_none")]
336    pub data: Option<ResponseData<T>>,
337    #[serde(skip_serializing_if = "Option::is_none")]
338    pub message: Option<String>,
339    #[serde(skip_serializing_if = "Option::is_none")]
340    pub code: Option<i32>,
341    #[serde(skip_serializing_if = "Option::is_none")]
342    pub pagination: Option<ResultPagination>,
343    #[serde(skip_serializing_if = "Option::is_none")]
344    pub extra: Option<Value>,
345}
346
347impl<T> ApiResult<T> {
348    /// Create a successful response with a single value
349    pub fn value(v: T) -> Self {
350        ApiResult {
351            success: true,
352            data: Some(ResponseData::single(v)),
353            message: None,
354            code: None,
355            pagination: None,
356            extra: None,
357        }
358    }
359
360    /// Create a successful response with a list of values
361    pub fn list(v: Vec<T>) -> Self {
362        ApiResult {
363            success: true,
364            data: Some(ResponseData::multiple(v)),
365            message: None,
366            code: None,
367            pagination: None,
368            extra: None,
369        }
370    }
371
372    /// Create a failed response with a message (alias for `failure`)
373    pub fn fail(message: &str) -> Self {
374        Self::failure(message)
375    }
376
377    /// Create a failed response with a message
378    pub fn failure(message: &str) -> Self {
379        ApiResult {
380            success: false,
381            data: None,
382            message: Some(message.to_string()),
383            code: None,
384            pagination: None,
385            extra: None,
386        }
387    }
388
389    /// Create a successful response without data
390    ///
391    /// Alias for `ok()` - use `ok()` for clarity to avoid confusion with the `success` field
392    pub fn success() -> Self {
393        Self::ok()
394    }
395
396    /// Create a successful response without data (recommended method name)
397    pub fn ok() -> Self {
398        ApiResult {
399            success: true,
400            data: None,
401            message: None,
402            code: None,
403            pagination: None,
404            extra: None,
405        }
406    }
407
408    /// Add extra metadata to the response
409    ///
410    /// # Example
411    /// ```rust
412    /// use anycms_core::ApiResult;
413    /// use serde_json::json;
414    ///
415    /// let result: ApiResult<()> = ApiResult::ok()
416    ///     .with_extra("timestamp", json!(1234567890))
417    ///     .with_extra("version", json!("1.0.0"));
418    /// ```
419    pub fn with_extra(mut self, key: &str, value: Value) -> Self {
420        match self.extra {
421            Some(ref mut v) => {
422                v[key] = value;
423            }
424            None => {
425                let mut v = serde_json::Map::new();
426                v.insert(key.to_string(), value);
427                self.extra = Some(v.into());
428            }
429        }
430        self
431    }
432
433    /// Set error code for the response
434    pub fn with_code(mut self, code: i32) -> Self {
435        self.code = Some(code);
436        self
437    }
438
439    /// Set pagination metadata for list responses
440    pub fn with_pagination(mut self, pagination: ResultPagination) -> Self {
441        self.pagination = Some(pagination);
442        self
443    }
444
445    /// Set message for the response
446    pub fn with_message(mut self, message: &str) -> Self {
447        self.message = Some(message.to_string());
448        self
449    }
450}
451
452/// Conversion into Result for convenient error handling
453impl<T, E> Into<Result<ApiResult<T>, E>> for ApiResult<T> {
454    fn into(self) -> Result<ApiResult<T>, E> {
455        Ok(self)
456    }
457}
458
459// ============================================================
460// From Implementations for Error Conversion
461// ============================================================
462
463impl<T> From<ApiError> for ApiResult<T> {
464    fn from(err: ApiError) -> Self {
465        ApiResult {
466            success: false,
467            data: None,
468            message: Some(err.message),
469            code: Some(err.code.as_i32()),
470            pagination: None,
471            extra: None,
472        }
473    }
474}
475
476impl<T> From<AppError> for ApiResult<T> {
477    fn from(err: AppError) -> Self {
478        ApiResult {
479            success: false,
480            data: None,
481            message: Some(err.to_string()),
482            code: Some(err.to_error_code().as_i32()),
483            pagination: None,
484            extra: None,
485        }
486    }
487}
488
489impl<T> From<anyhow::Error> for ApiResult<T> {
490    fn from(err: anyhow::Error) -> Self {
491        ApiResult {
492            success: false,
493            data: None,
494            message: Some(err.to_string()),
495            code: Some(ErrorCode::InternalError.as_i32()),
496            pagination: None,
497            extra: None,
498        }
499    }
500}
501
502/// Convert any `Result<T, E>` where E implements Display to `ApiResult<T>`
503impl<T, E: std::fmt::Display + std::fmt::Debug> From<Result<T, E>> for ApiResult<T> {
504    fn from(result: Result<T, E>) -> Self {
505        match result {
506            Ok(data) => ApiResult::value(data),
507            Err(err) => ApiResult {
508                success: false,
509                data: None,
510                message: Some(format!("{}", err)),
511                code: Some(ErrorCode::InternalError.as_i32()),
512                pagination: None,
513                extra: None,
514            },
515        }
516    }
517}
518
519/// Helper trait for converting Results to ApiResults with custom error handling
520pub trait IntoApiResult<T> {
521    /// Convert a Result to ApiResult, mapping errors with a provided function
522    fn into_api_result_with<F>(self, f: F) -> ApiResult<T>
523    where
524        F: FnOnce() -> ApiError;
525
526    /// Convert a Result to ApiResult with default error handling
527    fn into_api_result(self) -> ApiResult<T>;
528}
529
530// Blanket implementation for any Result where Error can be converted to ApiError
531impl<T, E: Into<ApiError>> IntoApiResult<T> for Result<T, E> {
532    fn into_api_result_with<F>(self, f: F) -> ApiResult<T>
533    where
534        F: FnOnce() -> ApiError,
535    {
536        match self {
537            Ok(data) => ApiResult::value(data),
538            Err(_) => ApiResult::from(f()),
539        }
540    }
541
542    fn into_api_result(self) -> ApiResult<T> {
543        match self {
544            Ok(data) => ApiResult::value(data),
545            Err(err) => ApiResult::from(err.into()),
546        }
547    }
548}
549
550/// Type alias for standard Result with `Box<dyn Error>`
551pub type DefaultResult<T> = Result<T, Box<dyn std::error::Error>>;
552
553/// Type alias for Result with anyhow::Error
554pub type AnyhowResult<T> = Result<T, anyhow::Error>;
555
556/// API result without any data payload
557///
558/// Use this for endpoints that only return success/failure status
559/// or endpoints that only use `extra` metadata without structured data.
560///
561/// # Example
562/// ```rust
563/// use anycms_core::EmptyResult;
564///
565/// // Simple success response
566/// let result: EmptyResult = EmptyResult::ok();
567///
568/// // Error response
569/// let result: EmptyResult = EmptyResult::failure("Operation failed");
570/// ```
571pub type EmptyResult = ApiResult<()>;