Skip to main content

anycms_core/
result.rs

1use crate::pagination::ResultPagination;
2use serde::{Deserialize, Serialize};
3use serde_json::Value;
4
5// ============================================================
6// Error Codes
7// ============================================================
8
9/// Standard error codes for API responses
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11#[repr(i32)]
12#[non_exhaustive]
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}
23
24impl ErrorCode {
25    /// Get the integer representation of the error code
26    pub fn as_i32(self) -> i32 {
27        self as i32
28    }
29
30    /// Convert error code to HTTP status message
31    pub fn as_str(self) -> &'static str {
32        match self {
33            ErrorCode::Success => "Success",
34            ErrorCode::BadRequest => "Bad Request",
35            ErrorCode::Unauthorized => "Unauthorized",
36            ErrorCode::Forbidden => "Forbidden",
37            ErrorCode::NotFound => "Not Found",
38            ErrorCode::Conflict => "Conflict",
39            ErrorCode::ValidationError => "Unprocessable Entity",
40            ErrorCode::InternalError => "Internal Server Error",
41        }
42    }
43
44    /// Try to convert an integer to an ErrorCode
45    pub fn from_i32(value: i32) -> Option<Self> {
46        match value {
47            0 => Some(ErrorCode::Success),
48            400 => Some(ErrorCode::BadRequest),
49            401 => Some(ErrorCode::Unauthorized),
50            403 => Some(ErrorCode::Forbidden),
51            404 => Some(ErrorCode::NotFound),
52            409 => Some(ErrorCode::Conflict),
53            422 => Some(ErrorCode::ValidationError),
54            500 => Some(ErrorCode::InternalError),
55            _ => None,
56        }
57    }
58}
59
60// ============================================================
61// Field-level Validation Errors
62// ============================================================
63
64/// A single field-level validation error
65#[derive(Debug, Clone, Serialize, Deserialize)]
66#[cfg_attr(not(feature = "snake-case"), serde(rename_all = "camelCase"))]
67#[cfg_attr(feature = "snake-case", serde(rename_all = "snake_case"))]
68pub struct FieldError {
69    pub field: String,
70    pub message: String,
71}
72
73impl FieldError {
74    pub fn new(field: impl Into<String>, message: impl Into<String>) -> Self {
75        Self {
76            field: field.into(),
77            message: message.into(),
78        }
79    }
80}
81
82// ============================================================
83// Response Data Types
84// ============================================================
85
86/// Response data wrapper that can hold either a single value or a list
87#[derive(Debug, Clone, Serialize, Deserialize, Default)]
88#[serde(untagged)]
89#[non_exhaustive]
90pub enum ResponseData<T> {
91    /// A single value
92    Single(T),
93    /// A list of values
94    Multiple(Vec<T>),
95    /// No data
96    #[default]
97    Empty,
98}
99
100impl<T> ResponseData<T> {
101    /// Create a single value response data
102    pub fn single(v: T) -> Self {
103        ResponseData::Single(v)
104    }
105
106    /// Create a multiple values response data
107    pub fn multiple(v: Vec<T>) -> Self {
108        ResponseData::Multiple(v)
109    }
110
111    /// Check if this is a single value
112    pub fn is_single(&self) -> bool {
113        matches!(self, ResponseData::Single(_))
114    }
115
116    /// Check if this is multiple values
117    pub fn is_multiple(&self) -> bool {
118        matches!(self, ResponseData::Multiple(_))
119    }
120
121    /// Check if this is empty
122    pub fn is_empty(&self) -> bool {
123        matches!(self, ResponseData::Empty)
124    }
125
126    /// Get reference to the single value, if present
127    pub fn as_single(&self) -> Option<&T> {
128        match self {
129            ResponseData::Single(v) => Some(v),
130            _ => None,
131        }
132    }
133
134    /// Get reference to the multiple values, if present
135    pub fn as_multiple(&self) -> Option<&Vec<T>> {
136        match self {
137            ResponseData::Multiple(v) => Some(v),
138            _ => None,
139        }
140    }
141
142    /// Number of items in the data: 1 for `Single`, vec len for `Multiple`, 0 for `Empty`
143    #[inline]
144    pub fn len(&self) -> usize {
145        match self {
146            ResponseData::Single(_) => 1,
147            ResponseData::Multiple(v) => v.len(),
148            ResponseData::Empty => 0,
149        }
150    }
151
152    /// Whether the data carries no items.
153    ///
154    /// Different from `is_empty()` which checks the `Empty` variant.
155    /// `Multiple(vec![])` has `is_empty_data() == true` but `is_empty() == false`.
156    #[inline]
157    #[allow(clippy::len_zero)]
158    pub fn is_empty_data(&self) -> bool {
159        self.len() == 0
160    }
161}
162
163impl<T> From<T> for ResponseData<T> {
164    fn from(v: T) -> Self {
165        ResponseData::Single(v)
166    }
167}
168
169impl<T> From<Vec<T>> for ResponseData<T> {
170    fn from(v: Vec<T>) -> Self {
171        ResponseData::Multiple(v)
172    }
173}
174
175// ============================================================
176// API Response Types
177// ============================================================
178
179/// API response wrapper with unified structure
180#[derive(Debug, Clone, Serialize, Deserialize)]
181#[cfg_attr(not(feature = "snake-case"), serde(rename_all = "camelCase"))]
182#[cfg_attr(feature = "snake-case", serde(rename_all = "snake_case"))]
183pub struct ApiResult<T> {
184    pub success: bool,
185    #[serde(skip_serializing_if = "Option::is_none")]
186    pub data: Option<ResponseData<T>>,
187    #[serde(skip_serializing_if = "Option::is_none")]
188    pub message: Option<String>,
189    #[serde(skip_serializing_if = "Option::is_none")]
190    pub code: Option<i32>,
191    #[serde(skip_serializing_if = "Option::is_none")]
192    pub pagination: Option<ResultPagination>,
193    #[serde(skip_serializing_if = "Option::is_none")]
194    pub errors: Option<Vec<FieldError>>,
195    #[serde(skip_serializing_if = "Option::is_none")]
196    pub trace_id: Option<String>,
197    #[serde(skip_serializing_if = "Option::is_none")]
198    pub biz_code: Option<i32>,
199    #[serde(skip_serializing_if = "Option::is_none")]
200    pub timestamp: Option<i64>,
201    #[serde(skip_serializing_if = "Option::is_none")]
202    pub extra: Option<Value>,
203}
204
205impl<T> ApiResult<T> {
206    // ── Internal constructor ──
207
208    /// Internal constructor that creates the base struct with all optional fields as None.
209    #[inline]
210    fn new(
211        success: bool,
212        data: Option<ResponseData<T>>,
213        message: Option<String>,
214        code: Option<i32>,
215    ) -> Self {
216        ApiResult {
217            success,
218            data,
219            message,
220            code,
221            pagination: None,
222            errors: None,
223            trace_id: None,
224            biz_code: None,
225            timestamp: None,
226            extra: None,
227        }
228    }
229
230    // ── Factory methods ──
231
232    /// Create a successful response with a single value
233    #[inline]
234    pub fn value(v: T) -> Self {
235        Self::new(true, Some(ResponseData::single(v)), None, None)
236    }
237
238    /// Create a successful response with a list of values
239    #[inline]
240    pub fn list(v: Vec<T>) -> Self {
241        Self::new(true, Some(ResponseData::multiple(v)), None, None)
242    }
243
244    /// Create a failed response with a message (alias for `failure`)
245    #[inline]
246    pub fn fail(message: &str) -> Self {
247        Self::failure(message)
248    }
249
250    /// Create a failed response with a message
251    #[inline]
252    pub fn failure(message: &str) -> Self {
253        Self::new(false, None, Some(message.to_string()), None)
254    }
255
256    /// Create a successful response without data
257    ///
258    /// Alias for `ok()` - use `ok()` for clarity
259    #[inline]
260    pub fn success() -> Self {
261        Self::ok()
262    }
263
264    /// Create a successful response without data (recommended method name)
265    #[inline]
266    pub fn ok() -> Self {
267        Self::new(true, None, None, None)
268    }
269
270    /// Create a validation error response with field-level details
271    pub fn validation_errors(errors: Vec<FieldError>) -> Self {
272        let mut r = Self::new(
273            false,
274            None,
275            Some("Validation failed".to_string()),
276            Some(ErrorCode::ValidationError as i32),
277        );
278        r.errors = Some(errors);
279        r
280    }
281
282    /// Convert a `Result<T, E>` into `ApiResult<T>`, mapping all errors to `InternalError`
283    pub fn from_result<E: std::fmt::Display>(result: Result<T, E>) -> Self {
284        match result {
285            Ok(data) => Self::value(data),
286            Err(err) => Self::new(
287                false,
288                None,
289                Some(err.to_string()),
290                Some(ErrorCode::InternalError as i32),
291            ),
292        }
293    }
294
295    // ── Query methods ──
296
297    /// Whether this is a successful response
298    #[inline]
299    pub fn is_success(&self) -> bool {
300        self.success
301    }
302
303    /// Whether this is an error response
304    #[inline]
305    pub fn is_error(&self) -> bool {
306        !self.success
307    }
308
309    /// Get the error code if this is an error response
310    pub fn error_code(&self) -> Option<ErrorCode> {
311        if !self.success {
312            self.code.and_then(ErrorCode::from_i32)
313        } else {
314            None
315        }
316    }
317
318    // ── Builder methods ──
319
320    /// Add extra metadata to the response
321    #[inline]
322    pub fn with_extra(mut self, key: &str, value: Value) -> Self {
323        match self.extra {
324            Some(ref mut v) => {
325                v[key] = value;
326            }
327            None => {
328                let mut v = serde_json::Map::new();
329                v.insert(key.to_string(), value);
330                self.extra = Some(v.into());
331            }
332        }
333        self
334    }
335
336    /// Set error code for the response
337    #[inline]
338    pub fn with_code(mut self, code: i32) -> Self {
339        self.code = Some(code);
340        self
341    }
342
343    /// Set pagination metadata for list responses
344    #[inline]
345    pub fn with_pagination(mut self, pagination: ResultPagination) -> Self {
346        self.pagination = Some(pagination);
347        self
348    }
349
350    /// Set message for the response
351    #[inline]
352    pub fn with_message(mut self, message: &str) -> Self {
353        self.message = Some(message.to_string());
354        self
355    }
356
357    /// Set field-level validation errors
358    #[inline]
359    pub fn with_errors(mut self, errors: Vec<FieldError>) -> Self {
360        self.errors = Some(errors);
361        self
362    }
363
364    /// Add a single field-level validation error
365    #[inline]
366    pub fn with_error(mut self, field: impl Into<String>, message: impl Into<String>) -> Self {
367        match self.errors {
368            Some(ref mut v) => v.push(FieldError::new(field, message)),
369            None => self.errors = Some(vec![FieldError::new(field, message)]),
370        }
371        self
372    }
373
374    /// Set request trace ID
375    #[inline]
376    pub fn with_trace_id(mut self, id: impl Into<String>) -> Self {
377        self.trace_id = Some(id.into());
378        self
379    }
380
381    /// Set business error code (independent of HTTP status code)
382    #[inline]
383    pub fn with_biz_code(mut self, code: i32) -> Self {
384        self.biz_code = Some(code);
385        self
386    }
387
388    /// Set timestamp (Unix milliseconds)
389    #[inline]
390    pub fn with_timestamp(mut self, ts: i64) -> Self {
391        self.timestamp = Some(ts);
392        self
393    }
394
395    /// Set timestamp to current time automatically
396    pub fn with_current_timestamp(self) -> Self {
397        use std::time::{SystemTime, UNIX_EPOCH};
398        let ts = SystemTime::now()
399            .duration_since(UNIX_EPOCH)
400            .map(|d| d.as_millis() as i64)
401            .unwrap_or(0);
402        self.with_timestamp(ts)
403    }
404}
405
406// ============================================================
407// ApiResult -> Result conversion (used by downstream handlers)
408// ============================================================
409
410/// Convert `ApiResult<T>` into `Result<ApiResult<T>, E>` for any error type `E`.
411///
412/// This allows `ApiResult::value(data).into()` to work as a shorthand for
413/// `Ok(ApiResult::value(data))` in handlers returning `DefaultResult<impl Responder>`.
414///
415/// The conversion always produces `Ok` — the `Err` variant is never used because
416/// `ApiResult` itself encodes success/failure in its `success` field.
417impl<T, E> From<ApiResult<T>> for Result<ApiResult<T>, E> {
418    fn from(val: ApiResult<T>) -> Self {
419        Ok(val)
420    }
421}
422
423// ============================================================
424// From Implementations for Error Conversion
425// ============================================================
426
427impl<T> From<anyhow::Error> for ApiResult<T> {
428    fn from(err: anyhow::Error) -> Self {
429        Self::new(
430            false,
431            None,
432            Some(err.to_string()),
433            Some(ErrorCode::InternalError as i32),
434        )
435    }
436}
437
438impl<T> From<std::io::Error> for ApiResult<T> {
439    fn from(err: std::io::Error) -> Self {
440        Self::new(
441            false,
442            None,
443            Some(err.to_string()),
444            Some(ErrorCode::InternalError as i32),
445        )
446    }
447}
448
449impl<T> From<std::string::FromUtf8Error> for ApiResult<T> {
450    fn from(err: std::string::FromUtf8Error) -> Self {
451        Self::new(
452            false,
453            None,
454            Some(err.to_string()),
455            Some(ErrorCode::InternalError as i32),
456        )
457    }
458}
459
460/// Convert any `Result<T, E>` where E implements Display to `ApiResult<T>`
461impl<T, E: std::fmt::Display + std::fmt::Debug> From<Result<T, E>> for ApiResult<T> {
462    fn from(result: Result<T, E>) -> Self {
463        match result {
464            Ok(data) => Self::value(data),
465            Err(err) => Self::new(
466                false,
467                None,
468                Some(format!("{}", err)),
469                Some(ErrorCode::InternalError as i32),
470            ),
471        }
472    }
473}
474
475/// Type alias for standard Result with `Box<dyn Error>`.
476///
477/// Commonly used as the return type for API handlers:
478/// `-> DefaultResult<impl Responder>` where `Ok(ApiResult::value(data))` is returned.
479pub type DefaultResult<T> = Result<T, Box<dyn std::error::Error>>;