Skip to main content

oopsie_core/
chain.rs

1//! Iterating an error and its transitive [`source`](std::error::Error::source) causes.
2
3use core::error::Error as StdError;
4
5/// An iterator over an error and its chain of [`source`](StdError::source) causes.
6///
7/// Yields the error itself first, then each successive `source()`, matching
8/// `anyhow::Chain`: `err.chain().next()` is `Some(err)`, not its first cause.
9/// Build one with [`ErrorChainExt::chain`].
10///
11/// Unlike the depth-capped walks elsewhere in the crate, this iterator is an
12/// honest cursor with no internal bound: a consumer caps it (`.take(n)`,
13/// `.find(..)`). A source chain that cycles back on itself — permitted by the
14/// `Error` contract — makes the iterator loop forever, the same as
15/// `anyhow::Chain`.
16#[derive(Clone, Debug)]
17pub struct Chain<'a> {
18    next: Option<&'a (dyn StdError + 'static)>,
19}
20
21impl<'a> Chain<'a> {
22    #[inline]
23    fn new(head: &'a (dyn StdError + 'static)) -> Self {
24        Self { next: Some(head) }
25    }
26}
27
28impl<'a> Iterator for Chain<'a> {
29    type Item = &'a (dyn StdError + 'static);
30
31    #[inline]
32    fn next(&mut self) -> Option<Self::Item> {
33        let current = self.next?;
34        self.next = current.source();
35        Some(current)
36    }
37}
38
39impl core::iter::FusedIterator for Chain<'_> {}
40
41/// Walk an error and its [`source`](StdError::source) chain.
42///
43/// Import via `use oopsie::prelude::*` or `use oopsie::ErrorChainExt`.
44///
45/// Implemented for every `E: Error + 'static` and for `dyn Error + 'static`
46/// itself, so it covers a typed `#[oopsie]` error, a [`Welp`](crate::Welp), and
47/// the `&(dyn Error + 'static)` returned by [`source`](StdError::source) with a
48/// single method call.
49///
50/// # Example
51///
52/// ```
53/// use oopsie::{Oopsie, ErrorChainExt as _, ResultExt as _};
54///
55/// #[derive(Debug, Oopsie)]
56/// #[oopsie(module(false))]
57/// enum ReadError {
58///     #[oopsie("read failed: {source}")]
59///     Read { source: std::io::Error },
60/// }
61///
62/// # fn main() {
63/// let err: Result<(), std::io::Error> = Err(std::io::Error::other("disk full"));
64/// let err = err.context(Read).unwrap_err();
65///
66/// let messages: Vec<String> = err.chain().map(ToString::to_string).collect();
67/// assert_eq!(messages, ["read failed: disk full", "disk full"]);
68/// assert_eq!(err.root_cause().to_string(), "disk full");
69/// # }
70/// ```
71pub trait ErrorChainExt {
72    /// Iterate this error followed by its transitive
73    /// [`source`](StdError::source) causes.
74    ///
75    /// The first item is the error itself ([`anyhow::Chain`] semantics), so the
76    /// chain is never empty.
77    ///
78    /// [`anyhow::Chain`]: https://docs.rs/anyhow/latest/anyhow/struct.Chain.html
79    fn chain(&self) -> Chain<'_>;
80
81    /// The last error in the chain: the deepest [`source`](StdError::source),
82    /// or this error itself when it has none.
83    fn root_cause(&self) -> &(dyn StdError + 'static);
84}
85
86/// The shared body, taking an already-erased head so both the blanket impl and
87/// the `dyn Error` impl reuse it — a `?Sized` blanket can't, since `&E` only
88/// unsizes to `&dyn Error` when `E: Sized`.
89#[inline]
90fn root_cause_of<'a>(head: &'a (dyn StdError + 'static)) -> &'a (dyn StdError + 'static) {
91    let mut cause = head;
92    while let Some(source) = cause.source() {
93        cause = source;
94    }
95    cause
96}
97
98impl<E: StdError + 'static> ErrorChainExt for E {
99    #[inline]
100    fn chain(&self) -> Chain<'_> {
101        Chain::new(self)
102    }
103
104    #[inline]
105    fn root_cause(&self) -> &(dyn StdError + 'static) {
106        root_cause_of(self)
107    }
108}
109
110impl ErrorChainExt for dyn StdError + 'static {
111    #[inline]
112    fn chain(&self) -> Chain<'_> {
113        Chain::new(self)
114    }
115
116    #[inline]
117    fn root_cause(&self) -> &(dyn StdError + 'static) {
118        root_cause_of(self)
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125    use std::fmt;
126
127    #[derive(Debug)]
128    struct Leaf;
129
130    impl fmt::Display for Leaf {
131        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132            f.write_str("leaf")
133        }
134    }
135
136    impl StdError for Leaf {}
137
138    #[derive(Debug)]
139    struct Layer {
140        label: &'static str,
141        source: Box<dyn StdError + 'static>,
142    }
143
144    impl fmt::Display for Layer {
145        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146            f.write_str(self.label)
147        }
148    }
149
150    impl StdError for Layer {
151        fn source(&self) -> Option<&(dyn StdError + 'static)> {
152            Some(&*self.source)
153        }
154    }
155
156    fn three_deep() -> Layer {
157        Layer {
158            label: "top",
159            source: Box::new(Layer {
160                label: "middle",
161                source: Box::new(Leaf),
162            }),
163        }
164    }
165
166    #[test]
167    fn chain_starts_at_self_then_walks_sources() {
168        let err = three_deep();
169        let labels: Vec<String> = err.chain().map(ToString::to_string).collect();
170        assert_eq!(labels, ["top", "middle", "leaf"]);
171    }
172
173    #[test]
174    fn chain_on_leaf_yields_only_self() {
175        let leaf = Leaf;
176        assert_eq!(leaf.chain().count(), 1);
177        assert_eq!(leaf.chain().next().unwrap().to_string(), "leaf");
178    }
179
180    #[test]
181    fn chain_over_dyn_error_from_source_starts_at_that_source() {
182        let err = three_deep();
183        let source = err.source().expect("has a source");
184        let labels: Vec<String> = source.chain().map(ToString::to_string).collect();
185        assert_eq!(labels, ["middle", "leaf"]);
186    }
187
188    #[test]
189    fn root_cause_is_the_deepest_link() {
190        let err = three_deep();
191        let root = err.root_cause();
192        assert_eq!(root.to_string(), "leaf");
193        // Identity: the root is the same value the last chain link points at.
194        let last = err.chain().last().expect("non-empty chain");
195        assert!(std::ptr::eq(
196            std::ptr::from_ref(root).cast::<()>(),
197            std::ptr::from_ref(last).cast::<()>(),
198        ));
199    }
200
201    #[test]
202    fn root_cause_of_leaf_is_itself() {
203        let leaf = Leaf;
204        assert_eq!(leaf.root_cause().to_string(), "leaf");
205    }
206}