1use std::fmt;
2use std::path::PathBuf;
3use std::io;
4use std::error::Error as StdError;
5use std::backtrace::Backtrace;
6
7#[cfg(feature = "serde")]
8use serde::Serialize;
9
10pub type Result<T> = std::result::Result<T, crate::error::AppError>;
12
13pub trait ForgeError: std::error::Error + Send + Sync + 'static {
15 fn kind(&self) -> &'static str;
17
18 fn caption(&self) -> &'static str;
20
21 fn is_retryable(&self) -> bool {
23 false
24 }
25
26 fn is_fatal(&self) -> bool {
28 false
29 }
30
31 fn status_code(&self) -> u16 {
33 500
34 }
35
36 fn exit_code(&self) -> i32 {
38 1
39 }
40
41 fn user_message(&self) -> String {
43 self.to_string()
44 }
45
46 fn dev_message(&self) -> String {
48 format!("[{}] {}", self.kind(), self)
49 }
50
51 fn backtrace(&self) -> Option<&Backtrace> {
53 None
54 }
55
56 fn register(&self) {
58 crate::macros::call_error_hook(self.caption(), self.kind(), self.is_fatal(), self.is_retryable());
59 }
60}
61
62#[derive(Debug)]
64#[cfg_attr(feature = "serde", derive(Serialize))]
65pub enum AppError {
66 Config {
68 message: String,
69 retryable: bool,
70 fatal: bool,
71 status: u16,
72 },
73
74 Filesystem {
76 path: Option<PathBuf>,
77 #[cfg_attr(feature = "serde", serde(skip))]
78 source: io::Error,
79 retryable: bool,
80 fatal: bool,
81 status: u16,
82 },
83
84 Network {
86 endpoint: String,
87 #[cfg_attr(feature = "serde", serde(skip))]
88 source: Option<Box<dyn StdError + Send + Sync>>,
89 retryable: bool,
90 fatal: bool,
91 status: u16,
92 },
93
94 Other {
96 message: String,
97 retryable: bool,
98 fatal: bool,
99 status: u16,
100 },
101}
102
103impl std::fmt::Display for AppError {
104 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105 match self {
106 Self::Config { message, .. } => write!(f, "⚙️ Configuration Error: {message}"),
107 Self::Filesystem { path, source, .. } => {
108 if let Some(p) = path {
109 write!(f, "💾 Filesystem Error at {p:?}: {source}")
110 } else {
111 write!(f, "💾 Filesystem Error: {source}")
112 }
113 },
114 Self::Network { endpoint, source, .. } => {
115 if let Some(src) = source {
116 write!(f, "🌐 Network Error on {endpoint}: {src}")
117 } else {
118 write!(f, "🌐 Network Error on {endpoint}")
119 }
120 },
121 Self::Other { message, .. } => write!(f, "🚨 Error: {message}"),
122 }
123 }
124}
125
126impl std::error::Error for AppError {
127 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
128 match self {
129 AppError::Filesystem { source, .. } => Some(source),
130 AppError::Network { source: Some(src), .. } => Some(src.as_ref()),
131 _ => None,
132 }
133 }
134}
135
136impl From<io::Error> for AppError {
137 fn from(e: io::Error) -> Self {
138 AppError::Filesystem {
139 path: None,
140 source: e,
141 retryable: false,
142 fatal: true,
143 status: 500
144 }
145 }
146}
147
148impl ForgeError for AppError {
149 fn kind(&self) -> &'static str {
150 match self {
151 Self::Config { .. } => "Config",
152 Self::Filesystem { .. } => "Filesystem",
153 Self::Network { .. } => "Network",
154 Self::Other { .. } => "Other",
155 }
156 }
157
158 fn caption(&self) -> &'static str {
159 match self {
160 Self::Config { .. } => "⚙️ Configuration",
161 Self::Filesystem { .. } => "💾 Filesystem",
162 Self::Network { .. } => "🌐 Network",
163 Self::Other { .. } => "🚨 Error",
164 }
165 }
166
167 fn is_retryable(&self) -> bool {
168 match self {
169 Self::Config { retryable, .. } => *retryable,
170 Self::Filesystem { retryable, .. } => *retryable,
171 Self::Network { retryable, .. } => *retryable,
172 Self::Other { retryable, .. } => *retryable,
173 }
174 }
175
176 fn is_fatal(&self) -> bool {
177 match self {
178 Self::Config { fatal, .. } => *fatal,
179 Self::Filesystem { fatal, .. } => *fatal,
180 Self::Network { fatal, .. } => *fatal,
181 Self::Other { fatal, .. } => *fatal,
182 }
183 }
184
185 fn status_code(&self) -> u16 {
186 match self {
187 Self::Config { status, .. } => *status,
188 Self::Filesystem { status, .. } => *status,
189 Self::Network { status, .. } => *status,
190 Self::Other { status, .. } => *status,
191 }
192 }
193}
194
195impl AppError {
197 pub fn config(message: impl Into<String>) -> Self {
199 let instance = Self::Config {
200 message: message.into(),
201 retryable: false,
202 fatal: false,
203 status: 500,
204 };
205 crate::macros::call_error_hook(instance.caption(), instance.kind(), instance.is_fatal(), instance.is_retryable());
206 instance
207 }
208
209 pub fn filesystem(path: impl Into<String>, source: impl Into<Option<io::Error>>) -> Self {
211 let source = match source.into() {
213 Some(err) => err,
214 None => io::Error::other("File operation failed"),
215 };
216
217 let instance = Self::Filesystem {
218 path: Some(path.into().into()),
219 source,
220 retryable: false,
221 fatal: false,
222 status: 500,
223 };
224 crate::macros::call_error_hook(instance.caption(), instance.kind(), instance.is_fatal(), instance.is_retryable());
225 instance
226 }
227
228 pub fn filesystem_with_source(path: impl Into<PathBuf>, source: io::Error) -> Self {
230 let instance = Self::Filesystem {
231 path: Some(path.into()),
232 source,
233 retryable: false,
234 fatal: false,
235 status: 500,
236 };
237 crate::macros::call_error_hook(instance.caption(), instance.kind(), instance.is_fatal(), instance.is_retryable());
238 instance
239 }
240
241 pub fn network(endpoint: impl Into<String>, source: impl Into<Option<Box<dyn StdError + Send + Sync>>>) -> Self {
243 let source = source.into();
245
246 let instance = Self::Network {
247 endpoint: endpoint.into(),
248 source,
249 retryable: true,
250 fatal: false,
251 status: 503,
252 };
253 crate::macros::call_error_hook(instance.caption(), instance.kind(), instance.is_fatal(), instance.is_retryable());
254 instance
255 }
256
257 pub fn network_with_source(endpoint: impl Into<String>, source: Option<Box<dyn StdError + Send + Sync>>) -> Self {
259 let instance = Self::Network {
260 endpoint: endpoint.into(),
261 source,
262 retryable: true,
263 fatal: false,
264 status: 503,
265 };
266 crate::macros::call_error_hook(instance.caption(), instance.kind(), instance.is_fatal(), instance.is_retryable());
267 instance
268 }
269
270 pub fn other(message: impl Into<String>) -> Self {
272 let instance = Self::Other {
273 message: message.into(),
274 retryable: false,
275 fatal: false,
276 status: 500,
277 };
278 crate::macros::call_error_hook(instance.caption(), instance.kind(), instance.is_fatal(), instance.is_retryable());
279 instance
280 }
281
282 pub fn with_retryable(mut self, retryable: bool) -> Self {
284 match &mut self {
285 Self::Config { retryable: r, .. } => *r = retryable,
286 Self::Filesystem { retryable: r, .. } => *r = retryable,
287 Self::Network { retryable: r, .. } => *r = retryable,
288 Self::Other { retryable: r, .. } => *r = retryable,
289 }
290 self
291 }
292
293 pub fn with_fatal(mut self, fatal: bool) -> Self {
295 match &mut self {
296 Self::Config { fatal: f, .. } => *f = fatal,
297 Self::Filesystem { fatal: f, .. } => *f = fatal,
298 Self::Network { fatal: f, .. } => *f = fatal,
299 Self::Other { fatal: f, .. } => *f = fatal,
300 }
301 self
302 }
303
304 pub fn with_status(mut self, status: u16) -> Self {
306 match &mut self {
307 Self::Config { status: s, .. } => *s = status,
308 Self::Filesystem { status: s, .. } => *s = status,
309 Self::Network { status: s, .. } => *s = status,
310 Self::Other { status: s, .. } => *s = status,
311 }
312 self
313 }
314
315 pub fn with_code(self, code: impl Into<String>) -> crate::registry::CodedError<Self> {
317 crate::registry::CodedError::new(self, code.into())
318 }
319
320 pub fn context<C: std::fmt::Display + std::fmt::Debug + Send + Sync + 'static>(self, context: C) -> crate::context::ContextError<Self, C> {
322 crate::context::ContextError::new(self, context)
323 }
324}