Skip to main content

linkrs_core/error/
codes.rs

1//! Definition of external error codes
2//!
3//! This module defines a standardized error code system for the following purposes:
4//! Client response
5//! The API returns the data.
6//! Protocol serialization
7//!
8//! Error code format: XXYY
9//! XX: Error category (00 = Success, 01 = Syntax, 02 = Execution, 03 = Validation, 04 = Permissions, 05 = Resources, 09 = System)
10//! - YY: Specific error
11
12use serde::{Deserialize, Serialize};
13
14/// External error code – used for client responses
15///
16/// Design principles:
17/// Stability: Error codes should not be modified arbitrarily once they have been defined, in order to ensure compatibility with clients.
18/// 2. Simplification: Only the necessary error information is displayed, without any details about the internal implementation.
19/// 3. Standardization: Adherence to common error code design specifications such as those for HTTP/GraphQL
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
21pub enum ErrorCode {
22    // ==================== Success (00xx) ====================
23    #[default]
24    Success = 0,
25
26    // ==================== Grammar error (01xx) ====================
27    /// Common grammar errors
28    SyntaxError = 100,
29    /// Analysis error
30    ParseError = 101,
31    /// Invalid statement.
32    InvalidStatement = 102,
33    /// The necessary parameters are missing.
34    MissingParameter = 103,
35
36    // ==================== Execution error (02xx) ====================
37    /// General Execution Error
38    ExecutionError = 200,
39    /// The operation timed out.
40    Timeout = 201,
41    /// Insufficient resources
42    ResourceExhausted = 202,
43    /// Concurrency conflicts
44    Conflict = 203,
45    /// Deadlock detection
46    Deadlock = 204,
47
48    // Verification error (03xx)
49    /// General Verification Error
50    ValidationError = 300,
51    /// Type error
52    TypeError = 301,
53    /// Invalid input.
54    InvalidInput = 302,
55    /// Constraint violation
56    ConstraintViolation = 303,
57
58    // ==================== Permission error (04xx) ====================
59    /// Insufficient permissions
60    PermissionDenied = 400,
61    /// Unauthenticated
62    Unauthorized = 401,
63    /// Access prohibited.
64    Forbidden = 403,
65
66    // Resource error (05xx)
67    /// The resource was not found.
68    ResourceNotFound = 500,
69    /// The resource already exists.
70    ResourceAlreadyExists = 501,
71    /// The resource is not available.
72    ResourceUnavailable = 502,
73
74    // ==================== System Error (09xx) ====================
75    /// Internal server error
76    InternalError = 900,
77    /// The service is not available.
78    ServiceUnavailable = 901,
79    /// Unknown error
80    Unknown = 999,
81}
82
83impl ErrorCode {
84    /// Obtain the i32 value of the error code
85    pub fn as_i32(&self) -> i32 {
86        *self as i32
87    }
88
89    /// Retrieve the error code based on the i32 value.
90    pub fn from_i32(code: i32) -> Option<Self> {
91        match code {
92            0 => Some(ErrorCode::Success),
93            100 => Some(ErrorCode::SyntaxError),
94            101 => Some(ErrorCode::ParseError),
95            102 => Some(ErrorCode::InvalidStatement),
96            103 => Some(ErrorCode::MissingParameter),
97            200 => Some(ErrorCode::ExecutionError),
98            201 => Some(ErrorCode::Timeout),
99            202 => Some(ErrorCode::ResourceExhausted),
100            203 => Some(ErrorCode::Conflict),
101            204 => Some(ErrorCode::Deadlock),
102            300 => Some(ErrorCode::ValidationError),
103            301 => Some(ErrorCode::TypeError),
104            302 => Some(ErrorCode::InvalidInput),
105            303 => Some(ErrorCode::ConstraintViolation),
106            400 => Some(ErrorCode::PermissionDenied),
107            401 => Some(ErrorCode::Unauthorized),
108            403 => Some(ErrorCode::Forbidden),
109            500 => Some(ErrorCode::ResourceNotFound),
110            501 => Some(ErrorCode::ResourceAlreadyExists),
111            502 => Some(ErrorCode::ResourceUnavailable),
112            900 => Some(ErrorCode::InternalError),
113            901 => Some(ErrorCode::ServiceUnavailable),
114            999 => Some(ErrorCode::Unknown),
115            _ => None,
116        }
117    }
118
119    /// Obtain the error category
120    pub fn category(&self) -> ErrorCategory {
121        match self.as_i32() {
122            0 => ErrorCategory::Success,
123            100..=199 => ErrorCategory::Syntax,
124            200..=299 => ErrorCategory::Execution,
125            300..=399 => ErrorCategory::Validation,
126            400..=499 => ErrorCategory::Permission,
127            500..=599 => ErrorCategory::Resource,
128            900..=999 => ErrorCategory::System,
129            _ => ErrorCategory::Unknown,
130        }
131    }
132
133    /// Retrieve the default error message.
134    pub fn default_message(&self) -> &'static str {
135        match self {
136            ErrorCode::Success => "success",
137            ErrorCode::SyntaxError => "syntax error",
138            ErrorCode::ParseError => "parse error",
139            ErrorCode::InvalidStatement => "invalid statement",
140            ErrorCode::MissingParameter => "missing required parameter",
141            ErrorCode::ExecutionError => "execution error",
142            ErrorCode::Timeout => "execution timeout",
143            ErrorCode::ResourceExhausted => "resource exhausted",
144            ErrorCode::Conflict => "conflict",
145            ErrorCode::Deadlock => "deadlock detected",
146            ErrorCode::ValidationError => "validation error",
147            ErrorCode::TypeError => "type error",
148            ErrorCode::InvalidInput => "invalid input",
149            ErrorCode::ConstraintViolation => "constraint violation",
150            ErrorCode::PermissionDenied => "permission denied",
151            ErrorCode::Unauthorized => "unauthorized",
152            ErrorCode::Forbidden => "forbidden",
153            ErrorCode::ResourceNotFound => "resource not found",
154            ErrorCode::ResourceAlreadyExists => "resource already exists",
155            ErrorCode::ResourceUnavailable => "resource unavailable",
156            ErrorCode::InternalError => "internal server error",
157            ErrorCode::ServiceUnavailable => "service unavailable",
158            ErrorCode::Unknown => "unknown error",
159        }
160    }
161
162    /// Determine whether it represents a successful state.
163    pub fn is_success(&self) -> bool {
164        matches!(self, ErrorCode::Success)
165    }
166
167    /// Determine whether it is a client-side error (a 4xx error).
168    pub fn is_client_error(&self) -> bool {
169        let code = self.as_i32();
170        (100..=499).contains(&code)
171    }
172
173    /// Determine whether it is a server error (errors of the 5xx/9xx type).
174    pub fn is_server_error(&self) -> bool {
175        let code = self.as_i32();
176        (500..=599).contains(&code) || (900..=999).contains(&code)
177    }
178
179    /// Determine whether a failed attempt can be retried.
180    pub fn is_retryable(&self) -> bool {
181        matches!(
182            self,
183            ErrorCode::Timeout
184                | ErrorCode::Conflict
185                | ErrorCode::Deadlock
186                | ErrorCode::ResourceExhausted
187                | ErrorCode::ServiceUnavailable
188        )
189    }
190}
191
192impl std::fmt::Display for ErrorCode {
193    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
194        write!(f, "{} ({})", self.as_i32(), self.default_message())
195    }
196}
197
198/// error category
199#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
200pub enum ErrorCategory {
201    Success,
202    Syntax,
203    Execution,
204    Validation,
205    Permission,
206    Resource,
207    System,
208    Unknown,
209}
210
211impl ErrorCategory {
212    /// Gets the HTTP status code mapping of the category
213    pub fn to_http_status(&self) -> u16 {
214        match self {
215            ErrorCategory::Success => 200,
216            ErrorCategory::Syntax => 400,
217            ErrorCategory::Execution => 500,
218            ErrorCategory::Validation => 422,
219            ErrorCategory::Permission => 403,
220            ErrorCategory::Resource => 404,
221            ErrorCategory::System => 500,
222            ErrorCategory::Unknown => 500,
223        }
224    }
225}
226
227/// External error message-used to serialize into the response
228#[derive(Debug, Clone, Serialize, Deserialize)]
229pub struct PublicError {
230    /// error code
231    pub code: ErrorCode,
232    /// error message
233    pub message: String,
234}
235
236impl PublicError {
237    /// Create new external errors
238    pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
239        Self {
240            code,
241            message: message.into(),
242        }
243    }
244
245    /// Create errors using default messages
246    pub fn with_default_message(code: ErrorCode) -> Self {
247        Self {
248            code,
249            message: code.default_message().to_string(),
250        }
251    }
252
253    /// Create a successful response
254    pub fn success() -> Self {
255        Self {
256            code: ErrorCode::Success,
257            message: "successes".to_string(),
258        }
259    }
260}
261
262/// conversion trait of internal errors to external errors
263///
264/// Implementing this trait can convert internal errors into external errors and filter sensitive information
265pub trait ToPublicError {
266    /// Convert to external error
267    fn to_public_error(&self) -> PublicError;
268
269    /// Get external error codes
270    fn to_error_code(&self) -> ErrorCode;
271
272    /// Get external error messages (filter sensitive information)
273    fn to_public_message(&self) -> String;
274}
275
276#[cfg(test)]
277mod tests {
278    use super::*;
279
280    #[test]
281    fn test_error_code_as_i32() {
282        assert_eq!(ErrorCode::Success.as_i32(), 0);
283        assert_eq!(ErrorCode::SyntaxError.as_i32(), 100);
284        assert_eq!(ErrorCode::InternalError.as_i32(), 900);
285    }
286
287    #[test]
288    fn test_error_code_from_i32() {
289        assert_eq!(ErrorCode::from_i32(0), Some(ErrorCode::Success));
290        assert_eq!(ErrorCode::from_i32(100), Some(ErrorCode::SyntaxError));
291        assert_eq!(ErrorCode::from_i32(999), Some(ErrorCode::Unknown));
292        assert_eq!(ErrorCode::from_i32(12345), None);
293    }
294
295    #[test]
296    fn test_error_code_category() {
297        assert_eq!(ErrorCode::Success.category(), ErrorCategory::Success);
298        assert_eq!(ErrorCode::SyntaxError.category(), ErrorCategory::Syntax);
299        assert_eq!(
300            ErrorCode::ExecutionError.category(),
301            ErrorCategory::Execution
302        );
303        assert_eq!(ErrorCode::InternalError.category(), ErrorCategory::System);
304    }
305
306    #[test]
307    fn test_error_code_is_success() {
308        assert!(ErrorCode::Success.is_success());
309        assert!(!ErrorCode::SyntaxError.is_success());
310        assert!(!ErrorCode::InternalError.is_success());
311    }
312
313    #[test]
314    fn test_error_code_is_retryable() {
315        assert!(ErrorCode::Timeout.is_retryable());
316        assert!(ErrorCode::Conflict.is_retryable());
317        assert!(ErrorCode::Deadlock.is_retryable());
318        assert!(!ErrorCode::SyntaxError.is_retryable());
319        assert!(!ErrorCode::PermissionDenied.is_retryable());
320    }
321
322    #[test]
323    fn test_public_error() {
324        let err = PublicError::new(
325            ErrorCode::ResourceNotFound,
326            "user does not exist".to_string(),
327        );
328        assert_eq!(err.code, ErrorCode::ResourceNotFound);
329        assert_eq!(err.message, "user does not exist");
330
331        let default_err = PublicError::with_default_message(ErrorCode::Timeout);
332        assert_eq!(default_err.code, ErrorCode::Timeout);
333        assert_eq!(default_err.message, "execution timeout");
334    }
335}