error_stack/hook/mod.rs
1pub(crate) mod context;
2
3#[cfg_attr(feature = "std", allow(unused_imports))]
4use alloc::vec::Vec;
5
6use crate::{
7 fmt::{install_builtin_hooks, Hooks},
8 Report,
9};
10
11#[cfg(feature = "std")]
12type RwLock<T> = std::sync::RwLock<T>;
13
14// Generally the std mutex is faster than spin, so if both `std` and `hooks` is enabled we use the
15// std variant.
16#[cfg(all(not(feature = "std"), feature = "hooks"))]
17type RwLock<T> = spin::rwlock::RwLock<T>;
18
19static FMT_HOOK: RwLock<Hooks> = RwLock::new(Hooks { inner: Vec::new() });
20
21impl Report<()> {
22 /// Can be used to globally set a [`Debug`] format hook, for a specific type `T`.
23 ///
24 /// This hook will be called on every [`Debug`] call, if an attachment with the same type has
25 /// been found.
26 ///
27 /// [`Debug`]: core::fmt::Debug
28 ///
29 /// # Examples
30 ///
31 /// ```rust
32 /// # // we only test the snapshot on nightly, therefore report is unused (so is render)
33 /// # #![cfg_attr(not(nightly), allow(dead_code, unused_variables, unused_imports))]
34 /// use std::io::{Error, ErrorKind};
35 ///
36 /// use error_stack::{
37 /// report, Report,
38 /// };
39 ///
40 /// struct Suggestion(&'static str);
41 ///
42 /// Report::install_debug_hook::<Suggestion>(|value, context| {
43 /// context.push_body(format!("suggestion: {}", value.0));
44 /// });
45 ///
46 /// let report =
47 /// report!(Error::from(ErrorKind::InvalidInput)).attach(Suggestion("oh no, try again"));
48 ///
49 /// # Report::set_color_mode(error_stack::fmt::ColorMode::Emphasis);
50 /// # fn render(value: String) -> String {
51 /// # let backtrace = regex::Regex::new(r"backtrace no\. (\d+)\n(?: .*\n)* .*").unwrap();
52 /// # let backtrace_info = regex::Regex::new(r"backtrace( with (\d+) frames)? \((\d+)\)").unwrap();
53 /// #
54 /// # let value = backtrace.replace_all(&value, "backtrace no. $1\n [redacted]");
55 /// # let value = backtrace_info.replace_all(value.as_ref(), "backtrace ($3)");
56 /// #
57 /// # ansi_to_html::convert(value.as_ref()).unwrap()
58 /// # }
59 /// #
60 /// # #[cfg(nightly)]
61 /// # expect_test::expect_file![concat!(env!("CARGO_MANIFEST_DIR"), "/tests/snapshots/doc/hook__debug_hook.snap")].assert_eq(&render(format!("{report:?}")));
62 /// #
63 /// println!("{report:?}");
64 /// ```
65 ///
66 /// Which will result in something like:
67 ///
68 /// <pre>
69 #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/snapshots/doc/hook__debug_hook.snap"))]
70 /// </pre>
71 ///
72 /// This example showcases the ability of hooks to be invoked for values provided via the
73 /// Provider API using [`Error::provide`].
74 ///
75 /// ```rust
76 /// # // this is a lot of boilerplate, if you find a better way, please change this!
77 /// # // with #![cfg(nightly)] docsrs will complain that there's no main in non-nightly
78 /// # #![cfg_attr(nightly, feature(error_generic_member_access))]
79 /// # const _: &'static str = r#"
80 /// #![feature(error_generic_member_access)]
81 /// # "#;
82 ///
83 /// # #[cfg(nightly)]
84 /// # mod nightly {
85 /// use core::error::{Request, Error};
86 /// use core::fmt::{Display, Formatter};
87 /// use error_stack::{Report, report};
88 ///
89 /// struct Suggestion(&'static str);
90 ///
91 /// #[derive(Debug)]
92 /// struct ErrorCode(u64);
93 ///
94 ///
95 /// #[derive(Debug)]
96 /// struct UserError {
97 /// code: ErrorCode
98 /// }
99 ///
100 /// impl Display for UserError {
101 /// fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
102 /// f.write_str("invalid user input")
103 /// }
104 /// }
105 ///
106 /// impl Error for UserError {
107 /// fn provide<'a>(&'a self, req: &mut Request<'a>) {
108 /// req.provide_value(Suggestion("try better next time!"));
109 /// req.provide_ref(&self.code);
110 /// }
111 /// }
112 ///
113 /// # pub fn main() {
114 /// Report::install_debug_hook::<Suggestion>(|Suggestion(value), context| {
115 /// context.push_body(format!("suggestion: {value}"));
116 /// });
117 /// Report::install_debug_hook::<ErrorCode>(|ErrorCode(value), context| {
118 /// context.push_body(format!("error code: {value}"));
119 /// });
120 ///
121 /// let report = report!(UserError {code: ErrorCode(420)});
122 ///
123 /// # Report::set_color_mode(error_stack::fmt::ColorMode::Emphasis);
124 /// # fn render(value: String) -> String {
125 /// # let backtrace = regex::Regex::new(r"backtrace no\. (\d+)\n(?: .*\n)* .*").unwrap();
126 /// # let backtrace_info = regex::Regex::new(r"backtrace( with (\d+) frames)? \((\d+)\)").unwrap();
127 /// #
128 /// # let value = backtrace.replace_all(&value, "backtrace no. $1\n [redacted]");
129 /// # let value = backtrace_info.replace_all(value.as_ref(), "backtrace ($3)");
130 /// #
131 /// # ansi_to_html::convert(value.as_ref()).unwrap()
132 /// # }
133 /// #
134 /// # expect_test::expect_file![concat!(env!("CARGO_MANIFEST_DIR"), "/tests/snapshots/doc/hook__debug_hook_provide.snap")].assert_eq(&render(format!("{report:?}")));
135 /// #
136 /// println!("{report:?}");
137 /// # }
138 /// # }
139 /// # #[cfg(not(nightly))]
140 /// # fn main() {}
141 /// # #[cfg(nightly)]
142 /// # fn main() {nightly::main()}
143 /// ```
144 ///
145 /// Which will result in something like:
146 ///
147 /// <pre>
148 #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/snapshots/doc/hook__debug_hook_provide.snap"))]
149 /// </pre>
150 ///
151 /// `error-stack` comes with some built-in hooks which can be overwritten. This is useful if you
152 /// want to change the output of the built-in hooks, or if you want to add additional
153 /// information to the output. For example, you can override the built-in hook for [`Location`]
154 /// to hide the file path:
155 ///
156 /// ```rust
157 /// # // we only test the snapshot on nightly, therefore report is unused (so is render)
158 /// # #![cfg_attr(not(nightly), allow(dead_code, unused_variables, unused_imports))]
159 /// use std::{
160 /// io::{Error, ErrorKind},
161 /// panic::Location,
162 /// };
163 ///
164 /// error_stack::Report::install_debug_hook::<Location>(|_location, _context| {
165 /// // Intentionally left empty so nothing will be printed
166 /// });
167 ///
168 /// let report = error_stack::report!(Error::from(ErrorKind::InvalidInput));
169 ///
170 /// # error_stack::Report::set_color_mode(error_stack::fmt::ColorMode::Emphasis);
171 /// # fn render(value: String) -> String {
172 /// # let backtrace = regex::Regex::new(r"backtrace no\. (\d+)\n(?: .*\n)* .*").unwrap();
173 /// # let backtrace_info = regex::Regex::new(r"backtrace( with (\d+) frames)? \((\d+)\)").unwrap();
174 /// #
175 /// # let value = backtrace.replace_all(&value, "backtrace no. $1\n [redacted]");
176 /// # let value = backtrace_info.replace_all(value.as_ref(), "backtrace ($3)");
177 /// #
178 /// # ansi_to_html::convert(value.as_ref()).unwrap()
179 /// # }
180 /// #
181 /// # #[cfg(nightly)]
182 /// # expect_test::expect_file![concat!(env!("CARGO_MANIFEST_DIR"), "/tests/snapshots/doc/hook__location_hook.snap")].assert_eq(&render(format!("{report:?}")));
183 /// #
184 /// println!("{report:?}");
185 /// ```
186 ///
187 /// Which will result in something like:
188 ///
189 /// <pre>
190 #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/snapshots/doc/hook__location_hook.snap"))]
191 /// </pre>
192 ///
193 /// [`Location`]: std::panic::Location
194 /// [`Error::provide`]: std::error::Error::provide
195 #[cfg(any(feature = "std", feature = "hooks"))]
196 pub fn install_debug_hook<T: Send + Sync + 'static>(
197 hook: impl Fn(&T, &mut crate::fmt::HookContext<T>) + Send + Sync + 'static,
198 ) {
199 install_builtin_hooks();
200
201 // TODO: Use `let ... else` when MSRV is 1.65
202 #[cfg(feature = "std")]
203 let mut lock = FMT_HOOK.write().unwrap_or_else(|_| {
204 unreachable!(
205 "Hook is poisoned. This is considered a bug and should be reported to \
206 https://github.com/hashintel/hash/issues/new/choose"
207 )
208 });
209
210 // The spin RwLock cannot panic
211 #[cfg(all(not(feature = "std"), feature = "hooks"))]
212 let mut lock = FMT_HOOK.write();
213
214 lock.insert(hook);
215 }
216
217 /// Returns the hook that was previously set by [`install_debug_hook`]
218 ///
219 /// [`install_debug_hook`]: Self::install_debug_hook
220 #[cfg(any(feature = "std", feature = "hooks"))]
221 pub(crate) fn invoke_debug_format_hook<T>(closure: impl FnOnce(&Hooks) -> T) -> T {
222 install_builtin_hooks();
223
224 // TODO: Use `let ... else` when MSRV is 1.65
225 #[cfg(feature = "std")]
226 let hook = FMT_HOOK.read().unwrap_or_else(|_| {
227 unreachable!(
228 "Hook is poisoned. This is considered a bug and should be reported to \
229 https://github.com/hashintel/hash/issues/new/choose"
230 )
231 });
232
233 // The spin RwLock cannot panic
234 #[cfg(all(not(feature = "std"), feature = "hooks"))]
235 let hook = FMT_HOOK.read();
236
237 closure(&hook)
238 }
239}