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;