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