error_stack/hook/
context.rs

1#[cfg_attr(feature = "std", allow(unused_imports))]
2use alloc::boxed::Box;
3use alloc::collections::BTreeMap;
4use core::any::{Any, TypeId};
5
6pub(crate) type Storage = BTreeMap<TypeId, BTreeMap<TypeId, Box<dyn Any>>>;
7
8/// Private struct which is used to hold the information about the current count for every type.
9/// This is used so that others cannot interfere with the counter and ensure that there's no
10/// unexpected behavior.
11pub(crate) struct Counter(isize);
12
13impl Counter {
14    pub(crate) const fn new(value: isize) -> Self {
15        Self(value)
16    }
17
18    pub(crate) const fn as_inner(&self) -> isize {
19        self.0
20    }
21
22    pub(crate) fn increment(&mut self) {
23        self.0 += 1;
24    }
25
26    pub(crate) fn decrement(&mut self) {
27        self.0 -= 1;
28    }
29}
30
31pub(crate) struct Inner<T> {
32    storage: Storage,
33    extra: T,
34}
35
36impl<T> Inner<T> {
37    pub(crate) fn new(extra: T) -> Self {
38        Self {
39            storage: Storage::new(),
40            extra,
41        }
42    }
43}
44
45impl<T> Inner<T> {
46    pub(crate) const fn storage(&self) -> &Storage {
47        &self.storage
48    }
49
50    pub(crate) fn storage_mut(&mut self) -> &mut Storage {
51        &mut self.storage
52    }
53
54    pub(crate) const fn extra(&self) -> &T {
55        &self.extra
56    }
57
58    pub(crate) fn extra_mut(&mut self) -> &mut T {
59        &mut self.extra
60    }
61}
62
63macro_rules! impl_hook_context {
64    ($(#[$meta:meta])* $vis:vis struct HookContext<$extra:ident> {..}) => {
65
66// TODO: add link to serde hooks once implemented
67// TODO: ideally we would want to make `HookContextInner` private, as it is an implementation
68//  detail, but "attribute privacy" as outlined in https://github.com/rust-lang/rust/pull/61969
69//  is currently not implemented for repr(transparent).
70$(#[$meta])*
71#[cfg_attr(not(doc), repr(transparent))]
72$vis struct HookContext<T> {
73    inner: $crate::hook::context::Inner<$extra>,
74    _marker: core::marker::PhantomData<fn(&T)>,
75}
76
77impl HookContext<()> {
78    pub(crate) fn new(extra: $extra) -> Self {
79        Self {
80            inner: $crate::hook::context::Inner::new(extra),
81            _marker: core::marker::PhantomData,
82        }
83    }
84}
85
86impl<T> HookContext<T> {
87    pub(crate) const fn inner(&self) -> &$crate::hook::context::Inner<$extra> {
88        &self.inner
89    }
90
91    pub(crate) fn inner_mut(&mut self) -> &mut $crate::hook::context::Inner<$extra> {
92        &mut self.inner
93    }
94
95    fn storage(&self) -> &$crate::hook::context::Storage {
96        self.inner().storage()
97    }
98
99    fn storage_mut(&mut self) -> &mut $crate::hook::context::Storage {
100        self.inner_mut().storage_mut()
101    }
102}
103
104#[cfg(any(feature = "std", feature = "hooks"))]
105impl<T> HookContext<T> {
106    /// Cast the [`HookContext`] to a new type `U`.
107    ///
108    /// The storage of [`HookContext`] is partitioned, meaning that if `T` and `U` are different
109    /// types the values stored in [`HookContext<_, T>`] will be separated from values in
110    /// [`HookContext<_, U>`].
111    ///
112    /// In most situations this functions isn't needed, as it transparently casts between different
113    /// partitions of the storage. Only hooks that share storage with hooks of different types
114    /// should need to use this function.
115    ///
116    /// ### Example
117    ///
118    /// ```rust
119    /// # // we only test with nightly, which means that `render()` is unused on earlier version
120    /// # #![cfg_attr(not(nightly), allow(dead_code, unused_variables, unused_imports))]
121    /// use std::io::ErrorKind;
122    ///
123    /// use error_stack::Report;
124    ///
125    /// struct Warning(&'static str);
126    /// struct Error(&'static str);
127    ///
128    /// Report::install_debug_hook::<Error>(|Error(frame), context| {
129    ///     let idx = context.increment_counter() + 1;
130    ///
131    ///     context.push_body(format!("[{idx}] [ERROR] {frame}"));
132    /// });
133    /// Report::install_debug_hook::<Warning>(|Warning(frame), context| {
134    ///     // We want to share the same counter with `Error`, so that we're able to have
135    ///     // a global counter to keep track of all errors and warnings in order, this means
136    ///     // we need to access the storage of `Error` using `cast()`.
137    ///     let context = context.cast::<Error>();
138    ///     let idx = context.increment_counter() + 1;
139    ///     context.push_body(format!("[{idx}] [WARN] {frame}"))
140    /// });
141    ///
142    /// let report = Report::new(std::io::Error::from(ErrorKind::InvalidInput))
143    ///     .attach(Error("unable to reach remote host"))
144    ///     .attach(Warning("disk nearly full"))
145    ///     .attach(Error("cannot resolve example.com: unknown host"));
146    ///
147    /// # Report::set_color_mode(error_stack::fmt::ColorMode::Emphasis);
148    /// # fn render(value: String) -> String {
149    /// #     let backtrace = regex::Regex::new(r"backtrace no\. (\d+)\n(?:  .*\n)*  .*").unwrap();
150    /// #     let backtrace_info = regex::Regex::new(r"backtrace( with (\d+) frames)? \((\d+)\)").unwrap();
151    /// #
152    /// #     let value = backtrace.replace_all(&value, "backtrace no. $1\n  [redacted]");
153    /// #     let value = backtrace_info.replace_all(value.as_ref(), "backtrace ($3)");
154    /// #
155    /// #     ansi_to_html::convert(value.as_ref()).unwrap()
156    /// # }
157    /// #
158    /// # #[cfg(nightly)]
159    /// # expect_test::expect_file![concat!(env!("CARGO_MANIFEST_DIR"), "/tests/snapshots/doc/fmt__hookcontext_cast.snap")].assert_eq(&render(format!("{report:?}")));
160    /// #
161    /// println!("{report:?}");
162    /// ```
163    ///
164    /// <pre>
165    #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/snapshots/doc/fmt__hookcontext_cast.snap"))]
166    /// </pre>
167    #[must_use]
168    pub fn cast<U>(&mut self) -> &mut HookContext<U> {
169        // SAFETY: `HookContext` is marked as repr(transparent) and the changed generic is only used
170        // inside of the `PhantomData`
171        unsafe { &mut *(self as *mut Self).cast::<HookContext<U>>() }
172    }
173}
174
175#[cfg(any(feature = "std", feature = "hooks"))]
176impl<T: 'static> HookContext<T> {
177    /// Return a reference to a value of type `U`, if a value of that type exists.
178    ///
179    /// Values returned are isolated and "bound" to `T`, this means that [`HookContext<_, Warning>`]
180    /// and [`HookContext<_, Error>`] do not share the same values. Values are only valid during the
181    /// invocation of the corresponding call (e.g. [`Debug`]).
182    ///
183    /// [`Debug`]: core::fmt::Debug
184    #[must_use]
185    pub fn get<U: 'static>(&self) -> Option<&U> {
186        self.storage()
187            .get(&TypeId::of::<T>())?
188            .get(&TypeId::of::<U>())?
189            .downcast_ref()
190    }
191
192    /// Return a mutable reference to a value of type `U`, if a value of that type exists.
193    ///
194    /// Values returned are isolated and "bound" to `T`, this means that [`HookContext<_, Warning>`]
195    /// and [`HookContext<_, Error>`] do not share the same values. Values are only valid during the
196    /// invocation of the corresponding call (e.g. [`Debug`]).
197    ///
198    /// [`Debug`]: core::fmt::Debug
199    pub fn get_mut<U: 'static>(&mut self) -> Option<&mut U> {
200        self.storage_mut()
201            .get_mut(&TypeId::of::<T>())?
202            .get_mut(&TypeId::of::<U>())?
203            .downcast_mut()
204    }
205
206    /// Insert a new value of type `U` into the storage of [`HookContext`].
207    ///
208    /// The returned value will the previously stored value of the same type `U` scoped over type
209    /// `T`, if it existed, did no such value exist it will return [`None`].
210    pub fn insert<U: 'static>(&mut self, value: U) -> Option<U> {
211        self.storage_mut()
212            .entry(TypeId::of::<T>())
213            .or_default()
214            .insert(TypeId::of::<U>(), Box::new(value))?
215            .downcast()
216            .map(|boxed| *boxed)
217            .ok()
218    }
219
220    /// Remove the value of type `U` from the storage of [`HookContext`] if it existed.
221    ///
222    /// The returned value will be the previously stored value of the same type `U` if it existed in
223    /// the scope of `T`, did no such value exist, it will return [`None`].
224    pub fn remove<U: 'static>(&mut self) -> Option<U> {
225        self.storage_mut()
226            .get_mut(&TypeId::of::<T>())?
227            .remove(&TypeId::of::<U>())?
228            .downcast()
229            .map(|boxed| *boxed)
230            .ok()
231    }
232
233    /// One of the most common interactions with [`HookContext`] is a counter to reference previous
234    /// frames in an entry to the appendix that was added using [`HookContext::push_appendix`].
235    ///
236    /// This is a utility method, which uses the other primitive methods provided to automatically
237    /// increment a counter, if the counter wasn't initialized this method will return `0`.
238    ///
239    /// ```rust
240    /// # // we only test with nightly, which means that `render()` is unused on earlier version
241    /// # #![cfg_attr(not(nightly), allow(dead_code, unused_variables, unused_imports))]
242    /// use std::io::ErrorKind;
243    ///
244    /// use error_stack::Report;
245    ///
246    /// struct Suggestion(&'static str);
247    ///
248    /// Report::install_debug_hook::<Suggestion>(|Suggestion(value), context| {
249    ///     let idx = context.increment_counter();
250    ///     context.push_body(format!("suggestion {idx}: {value}"));
251    /// });
252    ///
253    /// let report = Report::new(std::io::Error::from(ErrorKind::InvalidInput))
254    ///     .attach(Suggestion("use a file you can read next time!"))
255    ///     .attach(Suggestion("don't press any random keys!"));
256    ///
257    /// # Report::set_color_mode(error_stack::fmt::ColorMode::Emphasis);
258    /// # fn render(value: String) -> String {
259    /// #     let backtrace = regex::Regex::new(r"backtrace no\. (\d+)\n(?:  .*\n)*  .*").unwrap();
260    /// #     let backtrace_info = regex::Regex::new(r"backtrace( with (\d+) frames)? \((\d+)\)").unwrap();
261    /// #
262    /// #     let value = backtrace.replace_all(&value, "backtrace no. $1\n  [redacted]");
263    /// #     let value = backtrace_info.replace_all(value.as_ref(), "backtrace ($3)");
264    /// #
265    /// #     ansi_to_html::convert(value.as_ref()).unwrap()
266    /// # }
267    /// #
268    /// # #[cfg(nightly)]
269    /// # expect_test::expect_file![concat!(env!("CARGO_MANIFEST_DIR"), "/tests/snapshots/doc/fmt__hookcontext_increment.snap")].assert_eq(&render(format!("{report:?}")));
270    /// #
271    /// println!("{report:?}");
272    /// ```
273    ///
274    /// <pre>
275    #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/snapshots/doc/fmt__hookcontext_increment.snap"))]
276    /// </pre>
277    ///
278    /// [`Debug`]: core::fmt::Debug
279    pub fn increment_counter(&mut self) -> isize {
280        let counter = self.get_mut::<$crate::hook::context::Counter>();
281
282        match counter {
283            None => {
284                // if the counter hasn't been set yet, default to `0`
285                self.insert($crate::hook::context::Counter::new(0));
286
287                0
288            }
289            Some(ctr) => {
290                ctr.increment();
291
292                ctr.as_inner()
293            }
294        }
295    }
296
297    /// One of the most common interactions with [`HookContext`] is a counter to reference previous
298    /// frames in an entry to the appendix that was added using [`HookContext::push_appendix`].
299    ///
300    /// This is a utility method, which uses the other primitive method provided to automatically
301    /// decrement a counter, if the counter wasn't initialized this method will return `-1` to stay
302    /// consistent with [`HookContext::increment_counter`].
303    ///
304    /// ```rust
305    /// # // we only test with nightly, which means that `render()` is unused on earlier version
306    /// # #![cfg_attr(not(nightly), allow(dead_code, unused_variables, unused_imports))]
307    /// use std::io::ErrorKind;
308    ///
309    /// use error_stack::Report;
310    ///
311    /// struct Suggestion(&'static str);
312    ///
313    /// Report::install_debug_hook::<Suggestion>(|Suggestion(value), context| {
314    ///     let idx = context.decrement_counter();
315    ///     context.push_body(format!("suggestion {idx}: {value}"));
316    /// });
317    ///
318    /// let report = Report::new(std::io::Error::from(ErrorKind::InvalidInput))
319    ///     .attach(Suggestion("use a file you can read next time!"))
320    ///     .attach(Suggestion("don't press any random keys!"));
321    ///
322    /// # Report::set_color_mode(error_stack::fmt::ColorMode::Emphasis);
323    /// # fn render(value: String) -> String {
324    /// #     let backtrace = regex::Regex::new(r"backtrace no\. (\d+)\n(?:  .*\n)*  .*").unwrap();
325    /// #     let backtrace_info = regex::Regex::new(r"backtrace( with (\d+) frames)? \((\d+)\)").unwrap();
326    /// #
327    /// #     let value = backtrace.replace_all(&value, "backtrace no. $1\n  [redacted]");
328    /// #     let value = backtrace_info.replace_all(value.as_ref(), "backtrace ($3)");
329    /// #
330    /// #     ansi_to_html::convert(value.as_ref()).unwrap()
331    /// # }
332    /// #
333    /// # #[cfg(nightly)]
334    /// # expect_test::expect_file![concat!(env!("CARGO_MANIFEST_DIR"), "/tests/snapshots/doc/fmt__hookcontext_decrement.snap")].assert_eq(&render(format!("{report:?}")));
335    /// #
336    /// println!("{report:?}");
337    /// ```
338    ///
339    /// <pre>
340    #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/snapshots/doc/fmt__hookcontext_decrement.snap"))]
341    /// </pre>
342    pub fn decrement_counter(&mut self) -> isize {
343        let counter = self.get_mut::<$crate::hook::context::Counter>();
344
345        match counter {
346            None => {
347                // given that increment starts with `0` (which is therefore the implicit default
348                // value) decrementing the default value results in `-1`,
349                // which is why we output that value.
350                self.insert($crate::hook::context::Counter::new(-1));
351
352                -1
353            }
354            Some(ctr) => {
355                ctr.decrement();
356
357                ctr.as_inner()
358            }
359        }
360    }
361}
362    };
363}
364
365pub(crate) use impl_hook_context;