pseudo_backtrace/
lib.rs

1#![no_std]
2#![warn(missing_docs)]
3#![doc = include_str!("../README.md")]
4
5pub use pseudo_backtrace_derive::StackError;
6#[doc(hidden)]
7pub mod private;
8
9/// One layer in a stack of chained errors.
10#[derive(Debug, Clone)]
11pub enum Chain<'a> {
12    /// A stacked error
13    Stacked(&'a dyn StackError),
14    /// A [core::error::Error].
15    Std(&'a dyn core::error::Error),
16}
17
18impl<'a> Chain<'a> {
19    /// Returns lower-level error
20    pub fn next(&self) -> Option<Chain<'a>> {
21        match self {
22            Chain::Stacked(stack_error) => stack_error.next(),
23            Chain::Std(error) => error.source().map(Chain::Std),
24        }
25    }
26
27    /// Returns the underlying error for this stack layer.
28    pub const fn inner(&'a self) -> &'a dyn core::error::Error {
29        match self {
30            Chain::Stacked(stack_error) => stack_error,
31            Chain::Std(error) => error,
32        }
33    }
34
35    /// Returns the recorded source location when available.
36    pub fn location(&self) -> Option<&'static core::panic::Location<'static>> {
37        match self {
38            Chain::Stacked(stack_error) => Some(stack_error.location()),
39            _ => None,
40        }
41    }
42}
43
44impl core::fmt::Display for Chain<'_> {
45    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
46        match self {
47            Chain::Stacked(stack_error) => {
48                write!(f, "{}, at {}", stack_error, stack_error.location())
49            }
50            Chain::Std(error) => error.fmt(f),
51        }
52    }
53}
54
55impl core::error::Error for Chain<'_> {
56    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
57        self.inner().source()
58    }
59}
60
61impl<'a> IntoIterator for Chain<'a> {
62    type Item = Chain<'a>;
63    type IntoIter = Iter<'a>;
64
65    fn into_iter(self) -> Self::IntoIter {
66        Iter { stack: Some(self) }
67    }
68}
69
70impl<'a, E> From<&'a E> for Chain<'a>
71where
72    E: StackError + Sized,
73{
74    fn from(stack_error: &'a E) -> Self {
75        Chain::Stacked(stack_error)
76    }
77}
78
79/// Error types that can report a stack trace-like chain.
80pub trait StackError: core::error::Error {
81    /// Returns the source location of this error.
82    fn location(&self) -> &'static core::panic::Location<'static>;
83    /// Returns the next detail in the stack.
84    fn next<'a>(&'a self) -> Option<Chain<'a>>;
85    /// Creates an iterator over this error's stack details.
86    fn iter<'a>(&'a self) -> Iter<'a>
87    where
88        Self: Sized,
89    {
90        Iter::new(self)
91    }
92}
93
94/// Iterator over individual error stack entries.
95#[derive(Debug, Clone)]
96pub struct Iter<'a> {
97    stack: Option<Chain<'a>>,
98}
99
100impl<'a> Iter<'a> {
101    const fn new<E>(source: &'a E) -> Self
102    where
103        E: StackError,
104    {
105        Iter {
106            stack: Some(Chain::Stacked(source)),
107        }
108    }
109}
110
111impl<'a> Iterator for Iter<'a> {
112    type Item = Chain<'a>;
113
114    fn next(&mut self) -> Option<Self::Item> {
115        match self.stack.take() {
116            Some(detail) => {
117                self.stack = detail.next();
118                Some(detail)
119            }
120            None => None,
121        }
122    }
123}
124
125/// Wrapper that records the call-site for any `core::error::Error` and exposes it as a [StackError].
126///
127/// This is useful when you already have an error type that implements [core::error::Error] but cannot be modified to derive [StackError].
128///
129/// # Examples
130/// ```
131/// # extern crate std;
132/// use pseudo_backtrace::{LocatedError, StackError};
133///
134/// fn assert_stack_error<T:StackError>(){}
135///     
136/// assert_stack_error::<LocatedError<std::io::Error>>();
137/// ```
138#[derive(Debug)]
139pub struct LocatedError<E> {
140    source: E,
141    location: &'static core::panic::Location<'static>,
142}
143
144impl<E> LocatedError<E> {
145    /// Returns the inner value
146    pub fn into_inner(self) -> E {
147        self.source
148    }
149}
150
151impl<E> core::fmt::Display for LocatedError<E>
152where
153    E: core::fmt::Display,
154{
155    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
156        self.source.fmt(f)
157    }
158}
159
160impl<E> core::error::Error for LocatedError<E>
161where
162    E: core::error::Error,
163{
164    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
165        self.source.source()
166    }
167}
168
169impl<E> StackError for LocatedError<E>
170where
171    E: core::error::Error,
172{
173    fn location(&self) -> &'static core::panic::Location<'static> {
174        self.location
175    }
176
177    fn next<'a>(&'a self) -> Option<Chain<'a>> {
178        self.source.source().map(Chain::Std)
179    }
180}
181
182impl<E> From<E> for LocatedError<E> {
183    #[track_caller]
184    fn from(value: E) -> Self {
185        LocatedError {
186            source: value,
187            location: core::panic::Location::caller(),
188        }
189    }
190}
191
192/// Helper for display [Chain]
193#[derive(Debug, Clone)]
194pub struct ChainWriter<'a> {
195    std_limit: usize,
196    stack: Chain<'a>,
197}
198
199impl<'a> core::fmt::Display for ChainWriter<'a> {
200    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
201        let mut std_remaining = self.std_limit;
202        for (i, err) in self.stack.clone().into_iter().enumerate() {
203            if matches!(err, Chain::Std(_)) {
204                if std_remaining == 0 {
205                    break;
206                }
207                std_remaining -= 1;
208            }
209
210            writeln!(f, "{}: {}", i, err)?;
211        }
212
213        Ok(())
214    }
215}
216
217/// Convenience helpers for types implementing [StackError].
218pub trait StackErrorExt: StackError + Sized {
219    /// Returns a [ChainWriter] that walks this error stack from the top and prints a single trailing non- [StackError] source when formatting.
220    ///
221    /// ## Example
222    ///
223    /// ```ignore
224    /// let err = StackErrorC::new();
225    /// println!("{}", err.to_chain());
226    /// // 0: StackError A, at src/main.rs:20:5
227    /// // 1: StackError B, at src/main.rs:19:5
228    /// // 2: StackError C, at src/main.rs:18:5  
229    /// // 3: StdError A
230    /// ```
231    fn to_chain<'a>(&'a self) -> ChainWriter<'a> {
232        self.to_chain_with_limit(1)
233    }
234
235    /// Returns a [ChainWriter] capped to `limit` trailing [core::error::Error] entries that do not implement [StackError].
236    ///
237    /// ## Example
238    ///
239    /// ```ignore
240    /// let err = StackErrorC::new();
241    /// println!("{}", err.to_chain_with_limit(usize::MAX));
242    /// // 0: StackError A, at src/main.rs:20:5
243    /// // 1: StackError B, at src/main.rs:19:5
244    /// // 2: StackError C, at src/main.rs:18:5  
245    /// // 3: StdError A
246    /// // 4: StdError B
247    /// // 5: StdError C
248    ///
249    /// println!("{}", err.to_chain_with_limit(0));
250    /// // 0: StackError A, at src/main.rs:20:5
251    /// // 1: StackError B, at src/main.rs:19:5
252    /// // 2: StackError C, at src/main.rs:18:5  
253    /// ```
254    fn to_chain_with_limit<'a>(&'a self, limit: usize) -> ChainWriter<'a> {
255        ChainWriter {
256            std_limit: limit,
257            stack: Chain::from(self),
258        }
259    }
260
261    /// Returns the deepest [Chain] in the chain.
262    /// ## Example
263    ///
264    /// ```ignore
265    /// 0: StackError A, at src/main.rs:20:5
266    /// 1: StackError B, at src/main.rs:19:5
267    /// 2: StackError C, at src/main.rs:18:5  
268    /// 3: StdError A
269    /// 4: StdError B
270    /// 5: StdError C <- Return this
271    /// ```
272    fn last<'a>(&'a self) -> Chain<'a>
273    where
274        Self: Sized,
275    {
276        self.iter().last().unwrap_or(Chain::from(self))
277    }
278
279    /// Returns the deepest [StackError] in the chain
280    ///
281    /// ## Example
282    ///
283    /// ```ignore
284    /// 0: StackError A, at src/main.rs:20:5
285    /// 1: StackError B, at src/main.rs:19:5
286    /// 2: StackError C, at src/main.rs:18:5  <- Return this
287    /// 3: StdError A
288    /// 4: StdError B
289    /// 5: StdError C
290    /// ```
291    fn last_stacked(&self) -> &dyn StackError {
292        self.iter()
293            .filter_map(|e| match e {
294                Chain::Stacked(stack_error) => Some(stack_error),
295                _ => None,
296            })
297            .last()
298            .unwrap_or(self)
299    }
300
301    /// Returns the first [core::error::Error] in the chain
302    ///
303    /// ## Example
304    ///
305    /// ```ignore
306    /// 0: StackError A, at src/main.rs:20:5
307    /// 1: StackError B, at src/main.rs:19:5
308    /// 2: StackError C, at src/main.rs:18:5
309    /// 3: StdError A <- Return this
310    /// 4: StdError B
311    /// 5: StdError C
312    /// ```
313    fn first_std(&self) -> Option<&dyn core::error::Error> {
314        self.iter()
315            .filter_map(|e| match e {
316                Chain::Std(error) => Some(error),
317                _ => None,
318            })
319            .next()
320    }
321}
322
323impl<E: StackError> StackErrorExt for E {}
324
325#[cfg(test)]
326mod tests {
327    extern crate std;
328
329    use super::{Chain, StackError, StackErrorExt};
330    #[derive(Debug)]
331    struct NestedStd {
332        source: std::boxed::Box<dyn core::error::Error + 'static>,
333    }
334
335    impl NestedStd {
336        fn new<E>(source: E) -> Self
337        where
338            E: core::error::Error + 'static,
339        {
340            Self {
341                source: std::boxed::Box::new(source),
342            }
343        }
344
345        fn nest(self) -> Self {
346            Self {
347                source: std::boxed::Box::new(self),
348            }
349        }
350    }
351
352    impl core::fmt::Display for NestedStd {
353        fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
354            self.source.fmt(f)
355        }
356    }
357
358    impl core::error::Error for NestedStd {
359        fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
360            Some(self.source.as_ref())
361        }
362    }
363
364    #[derive(Debug)]
365    enum Stacked {
366        Stacked {
367            source: std::boxed::Box<Stacked>,
368            location: &'static core::panic::Location<'static>,
369        },
370        Std {
371            source: std::boxed::Box<dyn core::error::Error + 'static>,
372            location: &'static core::panic::Location<'static>,
373        },
374    }
375
376    impl Stacked {
377        fn new<E>(source: E) -> Self
378        where
379            E: core::error::Error + 'static,
380        {
381            Stacked::Std {
382                source: std::boxed::Box::new(source),
383                location: core::panic::Location::caller(),
384            }
385        }
386
387        #[track_caller]
388        fn stack(self) -> Self {
389            Self::Stacked {
390                source: std::boxed::Box::new(self),
391                location: core::panic::Location::caller(),
392            }
393        }
394    }
395
396    impl core::fmt::Display for Stacked {
397        fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
398            match self {
399                Stacked::Stacked { .. } => "Stacked".fmt(f),
400                Stacked::Std { .. } => "Std".fmt(f),
401            }
402        }
403    }
404
405    impl core::error::Error for Stacked {
406        fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
407            match self {
408                Stacked::Stacked { source, .. } => Some(source),
409                Stacked::Std { source, .. } => Some(source.as_ref()),
410            }
411        }
412    }
413
414    impl StackError for Stacked {
415        fn location(&self) -> &'static core::panic::Location<'static> {
416            match self {
417                Stacked::Stacked { location, .. } => location,
418                Stacked::Std { location, .. } => location,
419            }
420        }
421
422        fn next<'a>(&'a self) -> Option<crate::Chain<'a>> {
423            match self {
424                Stacked::Stacked { source, .. } => Some(Chain::Stacked(source.as_ref())),
425                Stacked::Std { source, .. } => Some(Chain::Std(source.as_ref())),
426            }
427        }
428    }
429
430    #[test]
431    fn stack_writer_limit() {
432        use std::string::ToString;
433        let a = std::io::Error::other("Error A");
434        let b = NestedStd::new(a);
435        let c = b.nest();
436        let d = Stacked::new(c);
437        let e = d.stack();
438        let f = e.stack();
439        let g = f.stack();
440
441        assert_eq!(g.iter().count(), 7);
442
443        let stack = g.to_chain().to_string();
444        assert_eq!(stack.lines().count(), 5);
445
446        let stack = g.to_chain_with_limit(usize::MAX).to_string();
447        std::println!("to_chain_with_limit: {stack}");
448        assert_eq!(stack.lines().count(), 7);
449    }
450}