1use crate::error::ForgeError;
2use std::fmt;
3
4#[derive(Debug)]
13#[non_exhaustive]
14pub struct ContextError<E, C> {
15 pub error: E,
17 pub context: C,
19}
20
21impl<E, C> ContextError<E, C> {
22 pub fn new(error: E, context: C) -> Self {
24 Self { error, context }
25 }
26
27 pub fn into_error(self) -> E {
29 self.error
30 }
31
32 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 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
66pub trait ResultExt<T, E> {
68 fn context<C>(self, context: C) -> Result<T, ContextError<E, C>>;
70
71 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
90impl<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}