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