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 using [`AppError`] as the
11/// error variant.
12///
13/// Prefer this over the historical [`Result`] alias — the
14/// `AppResult` name does not shadow [`std::result::Result`] in
15/// `use error_forge::*` glob imports.
16pub type AppResult<T> = std::result::Result<T, crate::error::AppError>;
17
18/// Historical alias for [`AppResult`].
19///
20/// **Deprecated since `1.0.0`** — the unqualified `Result` name
21/// shadows [`std::result::Result`] in `use error_forge::*` glob
22/// imports, which is a quiet footgun in caller code. Use
23/// [`AppResult`] instead.
24#[deprecated(
25    since = "1.0.0",
26    note = "use `error_forge::AppResult` instead — the unqualified `Result` \
27            name shadows `std::result::Result` in glob imports"
28)]
29pub type Result<T> = AppResult<T>;
30
31/// Base trait for all custom error variants.
32pub trait ForgeError: std::error::Error + Send + Sync + 'static {
33    /// Returns the kind of error, typically matching the enum variant
34    fn kind(&self) -> &'static str;
35
36    /// Returns a human-readable caption for the error
37    fn caption(&self) -> &'static str;
38
39    /// Returns true if the operation can be retried
40    fn is_retryable(&self) -> bool {
41        false
42    }
43
44    /// Returns true if the error is fatal and should terminate the program
45    fn is_fatal(&self) -> bool {
46        false
47    }
48
49    /// Returns an appropriate HTTP status code for the error
50    fn status_code(&self) -> u16 {
51        500
52    }
53
54    /// Returns an appropriate process exit code for the error
55    fn exit_code(&self) -> i32 {
56        1
57    }
58
59    /// Returns a user-facing message that can be shown to end users
60    fn user_message(&self) -> String {
61        self.to_string()
62    }
63
64    /// Returns a detailed technical message for developers/logs
65    fn dev_message(&self) -> String {
66        format!("[{}] {}", self.kind(), self)
67    }
68
69    /// Returns a backtrace if available
70    fn backtrace(&self) -> Option<&Backtrace> {
71        None
72    }
73
74    /// Registers the error with the central error registry
75    fn register(&self) {
76        crate::macros::call_error_hook(
77            self.caption(),
78            self.kind(),
79            self.is_fatal(),
80            self.is_retryable(),
81        );
82    }
83}
84
85/// Example error enum that can be replaced by the define_errors! macro.
86#[derive(Debug)]
87#[cfg_attr(feature = "serde", derive(Serialize))]
88pub enum AppError {
89    /// Configuration-related errors
90    Config {
91        message: String,
92        retryable: bool,
93        fatal: bool,
94        status: u16,
95    },
96
97    /// Filesystem-related errors with optional path and source error
98    Filesystem {
99        path: Option<PathBuf>,
100        #[cfg_attr(feature = "serde", serde(skip))]
101        source: io::Error,
102        retryable: bool,
103        fatal: bool,
104        status: u16,
105    },
106
107    /// Network-related errors
108    Network {
109        endpoint: String,
110        #[cfg_attr(feature = "serde", serde(skip))]
111        source: Option<Box<dyn StdError + Send + Sync>>,
112        retryable: bool,
113        fatal: bool,
114        status: u16,
115    },
116
117    /// Generic errors for anything not covered by specific variants
118    Other {
119        message: String,
120        retryable: bool,
121        fatal: bool,
122        status: u16,
123    },
124}
125
126impl std::fmt::Display for AppError {
127    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128        match self {
129            Self::Config { message, .. } => write!(f, "⚙️ Configuration Error: {message}"),
130            Self::Filesystem { path, source, .. } => {
131                if let Some(p) = path {
132                    write!(f, "💾 Filesystem Error at {p:?}: {source}")
133                } else {
134                    write!(f, "💾 Filesystem Error: {source}")
135                }
136            }
137            Self::Network {
138                endpoint, source, ..
139            } => {
140                if let Some(src) = source {
141                    write!(f, "🌐 Network Error on {endpoint}: {src}")
142                } else {
143                    write!(f, "🌐 Network Error on {endpoint}")
144                }
145            }
146            Self::Other { message, .. } => write!(f, "🚨 Error: {message}"),
147        }
148    }
149}
150
151impl std::error::Error for AppError {
152    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
153        match self {
154            AppError::Filesystem { source, .. } => Some(source),
155            AppError::Network {
156                source: Some(src), ..
157            } => Some(src.as_ref()),
158            _ => None,
159        }
160    }
161}
162
163impl From<io::Error> for AppError {
164    fn from(e: io::Error) -> Self {
165        AppError::Filesystem {
166            path: None,
167            source: e,
168            retryable: false,
169            fatal: true,
170            status: 500,
171        }
172    }
173}
174
175impl ForgeError for AppError {
176    fn kind(&self) -> &'static str {
177        match self {
178            Self::Config { .. } => "Config",
179            Self::Filesystem { .. } => "Filesystem",
180            Self::Network { .. } => "Network",
181            Self::Other { .. } => "Other",
182        }
183    }
184
185    fn caption(&self) -> &'static str {
186        match self {
187            Self::Config { .. } => "⚙️ Configuration",
188            Self::Filesystem { .. } => "💾 Filesystem",
189            Self::Network { .. } => "🌐 Network",
190            Self::Other { .. } => "🚨 Error",
191        }
192    }
193
194    fn is_retryable(&self) -> bool {
195        match self {
196            Self::Config { retryable, .. } => *retryable,
197            Self::Filesystem { retryable, .. } => *retryable,
198            Self::Network { retryable, .. } => *retryable,
199            Self::Other { retryable, .. } => *retryable,
200        }
201    }
202
203    fn is_fatal(&self) -> bool {
204        match self {
205            Self::Config { fatal, .. } => *fatal,
206            Self::Filesystem { fatal, .. } => *fatal,
207            Self::Network { fatal, .. } => *fatal,
208            Self::Other { fatal, .. } => *fatal,
209        }
210    }
211
212    fn status_code(&self) -> u16 {
213        match self {
214            Self::Config { status, .. } => *status,
215            Self::Filesystem { status, .. } => *status,
216            Self::Network { status, .. } => *status,
217            Self::Other { status, .. } => *status,
218        }
219    }
220}
221
222/// Constructor methods for AppError
223impl AppError {
224    /// Create a new Config error
225    pub fn config(message: impl Into<String>) -> Self {
226        let instance = Self::Config {
227            message: message.into(),
228            retryable: false,
229            fatal: false,
230            status: 500,
231        };
232        crate::macros::call_error_hook(
233            instance.caption(),
234            instance.kind(),
235            instance.is_fatal(),
236            instance.is_retryable(),
237        );
238        instance
239    }
240
241    /// Create a new Filesystem error
242    pub fn filesystem(path: impl Into<String>, source: impl Into<Option<io::Error>>) -> Self {
243        // Convert the source parameter
244        let source = match source.into() {
245            Some(err) => err,
246            None => io::Error::other("File operation failed"),
247        };
248
249        let instance = Self::Filesystem {
250            path: Some(path.into().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 filesystem error with specific source error
266    pub fn filesystem_with_source(path: impl Into<PathBuf>, source: io::Error) -> Self {
267        let instance = Self::Filesystem {
268            path: Some(path.into()),
269            source,
270            retryable: false,
271            fatal: false,
272            status: 500,
273        };
274        crate::macros::call_error_hook(
275            instance.caption(),
276            instance.kind(),
277            instance.is_fatal(),
278            instance.is_retryable(),
279        );
280        instance
281    }
282
283    /// Create a new Network error
284    pub fn network(
285        endpoint: impl Into<String>,
286        source: impl Into<Option<Box<dyn StdError + Send + Sync>>>,
287    ) -> Self {
288        // Convert the source parameter
289        let source = source.into();
290
291        let instance = Self::Network {
292            endpoint: endpoint.into(),
293            source,
294            retryable: true,
295            fatal: false,
296            status: 503,
297        };
298        crate::macros::call_error_hook(
299            instance.caption(),
300            instance.kind(),
301            instance.is_fatal(),
302            instance.is_retryable(),
303        );
304        instance
305    }
306
307    /// Create a network error with specific source error
308    pub fn network_with_source(
309        endpoint: impl Into<String>,
310        source: Option<Box<dyn StdError + Send + Sync>>,
311    ) -> Self {
312        let instance = Self::Network {
313            endpoint: endpoint.into(),
314            source,
315            retryable: true,
316            fatal: false,
317            status: 503,
318        };
319        crate::macros::call_error_hook(
320            instance.caption(),
321            instance.kind(),
322            instance.is_fatal(),
323            instance.is_retryable(),
324        );
325        instance
326    }
327
328    /// Create a new generic error
329    pub fn other(message: impl Into<String>) -> Self {
330        let instance = Self::Other {
331            message: message.into(),
332            retryable: false,
333            fatal: false,
334            status: 500,
335        };
336        crate::macros::call_error_hook(
337            instance.caption(),
338            instance.kind(),
339            instance.is_fatal(),
340            instance.is_retryable(),
341        );
342        instance
343    }
344
345    /// Set whether this error is retryable
346    pub fn with_retryable(mut self, retryable: bool) -> Self {
347        match &mut self {
348            Self::Config { retryable: r, .. } => *r = retryable,
349            Self::Filesystem { retryable: r, .. } => *r = retryable,
350            Self::Network { retryable: r, .. } => *r = retryable,
351            Self::Other { retryable: r, .. } => *r = retryable,
352        }
353        self
354    }
355
356    /// Set whether this error is fatal
357    pub fn with_fatal(mut self, fatal: bool) -> Self {
358        match &mut self {
359            Self::Config { fatal: f, .. } => *f = fatal,
360            Self::Filesystem { fatal: f, .. } => *f = fatal,
361            Self::Network { fatal: f, .. } => *f = fatal,
362            Self::Other { fatal: f, .. } => *f = fatal,
363        }
364        self
365    }
366
367    /// Set the HTTP status code for this error
368    pub fn with_status(mut self, status: u16) -> Self {
369        match &mut self {
370            Self::Config { status: s, .. } => *s = status,
371            Self::Filesystem { status: s, .. } => *s = status,
372            Self::Network { status: s, .. } => *s = status,
373            Self::Other { status: s, .. } => *s = status,
374        }
375        self
376    }
377
378    /// Add a code to this error
379    pub fn with_code(self, code: impl Into<String>) -> crate::registry::CodedError<Self> {
380        crate::registry::CodedError::new(self, code.into())
381    }
382
383    /// Add context to this error
384    pub fn context<C: std::fmt::Display + std::fmt::Debug + Send + Sync + 'static>(
385        self,
386        context: C,
387    ) -> crate::context::ContextError<Self, C> {
388        crate::context::ContextError::new(self, context)
389    }
390}