Skip to main content

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