use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
pub enum ErrorCode {
#[default]
Success = 0,
SyntaxError = 100,
ParseError = 101,
InvalidStatement = 102,
MissingParameter = 103,
ExecutionError = 200,
Timeout = 201,
ResourceExhausted = 202,
Conflict = 203,
Deadlock = 204,
ValidationError = 300,
TypeError = 301,
InvalidInput = 302,
ConstraintViolation = 303,
PermissionDenied = 400,
Unauthorized = 401,
Forbidden = 403,
ResourceNotFound = 500,
ResourceAlreadyExists = 501,
ResourceUnavailable = 502,
InternalError = 900,
ServiceUnavailable = 901,
Unknown = 999,
}
impl ErrorCode {
pub fn as_i32(&self) -> i32 {
*self as i32
}
pub fn from_i32(code: i32) -> Option<Self> {
match code {
0 => Some(ErrorCode::Success),
100 => Some(ErrorCode::SyntaxError),
101 => Some(ErrorCode::ParseError),
102 => Some(ErrorCode::InvalidStatement),
103 => Some(ErrorCode::MissingParameter),
200 => Some(ErrorCode::ExecutionError),
201 => Some(ErrorCode::Timeout),
202 => Some(ErrorCode::ResourceExhausted),
203 => Some(ErrorCode::Conflict),
204 => Some(ErrorCode::Deadlock),
300 => Some(ErrorCode::ValidationError),
301 => Some(ErrorCode::TypeError),
302 => Some(ErrorCode::InvalidInput),
303 => Some(ErrorCode::ConstraintViolation),
400 => Some(ErrorCode::PermissionDenied),
401 => Some(ErrorCode::Unauthorized),
403 => Some(ErrorCode::Forbidden),
500 => Some(ErrorCode::ResourceNotFound),
501 => Some(ErrorCode::ResourceAlreadyExists),
502 => Some(ErrorCode::ResourceUnavailable),
900 => Some(ErrorCode::InternalError),
901 => Some(ErrorCode::ServiceUnavailable),
999 => Some(ErrorCode::Unknown),
_ => None,
}
}
pub fn category(&self) -> ErrorCategory {
match self.as_i32() {
0 => ErrorCategory::Success,
100..=199 => ErrorCategory::Syntax,
200..=299 => ErrorCategory::Execution,
300..=399 => ErrorCategory::Validation,
400..=499 => ErrorCategory::Permission,
500..=599 => ErrorCategory::Resource,
900..=999 => ErrorCategory::System,
_ => ErrorCategory::Unknown,
}
}
pub fn default_message(&self) -> &'static str {
match self {
ErrorCode::Success => "success",
ErrorCode::SyntaxError => "syntax error",
ErrorCode::ParseError => "parse error",
ErrorCode::InvalidStatement => "invalid statement",
ErrorCode::MissingParameter => "missing required parameter",
ErrorCode::ExecutionError => "execution error",
ErrorCode::Timeout => "execution timeout",
ErrorCode::ResourceExhausted => "resource exhausted",
ErrorCode::Conflict => "conflict",
ErrorCode::Deadlock => "deadlock detected",
ErrorCode::ValidationError => "validation error",
ErrorCode::TypeError => "type error",
ErrorCode::InvalidInput => "invalid input",
ErrorCode::ConstraintViolation => "constraint violation",
ErrorCode::PermissionDenied => "permission denied",
ErrorCode::Unauthorized => "unauthorized",
ErrorCode::Forbidden => "forbidden",
ErrorCode::ResourceNotFound => "resource not found",
ErrorCode::ResourceAlreadyExists => "resource already exists",
ErrorCode::ResourceUnavailable => "resource unavailable",
ErrorCode::InternalError => "internal server error",
ErrorCode::ServiceUnavailable => "service unavailable",
ErrorCode::Unknown => "unknown error",
}
}
pub fn is_success(&self) -> bool {
matches!(self, ErrorCode::Success)
}
pub fn is_client_error(&self) -> bool {
let code = self.as_i32();
(100..=499).contains(&code)
}
pub fn is_server_error(&self) -> bool {
let code = self.as_i32();
(500..=599).contains(&code) || (900..=999).contains(&code)
}
pub fn is_retryable(&self) -> bool {
matches!(
self,
ErrorCode::Timeout
| ErrorCode::Conflict
| ErrorCode::Deadlock
| ErrorCode::ResourceExhausted
| ErrorCode::ServiceUnavailable
)
}
}
impl std::fmt::Display for ErrorCode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} ({})", self.as_i32(), self.default_message())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ErrorCategory {
Success,
Syntax,
Execution,
Validation,
Permission,
Resource,
System,
Unknown,
}
impl ErrorCategory {
pub fn to_http_status(&self) -> u16 {
match self {
ErrorCategory::Success => 200,
ErrorCategory::Syntax => 400,
ErrorCategory::Execution => 500,
ErrorCategory::Validation => 422,
ErrorCategory::Permission => 403,
ErrorCategory::Resource => 404,
ErrorCategory::System => 500,
ErrorCategory::Unknown => 500,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PublicError {
pub code: ErrorCode,
pub message: String,
}
impl PublicError {
pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
}
}
pub fn with_default_message(code: ErrorCode) -> Self {
Self {
code,
message: code.default_message().to_string(),
}
}
pub fn success() -> Self {
Self {
code: ErrorCode::Success,
message: "successes".to_string(),
}
}
}
pub trait ToPublicError {
fn to_public_error(&self) -> PublicError;
fn to_error_code(&self) -> ErrorCode;
fn to_public_message(&self) -> String;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_code_as_i32() {
assert_eq!(ErrorCode::Success.as_i32(), 0);
assert_eq!(ErrorCode::SyntaxError.as_i32(), 100);
assert_eq!(ErrorCode::InternalError.as_i32(), 900);
}
#[test]
fn test_error_code_from_i32() {
assert_eq!(ErrorCode::from_i32(0), Some(ErrorCode::Success));
assert_eq!(ErrorCode::from_i32(100), Some(ErrorCode::SyntaxError));
assert_eq!(ErrorCode::from_i32(999), Some(ErrorCode::Unknown));
assert_eq!(ErrorCode::from_i32(12345), None);
}
#[test]
fn test_error_code_category() {
assert_eq!(ErrorCode::Success.category(), ErrorCategory::Success);
assert_eq!(ErrorCode::SyntaxError.category(), ErrorCategory::Syntax);
assert_eq!(
ErrorCode::ExecutionError.category(),
ErrorCategory::Execution
);
assert_eq!(ErrorCode::InternalError.category(), ErrorCategory::System);
}
#[test]
fn test_error_code_is_success() {
assert!(ErrorCode::Success.is_success());
assert!(!ErrorCode::SyntaxError.is_success());
assert!(!ErrorCode::InternalError.is_success());
}
#[test]
fn test_error_code_is_retryable() {
assert!(ErrorCode::Timeout.is_retryable());
assert!(ErrorCode::Conflict.is_retryable());
assert!(ErrorCode::Deadlock.is_retryable());
assert!(!ErrorCode::SyntaxError.is_retryable());
assert!(!ErrorCode::PermissionDenied.is_retryable());
}
#[test]
fn test_public_error() {
let err = PublicError::new(
ErrorCode::ResourceNotFound,
"user does not exist".to_string(),
);
assert_eq!(err.code, ErrorCode::ResourceNotFound);
assert_eq!(err.message, "user does not exist");
let default_err = PublicError::with_default_message(ErrorCode::Timeout);
assert_eq!(default_err.code, ErrorCode::Timeout);
assert_eq!(default_err.message, "execution timeout");
}
}