1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323
use std::{error::Error, fmt, sync::RwLock};
use crate::{
fmt::{install_builtin_hooks, HookContext, Hooks},
Report, Result,
};
type FormatterHook = Box<dyn Fn(&Report<()>, &mut fmt::Formatter<'_>) -> fmt::Result + Send + Sync>;
static FMT_HOOK: RwLock<Hooks> = RwLock::new(Hooks { inner: Vec::new() });
static DEBUG_HOOK: RwLock<Option<FormatterHook>> = RwLock::new(None);
static DISPLAY_HOOK: RwLock<Option<FormatterHook>> = RwLock::new(None);
/// A hook can only be set once.
///
/// Returned by [`Report::set_debug_hook()`] or [`Report::set_display_hook()`] if a hook was already
/// set.
#[derive(Debug, Copy, Clone)]
#[non_exhaustive]
#[deprecated(
since = "0.2.0",
note = "`Report::install_debug_hook()` and `Report::install_display_hook()` are infallible"
)]
pub struct HookAlreadySet;
#[allow(deprecated)]
impl fmt::Display for HookAlreadySet {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt.write_str("Hook can only be set once")
}
}
#[allow(deprecated)]
impl Error for HookAlreadySet {}
impl Report<()> {
/// Can be used to globally set a [`Debug`] format hook, for a specific type `T`.
///
/// This hook will be called on every [`Debug`] call, if an attachment with the same type has
/// been found.
///
/// [`Debug`]: core::fmt::Debug
///
/// # Examples
///
/// ```
/// # // we only test the snapshot on rust 1.65, therefore report is unused (so is render)
/// # #![cfg_attr(not(rust_1_65), allow(dead_code, unused_variables, unused_imports))]
/// use std::io::{Error, ErrorKind};
///
/// use error_stack::{
/// report, Report,
/// };
///
/// struct Suggestion(&'static str);
///
/// Report::install_debug_hook::<Suggestion>(|value, context| {
/// context.push_body(format!("suggestion: {}", value.0));
/// });
///
/// let report =
/// report!(Error::from(ErrorKind::InvalidInput)).attach(Suggestion("oh no, try again"));
///
/// # owo_colors::set_override(true);
/// # fn render(value: String) -> String {
/// # let backtrace = regex::Regex::new(r"backtrace no\. (\d+)\n(?: .*\n)* .*").unwrap();
/// # let backtrace_info = regex::Regex::new(r"backtrace( with (\d+) frames)? \((\d+)\)").unwrap();
/// #
/// # let value = backtrace.replace_all(&value, "backtrace no. $1\n [redacted]");
/// # let value = backtrace_info.replace_all(value.as_ref(), "backtrace ($3)");
/// #
/// # ansi_to_html::convert_escaped(value.as_ref()).unwrap()
/// # }
/// #
/// # #[cfg(rust_1_65)]
/// # expect_test::expect_file![concat!(env!("CARGO_MANIFEST_DIR"), "/tests/snapshots/doc/hook__debug_hook.snap")].assert_eq(&render(format!("{report:?}")));
/// #
/// println!("{report:?}");
/// ```
///
/// Which will result in something like:
///
/// <pre>
#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/snapshots/doc/hook__debug_hook.snap"))]
/// </pre>
///
/// This example showcases the ability of hooks to be invoked for values provided via the
/// Provider API using [`Error::provide`].
///
/// ```
/// # // this is a lot of boilerplate, if you find a better way, please change this!
/// # // with #![cfg(nightly)] docsrs will complain that there's no main in non-nightly
/// # #![cfg_attr(nightly, feature(error_generic_member_access, provide_any))]
/// # const _: &'static str = r#"
/// #![feature(error_generic_member_access, provide_any)]
/// # "#;
///
/// # #[cfg(nightly)]
/// # mod nightly {
/// use std::any::Demand;
/// use std::error::Error;
/// use std::fmt::{Display, Formatter};
/// use error_stack::{Report, report};
///
/// struct Suggestion(&'static str);
///
/// #[derive(Debug)]
/// struct ErrorCode(u64);
///
///
/// #[derive(Debug)]
/// struct UserError {
/// code: ErrorCode
/// }
///
/// impl Display for UserError {
/// fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
/// f.write_str("invalid user input")
/// }
/// }
///
/// impl Error for UserError {
/// fn provide<'a>(&'a self, req: &mut Demand<'a>) {
/// req.provide_value(Suggestion("try better next time!"));
/// req.provide_ref(&self.code);
/// }
/// }
///
/// # pub fn main() {
/// Report::install_debug_hook::<Suggestion>(|Suggestion(value), context| {
/// context.push_body(format!("suggestion: {value}"));
/// });
/// Report::install_debug_hook::<ErrorCode>(|ErrorCode(value), context| {
/// context.push_body(format!("error code: {value}"));
/// });
///
/// let report = report!(UserError {code: ErrorCode(420)});
///
/// # owo_colors::set_override(true);
/// # fn render(value: String) -> String {
/// # let backtrace = regex::Regex::new(r"backtrace no\. (\d+)\n(?: .*\n)* .*").unwrap();
/// # let backtrace_info = regex::Regex::new(r"backtrace( with (\d+) frames)? \((\d+)\)").unwrap();
/// #
/// # let value = backtrace.replace_all(&value, "backtrace no. $1\n [redacted]");
/// # let value = backtrace_info.replace_all(value.as_ref(), "backtrace ($3)");
/// #
/// # ansi_to_html::convert_escaped(value.as_ref()).unwrap()
/// # }
/// #
/// # expect_test::expect_file![concat!(env!("CARGO_MANIFEST_DIR"), "/tests/snapshots/doc/hook__debug_hook_provide.snap")].assert_eq(&render(format!("{report:?}")));
/// #
/// println!("{report:?}");
/// # }
/// # }
/// # #[cfg(not(nightly))]
/// # fn main() {}
/// # #[cfg(nightly)]
/// # fn main() {nightly::main()}
/// ```
///
/// Which will result in something like:
///
/// <pre>
#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/snapshots/doc/hook__debug_hook_provide.snap"))]
/// </pre>
#[cfg(feature = "std")]
pub fn install_debug_hook<T: Send + Sync + 'static>(
hook: impl Fn(&T, &mut HookContext<T>) + Send + Sync + 'static,
) {
install_builtin_hooks();
let mut lock = FMT_HOOK.write().expect("should not be poisoned");
lock.insert(hook);
}
/// Returns the hook that was previously set by [`install_debug_hook`]
///
/// [`install_debug_hook`]: Self::install_debug_hook
#[cfg(feature = "std")]
pub(crate) fn invoke_debug_format_hook<T>(closure: impl FnOnce(&Hooks) -> T) -> T {
install_builtin_hooks();
let hook = FMT_HOOK.read().expect("should not be poisoned");
closure(&hook)
}
/// Globally sets a hook which is called when formatting [`Report`] with the [`Debug`] trait.
///
/// By intercepting the default [`Debug`] implementation, this hook adds the possibility for
/// downstream crates to provide their own formatting like colored output or a machine-readable
/// output (i.e. JSON).
///
/// If not set, [`Debug`] will print
/// * The latest error
/// * The errors causes
/// * The [`Backtrace`] and [`SpanTrace`] **if captured**
///
/// [`Debug`]: core::fmt::Debug
/// [`Backtrace`]: std::backtrace::Backtrace
/// [`SpanTrace`]: tracing_error::SpanTrace
///
/// # Note
///
/// Since `0.2` this will overwrite the previous hook (if set) instead of returning
/// [`HookAlreadySet`].
///
/// # Errors
///
/// No longer returns an error since version `0.2`, the return value has been preserved for
/// compatibility.
///
/// # Example
///
/// ```
/// use std::io::{Error, ErrorKind};
///
/// use error_stack::{report, Report};
///
/// #[allow(deprecated)]
/// # fn main() -> Result<(), Report<error_stack::HookAlreadySet>> {
/// # #[allow(deprecated)]
/// Report::set_debug_hook(|_, fmt| write!(fmt, "custom debug implementation"))?;
///
/// let report = report!(Error::from(ErrorKind::InvalidInput));
/// assert_eq!(format!("{report:?}"), "custom debug implementation");
/// # Ok(()) }
/// ```
#[deprecated(since = "0.2.0", note = "use Report::install_debug_hook() instead")]
#[cfg(feature = "std")]
#[allow(deprecated)]
pub fn set_debug_hook<H>(hook: H) -> Result<(), HookAlreadySet>
where
H: Fn(&Self, &mut fmt::Formatter) -> fmt::Result + Send + Sync + 'static,
{
let mut write = DEBUG_HOOK.write().expect("should not poisoned");
*write = Some(Box::new(hook));
Ok(())
}
/// Returns the hook that was previously set by [`set_debug_hook`], if any.
///
/// [`set_debug_hook`]: Self::set_debug_hook
#[cfg(feature = "std")]
pub(crate) fn invoke_debug_hook<T>(closure: impl FnOnce(&FormatterHook) -> T) -> Option<T> {
let hook = DEBUG_HOOK.read().expect("should not poisoned");
hook.as_ref().map(|hook| closure(hook))
}
/// Globally sets a hook that is called when formatting [`Report`] with the [`Display`] trait.
///
/// By intercepting the default [`Display`] implementation, this hook adds the possibility
/// for downstream crates to provide their own formatting like colored output or a
/// machine-readable output (i.e. JSON).
///
/// If not set, [`Display`] will print the latest error and, if alternate formatting is enabled
/// (`"{:#}"`) and it exists, its direct cause.
///
/// [`Display`]: fmt::Display
///
/// # Note
///
/// Since `0.2` this will overwrite the previous hook (if set) instead of returning
/// [`HookAlreadySet`].
///
/// # Errors
///
/// No longer returns an error since version `0.2`, the return value has been preserved for
/// compatibility.
///
/// # Example
///
/// ```
/// use std::io::{Error, ErrorKind};
///
/// use error_stack::{report, Report};
///
/// #[allow(deprecated)]
/// # fn main() -> Result<(), Report<error_stack::HookAlreadySet>> {
/// # #[allow(deprecated)]
/// Report::set_display_hook(|_, fmt| write!(fmt, "custom display implementation"))?;
///
/// let report = report!(Error::from(ErrorKind::InvalidInput));
/// assert_eq!(report.to_string(), "custom display implementation");
/// # Ok(()) }
/// ```
#[deprecated(since = "0.2.0")]
#[cfg(feature = "std")]
#[allow(deprecated)]
pub fn set_display_hook<H>(hook: H) -> Result<(), HookAlreadySet>
where
H: Fn(&Self, &mut fmt::Formatter) -> fmt::Result + Send + Sync + 'static,
{
let mut write = DISPLAY_HOOK.write().expect("should not poisoned");
*write = Some(Box::new(hook));
Ok(())
}
/// Returns the hook that was previously set by [`set_display_hook`], if any.
///
/// [`set_display_hook`]: Self::set_display_hook
#[cfg(feature = "std")]
pub(crate) fn invoke_display_hook<T>(closure: impl FnOnce(&FormatterHook) -> T) -> Option<T> {
let hook = DISPLAY_HOOK.read().expect("should not poisoned");
hook.as_ref().map(|hook| closure(hook))
}
}
impl<T> Report<T> {
/// Converts the `&Report<T>` to `&Report<()>` without modifying the frame stack.
///
/// Changing `Report<T>` to `Report<()>` is only used internally for calling
/// [`invoke_debug_hook`] and [`invoke_display_hook`] and is intentionally not exposed.
///
/// [`invoke_debug_hook`]: Self::invoke_debug_hook
/// [`invoke_display_hook`]: Self::invoke_display_hook
pub(crate) const fn generalized(&self) -> &Report<()> {
// SAFETY: `Report` is repr(transparent), so it's safe to cast between `Report<A>` and
// `Report<B>`
unsafe { &*(self as *const Self).cast() }
}
}