1use std::backtrace::Backtrace;
2use std::error::Error as StdError;
3use std::fmt;
4use std::io;
5use std::path::PathBuf;
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(
59 self.caption(),
60 self.kind(),
61 self.is_fatal(),
62 self.is_retryable(),
63 );
64 }
65}
66
67#[derive(Debug)]
69#[cfg_attr(feature = "serde", derive(Serialize))]
70pub enum AppError {
71 Config {
73 message: String,
74 retryable: bool,
75 fatal: bool,
76 status: u16,
77 },
78
79 Filesystem {
81 path: Option<PathBuf>,
82 #[cfg_attr(feature = "serde", serde(skip))]
83 source: io::Error,
84 retryable: bool,
85 fatal: bool,
86 status: u16,
87 },
88
89 Network {
91 endpoint: String,
92 #[cfg_attr(feature = "serde", serde(skip))]
93 source: Option<Box<dyn StdError + Send + Sync>>,
94 retryable: bool,
95 fatal: bool,
96 status: u16,
97 },
98
99 Other {
101 message: String,
102 retryable: bool,
103 fatal: bool,
104 status: u16,
105 },
106}
107
108impl std::fmt::Display for AppError {
109 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110 match self {
111 Self::Config { message, .. } => write!(f, "⚙️ Configuration Error: {message}"),
112 Self::Filesystem { path, source, .. } => {
113 if let Some(p) = path {
114 write!(f, "💾 Filesystem Error at {p:?}: {source}")
115 } else {
116 write!(f, "💾 Filesystem Error: {source}")
117 }
118 }
119 Self::Network {
120 endpoint, source, ..
121 } => {
122 if let Some(src) = source {
123 write!(f, "🌐 Network Error on {endpoint}: {src}")
124 } else {
125 write!(f, "🌐 Network Error on {endpoint}")
126 }
127 }
128 Self::Other { message, .. } => write!(f, "🚨 Error: {message}"),
129 }
130 }
131}
132
133impl std::error::Error for AppError {
134 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
135 match self {
136 AppError::Filesystem { source, .. } => Some(source),
137 AppError::Network {
138 source: Some(src), ..
139 } => Some(src.as_ref()),
140 _ => None,
141 }
142 }
143}
144
145impl From<io::Error> for AppError {
146 fn from(e: io::Error) -> Self {
147 AppError::Filesystem {
148 path: None,
149 source: e,
150 retryable: false,
151 fatal: true,
152 status: 500,
153 }
154 }
155}
156
157impl ForgeError for AppError {
158 fn kind(&self) -> &'static str {
159 match self {
160 Self::Config { .. } => "Config",
161 Self::Filesystem { .. } => "Filesystem",
162 Self::Network { .. } => "Network",
163 Self::Other { .. } => "Other",
164 }
165 }
166
167 fn caption(&self) -> &'static str {
168 match self {
169 Self::Config { .. } => "⚙️ Configuration",
170 Self::Filesystem { .. } => "💾 Filesystem",
171 Self::Network { .. } => "🌐 Network",
172 Self::Other { .. } => "🚨 Error",
173 }
174 }
175
176 fn is_retryable(&self) -> bool {
177 match self {
178 Self::Config { retryable, .. } => *retryable,
179 Self::Filesystem { retryable, .. } => *retryable,
180 Self::Network { retryable, .. } => *retryable,
181 Self::Other { retryable, .. } => *retryable,
182 }
183 }
184
185 fn is_fatal(&self) -> bool {
186 match self {
187 Self::Config { fatal, .. } => *fatal,
188 Self::Filesystem { fatal, .. } => *fatal,
189 Self::Network { fatal, .. } => *fatal,
190 Self::Other { fatal, .. } => *fatal,
191 }
192 }
193
194 fn status_code(&self) -> u16 {
195 match self {
196 Self::Config { status, .. } => *status,
197 Self::Filesystem { status, .. } => *status,
198 Self::Network { status, .. } => *status,
199 Self::Other { status, .. } => *status,
200 }
201 }
202}
203
204impl AppError {
206 pub fn config(message: impl Into<String>) -> Self {
208 let instance = Self::Config {
209 message: message.into(),
210 retryable: false,
211 fatal: false,
212 status: 500,
213 };
214 crate::macros::call_error_hook(
215 instance.caption(),
216 instance.kind(),
217 instance.is_fatal(),
218 instance.is_retryable(),
219 );
220 instance
221 }
222
223 pub fn filesystem(path: impl Into<String>, source: impl Into<Option<io::Error>>) -> Self {
225 let source = match source.into() {
227 Some(err) => err,
228 None => io::Error::other("File operation failed"),
229 };
230
231 let instance = Self::Filesystem {
232 path: Some(path.into().into()),
233 source,
234 retryable: false,
235 fatal: false,
236 status: 500,
237 };
238 crate::macros::call_error_hook(
239 instance.caption(),
240 instance.kind(),
241 instance.is_fatal(),
242 instance.is_retryable(),
243 );
244 instance
245 }
246
247 pub fn filesystem_with_source(path: impl Into<PathBuf>, source: io::Error) -> Self {
249 let instance = Self::Filesystem {
250 path: Some(path.into()),
251 source,
252 retryable: false,
253 fatal: false,
254 status: 500,
255 };
256 crate::macros::call_error_hook(
257 instance.caption(),
258 instance.kind(),
259 instance.is_fatal(),
260 instance.is_retryable(),
261 );
262 instance
263 }
264
265 pub fn network(
267 endpoint: impl Into<String>,
268 source: impl Into<Option<Box<dyn StdError + Send + Sync>>>,
269 ) -> Self {
270 let source = source.into();
272
273 let instance = Self::Network {
274 endpoint: endpoint.into(),
275 source,
276 retryable: true,
277 fatal: false,
278 status: 503,
279 };
280 crate::macros::call_error_hook(
281 instance.caption(),
282 instance.kind(),
283 instance.is_fatal(),
284 instance.is_retryable(),
285 );
286 instance
287 }
288
289 pub fn network_with_source(
291 endpoint: impl Into<String>,
292 source: Option<Box<dyn StdError + Send + Sync>>,
293 ) -> Self {
294 let instance = Self::Network {
295 endpoint: endpoint.into(),
296 source,
297 retryable: true,
298 fatal: false,
299 status: 503,
300 };
301 crate::macros::call_error_hook(
302 instance.caption(),
303 instance.kind(),
304 instance.is_fatal(),
305 instance.is_retryable(),
306 );
307 instance
308 }
309
310 pub fn other(message: impl Into<String>) -> Self {
312 let instance = Self::Other {
313 message: message.into(),
314 retryable: false,
315 fatal: false,
316 status: 500,
317 };
318 crate::macros::call_error_hook(
319 instance.caption(),
320 instance.kind(),
321 instance.is_fatal(),
322 instance.is_retryable(),
323 );
324 instance
325 }
326
327 pub fn with_retryable(mut self, retryable: bool) -> Self {
329 match &mut self {
330 Self::Config { retryable: r, .. } => *r = retryable,
331 Self::Filesystem { retryable: r, .. } => *r = retryable,
332 Self::Network { retryable: r, .. } => *r = retryable,
333 Self::Other { retryable: r, .. } => *r = retryable,
334 }
335 self
336 }
337
338 pub fn with_fatal(mut self, fatal: bool) -> Self {
340 match &mut self {
341 Self::Config { fatal: f, .. } => *f = fatal,
342 Self::Filesystem { fatal: f, .. } => *f = fatal,
343 Self::Network { fatal: f, .. } => *f = fatal,
344 Self::Other { fatal: f, .. } => *f = fatal,
345 }
346 self
347 }
348
349 pub fn with_status(mut self, status: u16) -> Self {
351 match &mut self {
352 Self::Config { status: s, .. } => *s = status,
353 Self::Filesystem { status: s, .. } => *s = status,
354 Self::Network { status: s, .. } => *s = status,
355 Self::Other { status: s, .. } => *s = status,
356 }
357 self
358 }
359
360 pub fn with_code(self, code: impl Into<String>) -> crate::registry::CodedError<Self> {
362 crate::registry::CodedError::new(self, code.into())
363 }
364
365 pub fn context<C: std::fmt::Display + std::fmt::Debug + Send + Sync + 'static>(
367 self,
368 context: C,
369 ) -> crate::context::ContextError<Self, C> {
370 crate::context::ContextError::new(self, context)
371 }
372}