Skip to main content

error_forge/
error.rs

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
10/// Type alias for error-forge results.
11pub type Result<T> = std::result::Result<T, crate::error::AppError>;
12
13/// Base trait for all custom error variants.
14pub trait ForgeError: std::error::Error + Send + Sync + 'static {
15    /// Returns the kind of error, typically matching the enum variant
16    fn kind(&self) -> &'static str;
17
18    /// Returns a human-readable caption for the error
19    fn caption(&self) -> &'static str;
20
21    /// Returns true if the operation can be retried
22    fn is_retryable(&self) -> bool {
23        false
24    }
25
26    /// Returns true if the error is fatal and should terminate the program
27    fn is_fatal(&self) -> bool {
28        false
29    }
30
31    /// Returns an appropriate HTTP status code for the error
32    fn status_code(&self) -> u16 {
33        500
34    }
35
36    /// Returns an appropriate process exit code for the error
37    fn exit_code(&self) -> i32 {
38        1
39    }
40
41    /// Returns a user-facing message that can be shown to end users
42    fn user_message(&self) -> String {
43        self.to_string()
44    }
45
46    /// Returns a detailed technical message for developers/logs
47    fn dev_message(&self) -> String {
48        format!("[{}] {}", self.kind(), self)
49    }
50
51    /// Returns a backtrace if available
52    fn backtrace(&self) -> Option<&Backtrace> {
53        None
54    }
55
56    /// Registers the error with the central error registry
57    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/// Example error enum that can be replaced by the define_errors! macro.
68#[derive(Debug)]
69#[cfg_attr(feature = "serde", derive(Serialize))]
70pub enum AppError {
71    /// Configuration-related errors
72    Config {
73        message: String,
74        retryable: bool,
75        fatal: bool,
76        status: u16,
77    },
78
79    /// Filesystem-related errors with optional path and source error
80    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-related errors
90    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    /// Generic errors for anything not covered by specific variants
100    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
204/// Constructor methods for AppError
205impl AppError {
206    /// Create a new Config error
207    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    /// Create a new Filesystem error
224    pub fn filesystem(path: impl Into<String>, source: impl Into<Option<io::Error>>) -> Self {
225        // Convert the source parameter
226        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    /// Create a filesystem error with specific source error
248    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    /// Create a new Network error
266    pub fn network(
267        endpoint: impl Into<String>,
268        source: impl Into<Option<Box<dyn StdError + Send + Sync>>>,
269    ) -> Self {
270        // Convert the source parameter
271        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    /// Create a network error with specific source error
290    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    /// Create a new generic error
311    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    /// Set whether this error is retryable
328    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    /// Set whether this error is fatal
339    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    /// Set the HTTP status code for this error
350    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    /// Add a code to this error
361    pub fn with_code(self, code: impl Into<String>) -> crate::registry::CodedError<Self> {
362        crate::registry::CodedError::new(self, code.into())
363    }
364
365    /// Add context to this error
366    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}