Skip to main content

error_forge/
context.rs

1use crate::error::ForgeError;
2use std::fmt;
3
4/// A wrapper error type that attaches contextual information to an error
5#[derive(Debug)]
6pub struct ContextError<E, C> {
7    /// The original error
8    pub error: E,
9    /// The context attached to the error
10    pub context: C,
11}
12
13impl<E, C> ContextError<E, C> {
14    /// Create a new context error wrapping the original error
15    pub fn new(error: E, context: C) -> Self {
16        Self { error, context }
17    }
18
19    /// Extract the original error, discarding the context
20    pub fn into_error(self) -> E {
21        self.error
22    }
23
24    /// Map the context to a new type using the provided function
25    pub fn map_context<D, F>(self, f: F) -> ContextError<E, D>
26    where
27        F: FnOnce(C) -> D,
28    {
29        ContextError {
30            error: self.error,
31            context: f(self.context),
32        }
33    }
34
35    /// Add another layer of context to this error
36    pub fn context<D>(self, context: D) -> ContextError<Self, D>
37    where
38        D: std::fmt::Display + std::fmt::Debug + Send + Sync + 'static,
39    {
40        ContextError::new(self, context)
41    }
42}
43
44impl<E: fmt::Display, C: fmt::Display> fmt::Display for ContextError<E, C> {
45    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46        write!(f, "{}: {}", self.context, self.error)
47    }
48}
49
50impl<E: std::error::Error + 'static, C: fmt::Display + fmt::Debug + Send + Sync + 'static>
51    std::error::Error for ContextError<E, C>
52{
53    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
54        Some(&self.error)
55    }
56}
57
58/// Extension trait for Result types to add context to errors
59pub trait ResultExt<T, E> {
60    /// Adds context to the error variant of the Result
61    fn context<C>(self, context: C) -> Result<T, ContextError<E, C>>;
62
63    /// Adds context to the error variant using a closure that is only called on error
64    fn with_context<C, F>(self, f: F) -> Result<T, ContextError<E, C>>
65    where
66        F: FnOnce() -> C;
67}
68
69impl<T, E> ResultExt<T, E> for Result<T, E> {
70    fn context<C>(self, context: C) -> Result<T, ContextError<E, C>> {
71        self.map_err(|error| ContextError::new(error, context))
72    }
73
74    fn with_context<C, F>(self, f: F) -> Result<T, ContextError<E, C>>
75    where
76        F: FnOnce() -> C,
77    {
78        self.map_err(|error| ContextError::new(error, f()))
79    }
80}
81
82// Implement ForgeError for ContextError when the inner error implements ForgeError
83impl<E: ForgeError, C: fmt::Display + fmt::Debug + Send + Sync + 'static> ForgeError
84    for ContextError<E, C>
85{
86    fn kind(&self) -> &'static str {
87        self.error.kind()
88    }
89
90    fn caption(&self) -> &'static str {
91        self.error.caption()
92    }
93
94    fn is_retryable(&self) -> bool {
95        self.error.is_retryable()
96    }
97
98    fn is_fatal(&self) -> bool {
99        self.error.is_fatal()
100    }
101
102    fn status_code(&self) -> u16 {
103        self.error.status_code()
104    }
105
106    fn exit_code(&self) -> i32 {
107        self.error.exit_code()
108    }
109
110    fn user_message(&self) -> String {
111        format!("{}: {}", self.context, self.error.user_message())
112    }
113
114    fn dev_message(&self) -> String {
115        format!("{}: {}", self.context, self.error.dev_message())
116    }
117
118    fn backtrace(&self) -> Option<&std::backtrace::Backtrace> {
119        self.error.backtrace()
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126    use crate::AppError;
127
128    #[test]
129    fn test_context_error() {
130        let error = AppError::config("Invalid config");
131        let ctx_error = error.context("Failed to load settings");
132
133        assert_eq!(
134            ctx_error.to_string(),
135            "Failed to load settings: ⚙️ Configuration Error: Invalid config"
136        );
137        assert_eq!(ctx_error.kind(), "Config");
138        assert_eq!(ctx_error.caption(), "⚙️ Configuration");
139    }
140
141    #[test]
142    fn test_result_context() {
143        let result: Result<(), AppError> = Err(AppError::config("Invalid config"));
144        let ctx_result = result.context("Failed to load settings");
145
146        assert!(ctx_result.is_err());
147        let err = ctx_result.unwrap_err();
148        assert_eq!(
149            err.to_string(),
150            "Failed to load settings: ⚙️ Configuration Error: Invalid config"
151        );
152    }
153}