error_forge/
error.rs

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
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(self.caption(), self.kind(), self.is_fatal(), self.is_retryable());
59    }
60}
61
62/// Example error enum that can be replaced by the define_errors! macro.
63#[derive(Debug)]
64#[cfg_attr(feature = "serde", derive(Serialize))]
65pub enum AppError {
66    /// Configuration-related errors
67    Config { 
68        message: String,
69        retryable: bool,
70        fatal: bool,
71        status: u16,
72    },
73    
74    /// Filesystem-related errors with optional path and source error
75    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-related errors
85    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    /// Generic errors for anything not covered by specific variants
95    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
195/// Constructor methods for AppError
196impl AppError {
197    /// Create a new Config error
198    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    /// Create a new Filesystem error
210    pub fn filesystem(path: impl Into<String>, source: impl Into<Option<io::Error>>) -> Self {
211        // Convert the source parameter
212        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    /// Create a filesystem error with specific source error
229    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    /// Create a new Network error
242    pub fn network(endpoint: impl Into<String>, source: impl Into<Option<Box<dyn StdError + Send + Sync>>>) -> Self {
243        // Convert the source parameter
244        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    /// Create a network error with specific source error
258    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    /// Create a new generic error
271    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    /// Set whether this error is retryable
283    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    /// Set whether this error is fatal
294    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    /// Set the HTTP status code for this error
305    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    /// Add a code to this error
316    pub fn with_code(self, code: impl Into<String>) -> crate::registry::CodedError<Self> {
317        crate::registry::CodedError::new(self, code.into())
318    }
319    
320    /// Add context to this error
321    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}