1use serde::{Deserialize, Serialize};
2use std::error::Error;
3use std::fmt::{Debug, Display};
4use thiserror::Error;
5
6pub type AppResult<T> = Result<T, AppError>;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
9#[serde(rename_all = "snake_case")]
10pub enum ErrorCode {
11 Validation,
12 Unauthorized,
13 Forbidden,
14 NotFound,
15 Conflict,
16 RateLimited,
17 ExternalDependency,
18 Internal,
19}
20
21impl ErrorCode {
22 pub fn as_str(self) -> &'static str {
23 match self {
24 Self::Validation => "validation_failed",
25 Self::Unauthorized => "unauthorized",
26 Self::Forbidden => "forbidden",
27 Self::NotFound => "not_found",
28 Self::Conflict => "conflict",
29 Self::RateLimited => "rate_limited",
30 Self::ExternalDependency => "external_dependency_failure",
31 Self::Internal => "internal_error",
32 }
33 }
34}
35
36#[derive(Error)]
37pub struct AppError {
38 pub code: ErrorCode,
39 pub public_message: String,
40 pub retryable: bool,
41 pub details: Vec<ErrorDetail>,
42 #[source]
43 source: Option<Box<dyn Error + Send + Sync>>,
44}
45
46impl Debug for AppError {
47 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 formatter
49 .debug_struct("AppError")
50 .field("code", &self.code)
51 .field("public_message", &self.public_message)
52 .field("retryable", &self.retryable)
53 .field("details", &self.details)
54 .finish_non_exhaustive()
55 }
56}
57
58impl Display for AppError {
59 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60 write!(formatter, "{}: {}", self.code.as_str(), self.public_message)
61 }
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
65pub struct ErrorDetail {
66 pub field: Option<String>,
67 pub reason: String,
68}
69
70impl AppError {
71 pub fn new(code: ErrorCode, public_message: impl Into<String>) -> Self {
72 Self {
73 code,
74 public_message: public_message.into(),
75 retryable: false,
76 details: Vec::new(),
77 source: None,
78 }
79 }
80
81 pub fn validation(public_message: impl Into<String>, details: Vec<ErrorDetail>) -> Self {
82 Self {
83 code: ErrorCode::Validation,
84 public_message: public_message.into(),
85 retryable: false,
86 details,
87 source: None,
88 }
89 }
90
91 pub fn with_source(mut self, source: impl Error + Send + Sync + 'static) -> Self {
92 self.source = Some(Box::new(source));
93 self
94 }
95
96 pub fn retryable(mut self) -> Self {
97 self.retryable = true;
98 self
99 }
100}