Skip to main content

suzunari_error/
boxed_stack_error.rs

1use alloc::boxed::Box;
2
3use crate::{Location, StackError};
4use core::error::Error;
5use core::fmt::{Debug, Display, Formatter, Result};
6
7/// Type-erased wrapper around `Box<dyn StackError + Send + Sync>`.
8///
9/// Provides uniform handling of heterogeneous `StackError` types while
10/// preserving location tracking through the error chain. Use this instead
11/// of `Box<dyn StackError + Send + Sync>` for shorter type signatures
12/// and automatic `From` generation by the derive macro.
13///
14/// Note: downcasting to the concrete type is not supported through this
15/// wrapper. Use `into_inner()` if you need the raw trait object.
16///
17/// `Clone` is not implemented because the inner trait object
18/// (`Box<dyn StackError + Send + Sync>`) cannot be cloned.
19///
20/// # Example
21///
22/// ```
23/// use suzunari_error::*;
24///
25/// #[suzunari_error]
26/// #[suzu(display("inner error"))]
27/// struct InnerError {}
28///
29/// #[suzunari_error]
30/// #[suzu(display("outer error"))]
31/// struct OuterError {
32///     source: BoxedStackError,
33/// }
34///
35/// fn inner() -> Result<(), InnerError> {
36///     ensure!(false, InnerSnafu);
37///     Ok(())
38/// }
39///
40/// fn outer() -> Result<(), OuterError> {
41///     inner()
42///         .map_err(BoxedStackError::new)
43///         .context(OuterSnafu)?;
44///     Ok(())
45/// }
46///
47/// let err = outer().unwrap_err();
48/// assert_eq!(err.type_name(), "OuterError");
49/// assert!(err.stack_source().is_some());
50/// ```
51pub struct BoxedStackError {
52    inner: Box<dyn StackError + Send + Sync>,
53}
54
55impl BoxedStackError {
56    /// Wraps a concrete `StackError` in a type-erased box.
57    #[must_use]
58    pub fn new<T: StackError + Send + Sync + 'static>(inner: T) -> Self {
59        Self {
60            inner: Box::new(inner),
61        }
62    }
63
64    /// Returns a reference to the inner trait object.
65    #[must_use]
66    pub fn inner(&self) -> &(dyn StackError + Send + Sync) {
67        &*self.inner
68    }
69
70    /// Unwraps into the inner trait object.
71    #[must_use]
72    pub fn into_inner(self) -> Box<dyn StackError + Send + Sync> {
73        self.inner
74    }
75}
76
77impl Display for BoxedStackError {
78    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
79        write!(f, "{}", self.inner)
80    }
81}
82
83impl Debug for BoxedStackError {
84    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
85        write!(f, "{:?}", self.inner)
86    }
87}
88
89impl Error for BoxedStackError {
90    fn source(&self) -> Option<&(dyn Error + 'static)> {
91        self.inner.source()
92    }
93}
94
95impl StackError for BoxedStackError {
96    fn location(&self) -> Location {
97        self.inner.location()
98    }
99    fn type_name(&self) -> &'static str {
100        self.inner.type_name()
101    }
102    fn stack_source(&self) -> Option<&dyn StackError> {
103        self.inner.stack_source()
104    }
105}
106
107impl From<Box<dyn StackError + Send + Sync>> for BoxedStackError {
108    fn from(inner: Box<dyn StackError + Send + Sync>) -> Self {
109        Self { inner }
110    }
111}
112
113impl From<BoxedStackError> for Box<dyn StackError + Send + Sync> {
114    fn from(inner: BoxedStackError) -> Self {
115        inner.into_inner()
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    // Tests use raw #[derive(Snafu)] + manual impl to test StackError trait
122    // independently of proc-macro layer. .build() is snafu's standard test pattern.
123    use super::*;
124    use crate::Location;
125    use alloc::format;
126    use snafu::IntoError;
127    use snafu::prelude::*;
128
129    #[derive(Debug, Snafu)]
130    #[snafu(display("Test error: {}", message))]
131    struct TestError {
132        message: alloc::string::String,
133        #[snafu(implicit)]
134        location: Location,
135    }
136
137    impl StackError for TestError {
138        fn location(&self) -> Location {
139            self.location
140        }
141        fn type_name(&self) -> &'static str {
142            "TestError"
143        }
144    }
145
146    #[test]
147    fn test_basic_error() {
148        let test_error = TestSnafu {
149            message: "Test message",
150        }
151        .build();
152        let error = BoxedStackError::new(test_error);
153
154        assert!(format!("{}", error).contains("Test error"));
155        assert!(format!("{}", error).contains("Test message"));
156        // Debug delegates to inner's derive(Debug), not stack trace
157        assert!(format!("{:?}", error).contains("Test message"));
158        assert!(error.source().is_none());
159
160        handle_stack_error(error);
161    }
162
163    #[test]
164    fn test_error_location() {
165        let test_error = TestSnafu {
166            message: "Location test",
167        }
168        .build();
169        let original_line = test_error.location().line();
170        let error = BoxedStackError::new(test_error);
171
172        assert_eq!(error.location().file(), file!());
173        assert_eq!(error.location().line(), original_line);
174
175        handle_stack_error(error);
176    }
177
178    #[test]
179    fn test_error_conversion() {
180        let test_error = TestSnafu {
181            message: "Convert test",
182        }
183        .build();
184        let boxed: Box<dyn StackError + Send + Sync> = Box::new(test_error);
185        let generic: BoxedStackError = boxed.into();
186        let back_to_box: Box<dyn StackError + Send + Sync> = generic.into();
187
188        assert!(format!("{:?}", back_to_box).contains("Convert test"));
189    }
190
191    // --- source chain delegation and depth ---
192
193    #[derive(Debug, Snafu)]
194    #[snafu(display("Wrapper: {}", message))]
195    struct WrapperTestError {
196        message: alloc::string::String,
197        source: BoxedStackError,
198        #[snafu(implicit)]
199        location: Location,
200    }
201    impl StackError for WrapperTestError {
202        fn location(&self) -> Location {
203            self.location
204        }
205        fn type_name(&self) -> &'static str {
206            "WrapperTestError"
207        }
208        fn stack_source(&self) -> Option<&dyn StackError> {
209            Some(&self.source)
210        }
211    }
212
213    #[test]
214    fn test_source_chain_delegation() {
215        // BoxedStackError wrapping an error with a source should delegate source()
216        let inner = TestSnafu { message: "root" }.build();
217        let boxed = BoxedStackError::new(inner);
218        let wrapper = WrapperTestSnafu { message: "wrap" }.into_error(boxed);
219        let outer = BoxedStackError::new(wrapper);
220
221        // Error::source() should return Some (delegates to WrapperTestError's source)
222        assert!(outer.source().is_some());
223        // stack_source() should return Some (WrapperTestError has a stack_source)
224        assert!(outer.stack_source().is_some());
225    }
226
227    #[test]
228    fn test_depth() {
229        // Leaf error: depth == 0
230        let leaf = BoxedStackError::new(TestSnafu { message: "leaf" }.build());
231        assert_eq!(leaf.depth(), 0);
232
233        // Wrapped error: depth == 1 (the BoxedStackError source counts as 1)
234        let inner = BoxedStackError::new(TestSnafu { message: "inner" }.build());
235        let wrapper = WrapperTestSnafu { message: "outer" }.into_error(inner);
236        let outer = BoxedStackError::new(wrapper);
237        assert_eq!(outer.depth(), 1);
238    }
239
240    fn handle_stack_error<T: StackError>(_: T) {}
241
242    #[test]
243    fn test_inner_ref() {
244        let test_error = TestSnafu {
245            message: "inner ref test",
246        }
247        .build();
248        let original_line = test_error.location().line();
249        let error = BoxedStackError::new(test_error);
250
251        let inner = error.inner();
252        assert_eq!(inner.location().line(), original_line);
253        assert_eq!(inner.type_name(), "TestError");
254    }
255
256    #[test]
257    fn test_into_inner_round_trip() {
258        let test_error = TestSnafu {
259            message: "round trip",
260        }
261        .build();
262        let original_line = test_error.location().line();
263
264        // BoxedStackError → into_inner → Box<dyn StackError + Send + Sync>
265        let boxed = BoxedStackError::new(test_error);
266        let inner: Box<dyn StackError + Send + Sync> = boxed.into_inner();
267
268        assert_eq!(inner.location().line(), original_line);
269        assert_eq!(inner.type_name(), "TestError");
270        assert!(format!("{inner}").contains("round trip"));
271
272        // Box<dyn StackError + Send + Sync> → BoxedStackError (via From)
273        let boxed_again: BoxedStackError = inner.into();
274        assert_eq!(boxed_again.location().line(), original_line);
275    }
276}