rootcause_preformat/lib.rs
1#![cfg_attr(not(doc), no_std)]
2#![deny(
3 missing_docs,
4 elided_lifetimes_in_paths,
5 unsafe_code,
6 rustdoc::invalid_rust_codeblocks,
7 rustdoc::broken_intra_doc_links,
8 missing_copy_implementations,
9 unused_doc_comments
10)]
11// Extra checks on nightly
12#![cfg_attr(nightly_extra_checks, feature(rustdoc_missing_doc_code_examples))]
13#![cfg_attr(nightly_extra_checks, forbid(rustdoc::missing_doc_code_examples))]
14
15//! Preformatting extensions for [`rootcause`] error reports.
16//!
17//! This crate adds the ability to turn a [`Report`] into a version where every
18//! context and attachment has been formatted into a `String` and the original
19//! types have been erased. The two storage types ([`PreformattedContext`] and
20//! [`PreformattedAttachment`]) plus a handful of extension traits are exposed:
21//!
22//! - [`PreformatReportExt::preformat`] — preformat an entire report tree.
23//! - [`PreformatAttachmentExt::preformat`] — preformat a single attachment.
24//! - [`PreformatRootExt::preformat_root`] — extract the typed root context and
25//! return a preformatted report alongside it.
26//! - [`ContextTransformNestedExt::context_transform_nested`] — transform the
27//! root context while nesting the original report as a preformatted child.
28//!
29//! # Why preformat?
30//!
31//! - **Regaining mutability**: After preformatting, you get back a [`Mutable`]
32//! report even if the original was [`Cloneable`].
33//! - **Thread safety**: Non-`Send`/`Sync` error types can be preformatted to
34//! produce a `Send + Sync` report that can cross thread boundaries.
35//! - **Preserving formatting**: The preformatted version will always display
36//! the same way, even if the original types or hooks are no longer
37//! available.
38//!
39//! # Quick Start
40//!
41//! ```
42//! use rootcause::{
43//! markers::{Mutable, SendSync},
44//! prelude::*,
45//! };
46//! use rootcause_preformat::{PreformatReportExt, PreformattedContext};
47//!
48//! let report: Report = report!("database connection failed");
49//! let preformatted: Report<PreformattedContext, Mutable, SendSync> = report.preformat();
50//!
51//! // The preformatted report displays identically to the original
52//! assert_eq!(format!("{}", report), format!("{}", preformatted));
53//! ```
54//!
55//! [`Mutable`]: rootcause::markers::Mutable
56//! [`Cloneable`]: rootcause::markers::Cloneable
57
58extern crate alloc;
59
60use rootcause::{
61 Report, ReportMut, ReportRef, handlers,
62 markers::{self, Mutable, ReportOwnershipMarker, SendSync},
63 report_attachment::{ReportAttachment, ReportAttachmentMut, ReportAttachmentRef},
64};
65
66mod preformatted;
67
68pub use preformatted::{PreformattedAttachment, PreformattedContext};
69
70/// Extension trait providing [`preformat`](Self::preformat) on [`Report`],
71/// [`ReportRef`], and [`ReportMut`].
72///
73/// # Examples
74///
75/// ```
76/// use rootcause::prelude::*;
77/// use rootcause_preformat::{PreformatReportExt, PreformattedContext};
78///
79/// let report: Report = report!("boom");
80/// let preformatted: Report<PreformattedContext, _, _> = report.preformat();
81/// ```
82pub trait PreformatReportExt {
83 /// Creates a new report, which has the same structure as the current
84 /// report, but has all the contexts and attachments preformatted.
85 ///
86 /// This can be useful, as the new report is mutable because it was just
87 /// created, and additionally the new report is [`Send`]+[`Sync`].
88 ///
89 /// # Examples
90 /// ```
91 /// # use rootcause::{prelude::*, ReportMut};
92 /// # use rootcause_preformat::{PreformatReportExt, PreformattedContext};
93 /// # #[derive(Default)]
94 /// # struct NonSendSyncError(core::cell::Cell<()>);
95 /// # let non_send_sync_error = NonSendSyncError::default();
96 /// # let mut report = report!(non_send_sync_error);
97 /// let report_mut: ReportMut<'_, NonSendSyncError, markers::Local> = report.as_mut();
98 /// let preformatted: Report<PreformattedContext, markers::Mutable, markers::SendSync> =
99 /// report_mut.preformat();
100 /// assert_eq!(format!("{report}"), format!("{preformatted}"));
101 /// ```
102 #[track_caller]
103 #[must_use]
104 fn preformat(&self) -> Report<PreformattedContext, Mutable, SendSync>;
105}
106
107/// Extension trait providing [`preformat`](Self::preformat) on
108/// [`ReportAttachment`], [`ReportAttachmentRef`], and [`ReportAttachmentMut`].
109///
110/// # Examples
111///
112/// ```
113/// use rootcause::report_attachment::ReportAttachment;
114/// use rootcause_preformat::PreformatAttachmentExt;
115///
116/// let attachment = ReportAttachment::new_sendsync(42i32);
117/// let preformatted = attachment.preformat();
118/// assert_eq!(
119/// attachment.format_inner().to_string(),
120/// preformatted.format_inner().to_string()
121/// );
122/// ```
123pub trait PreformatAttachmentExt {
124 /// Creates a new attachment, with the inner attachment data preformatted.
125 ///
126 /// This can be useful, as the preformatted attachment is a newly allocated
127 /// object and additionally is [`Send`]+[`Sync`].
128 ///
129 /// See [`PreformattedAttachment`] for more information.
130 ///
131 /// # Examples
132 ///
133 /// ```
134 /// use rootcause::report_attachment::ReportAttachment;
135 /// use rootcause_preformat::PreformatAttachmentExt;
136 ///
137 /// let attachment = ReportAttachment::new_sendsync(42i32);
138 /// let preformatted = attachment.preformat();
139 /// ```
140 #[track_caller]
141 #[must_use]
142 fn preformat(&self) -> ReportAttachment<PreformattedAttachment, SendSync>;
143}
144
145impl<A: ?Sized, T> PreformatAttachmentExt for ReportAttachment<A, T> {
146 fn preformat(&self) -> ReportAttachment<PreformattedAttachment, SendSync> {
147 self.as_ref().preformat()
148 }
149}
150
151impl<'a, A: ?Sized> PreformatAttachmentExt for ReportAttachmentMut<'a, A> {
152 fn preformat(&self) -> ReportAttachment<PreformattedAttachment, SendSync> {
153 self.as_ref().preformat()
154 }
155}
156
157impl<'a, A: ?Sized> PreformatAttachmentExt for ReportAttachmentRef<'a, A> {
158 fn preformat(&self) -> ReportAttachment<PreformattedAttachment, SendSync> {
159 ReportAttachment::new_custom::<preformatted::PreformattedHandler>(
160 PreformattedAttachment::new_from_attachment(*self),
161 )
162 }
163}
164
165/// Extension trait providing [`preformat_root`](Self::preformat_root) on
166/// [`Report`] with a [`Mutable`] root.
167///
168/// # Examples
169///
170/// ```
171/// use rootcause::prelude::*;
172/// use rootcause_preformat::{PreformatRootExt, PreformattedContext};
173///
174/// #[derive(Debug)]
175/// struct Boom;
176/// # impl std::fmt::Display for Boom {
177/// # fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "boom") }
178/// # }
179///
180/// let report: Report<Boom> = report!(Boom);
181/// let (_context, _preformatted): (Boom, Report<PreformattedContext>) = report.preformat_root();
182/// ```
183pub trait PreformatRootExt<C, T>: Sized {
184 /// Extracts the context and returns it with a preformatted version of the
185 /// report.
186 ///
187 /// Returns a tuple: the original typed context and a new report with
188 /// [`PreformattedContext`] containing the string representation. The
189 /// preformatted report maintains the same structure (children and
190 /// attachments). Useful when you need the typed value for processing and
191 /// the formatted version for display.
192 ///
193 /// This is a lower-level method primarily for custom transformation logic.
194 /// Most users should use
195 /// [`context_transform_nested`](ContextTransformNestedExt::context_transform_nested)
196 /// instead.
197 ///
198 /// See also: [`preformat`](PreformatReportExt::preformat) (formats entire
199 /// hierarchy), [`into_parts`](rootcause::Report::into_parts) (extracts
200 /// without formatting),
201 /// [`current_context`](rootcause::ReportRef::current_context) (reference
202 /// without extraction).
203 ///
204 /// # Examples
205 ///
206 /// ```
207 /// # use rootcause::prelude::*;
208 /// # use rootcause_preformat::{PreformatRootExt, PreformattedContext};
209 /// # #[derive(Debug)]
210 /// struct MyError {
211 /// code: u32
212 /// }
213 /// # impl std::fmt::Display for MyError {
214 /// # fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "error {}", self.code) }
215 /// # }
216 ///
217 /// let report: Report<MyError> = report!(MyError { code: 500 });
218 /// let (context, preformatted): (MyError, Report<PreformattedContext>) = report.preformat_root();
219 /// ```
220 #[track_caller]
221 #[must_use]
222 fn preformat_root(self) -> (C, Report<PreformattedContext, Mutable, T>)
223 where
224 PreformattedContext: markers::ObjectMarkerFor<T>;
225}
226
227/// Extension trait providing
228/// [`context_transform_nested`](Self::context_transform_nested) on [`Report`]
229/// with a [`Mutable`] root, and on `Result<_, Report<_, Mutable, _>>`.
230///
231/// # Examples
232///
233/// ```
234/// use rootcause::prelude::*;
235/// use rootcause_preformat::ContextTransformNestedExt;
236///
237/// #[derive(Debug)]
238/// struct Inner;
239/// # impl std::fmt::Display for Inner {
240/// # fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "inner") }
241/// # }
242/// #[derive(Debug)]
243/// struct Outer(Inner);
244/// # impl std::fmt::Display for Outer {
245/// # fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "outer") }
246/// # }
247///
248/// let inner: Report<Inner> = report!(Inner);
249/// let wrapped: Report<Outer> = inner.context_transform_nested(Outer);
250/// ```
251pub trait ContextTransformNestedExt<C, T>: Sized {
252 /// The output type after transforming the context to `D`. Either
253 /// `Report<D, Mutable, T>` or `Result<V, Report<D, Mutable, T>>`. See
254 /// [`context_transform_nested`](Self::context_transform_nested) for an
255 /// example.
256 type Output<D: 'static>;
257
258 /// Transforms the context and nests the original report as a preformatted
259 /// child.
260 ///
261 /// Creates a new parent node with fresh hook data (location, backtrace),
262 /// but the original context type is lost—the child becomes
263 /// [`PreformattedContext`] and cannot be downcast.
264 ///
265 /// # Examples
266 ///
267 /// ```
268 /// # use rootcause::prelude::*;
269 /// # use rootcause_preformat::ContextTransformNestedExt;
270 /// # #[derive(Debug)]
271 /// # struct LibError;
272 /// # impl std::fmt::Display for LibError {
273 /// # fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "lib error") }
274 /// # }
275 /// # #[derive(Debug)]
276 /// enum AppError {
277 /// Lib(LibError)
278 /// }
279 /// # impl std::fmt::Display for AppError {
280 /// # fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "app error") }
281 /// # }
282 ///
283 /// let lib_report: Report<LibError> = report!(LibError);
284 /// let app_report: Report<AppError> = lib_report.context_transform_nested(AppError::Lib);
285 /// ```
286 ///
287 /// # See Also
288 ///
289 /// - [`context()`](rootcause::Report::context) - Adds new parent, preserves
290 /// child's type
291 /// - [`preformat_root()`](PreformatRootExt::preformat_root) - Lower-level
292 /// operation used internally
293 /// - [`examples/context_methods.rs`] - Comparison guide
294 ///
295 /// [`examples/context_methods.rs`]: https://github.com/rootcause-rs/rootcause/blob/main/examples/context_methods.rs
296 #[track_caller]
297 #[must_use]
298 fn context_transform_nested<F, D>(self, f: F) -> Self::Output<D>
299 where
300 F: FnOnce(C) -> D,
301 D: markers::ObjectMarkerFor<T> + core::fmt::Display + core::fmt::Debug,
302 PreformattedContext: markers::ObjectMarkerFor<T>;
303}
304
305impl<C: ?Sized, O, T> PreformatReportExt for Report<C, O, T>
306where
307 O: ReportOwnershipMarker,
308{
309 fn preformat(&self) -> Report<PreformattedContext, Mutable, SendSync> {
310 self.as_ref().preformat()
311 }
312}
313
314impl<'a, C: ?Sized, T> PreformatReportExt for ReportMut<'a, C, T> {
315 fn preformat(&self) -> Report<PreformattedContext, Mutable, SendSync> {
316 self.as_ref().preformat()
317 }
318}
319
320impl<'a, C: ?Sized, O, T> PreformatReportExt for ReportRef<'a, C, O, T> {
321 fn preformat(&self) -> Report<PreformattedContext, Mutable, SendSync> {
322 let preformatted_context = PreformattedContext::new_from_context(*self);
323 Report::from_parts_unhooked::<preformatted::PreformattedHandler>(
324 preformatted_context,
325 self.children()
326 .iter()
327 .map(|sub_report| sub_report.preformat())
328 .collect(),
329 self.attachments()
330 .iter()
331 .map(|attachment| attachment.preformat().into_dynamic())
332 .collect(),
333 )
334 }
335}
336
337impl<C, T> PreformatRootExt<C, T> for Report<C, Mutable, T> {
338 fn preformat_root(self) -> (C, Report<PreformattedContext, Mutable, T>)
339 where
340 PreformattedContext: markers::ObjectMarkerFor<T>,
341 {
342 let preformatted = PreformattedContext::new_from_context(self.as_ref());
343 let (context, children, attachments) = self.into_parts();
344
345 (
346 context,
347 Report::from_parts_unhooked::<preformatted::PreformattedHandler>(
348 preformatted,
349 children,
350 attachments,
351 ),
352 )
353 }
354}
355
356impl<C, T> ContextTransformNestedExt<C, T> for Report<C, Mutable, T> {
357 type Output<D: 'static> = Report<D, Mutable, T>;
358
359 fn context_transform_nested<F, D>(self, f: F) -> Report<D, Mutable, T>
360 where
361 F: FnOnce(C) -> D,
362 D: markers::ObjectMarkerFor<T> + core::fmt::Display + core::fmt::Debug,
363 PreformattedContext: markers::ObjectMarkerFor<T>,
364 {
365 let (context, report) = self.preformat_root();
366 report.context_custom::<handlers::Display, _>(f(context))
367 }
368}
369
370impl<V, C, T> ContextTransformNestedExt<C, T> for Result<V, Report<C, Mutable, T>> {
371 type Output<D: 'static> = Result<V, Report<D, Mutable, T>>;
372
373 fn context_transform_nested<F, D>(self, f: F) -> Result<V, Report<D, Mutable, T>>
374 where
375 F: FnOnce(C) -> D,
376 D: markers::ObjectMarkerFor<T> + core::fmt::Display + core::fmt::Debug,
377 PreformattedContext: markers::ObjectMarkerFor<T>,
378 {
379 match self {
380 Ok(value) => Ok(value),
381 Err(report) => {
382 let (context, report) = report.preformat_root();
383 Err(report.context_custom::<handlers::Display, _>(f(context)))
384 }
385 }
386 }
387}
388
389#[cfg(test)]
390mod tests {
391 use alloc::format;
392 use core::any::TypeId;
393
394 use rootcause::{
395 ReportRef,
396 markers::{Local, Mutable, SendSync, Uncloneable},
397 prelude::*,
398 report_attachment::ReportAttachment,
399 };
400
401 use super::*;
402
403 #[derive(Debug)]
404 struct DemoError(u32);
405
406 impl core::fmt::Display for DemoError {
407 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
408 write!(f, "demo {}", self.0)
409 }
410 }
411
412 #[derive(Debug)]
413 struct Wrapper(#[allow(dead_code)] DemoError);
414
415 impl core::fmt::Display for Wrapper {
416 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
417 write!(f, "wrapper")
418 }
419 }
420
421 #[test]
422 fn test_preformat() {
423 #[derive(Default)]
424 struct NonSendSyncError(core::cell::Cell<()>);
425 let non_send_sync_error = NonSendSyncError::default();
426 let report = report!(non_send_sync_error);
427 let report_ref: ReportRef<'_, NonSendSyncError, Uncloneable, Local> = report.as_ref();
428 let preformatted: Report<PreformattedContext, Mutable, SendSync> = report_ref.preformat();
429 assert_eq!(format!("{report}"), format!("{preformatted}"));
430 }
431
432 #[test]
433 fn test_preformat_root_extracts_typed_context() {
434 let report: Report<DemoError> = report!(DemoError(7)).attach("ctx-detail");
435 let display_before = format!("{report}");
436 let attachments_before = report.attachments().len();
437
438 let (context, preformatted) = report.preformat_root();
439
440 assert_eq!(context.0, 7);
441 assert_eq!(format!("{preformatted}"), display_before);
442 assert_eq!(
443 preformatted.current_context().original_type_id(),
444 TypeId::of::<DemoError>(),
445 );
446 assert_eq!(preformatted.attachments().len(), attachments_before);
447 }
448
449 #[test]
450 fn test_context_transform_nested_on_report() {
451 let inner: Report<DemoError> = report!(DemoError(3));
452
453 let outer: Report<Wrapper> = inner.context_transform_nested(Wrapper);
454
455 assert_eq!(outer.current_context_type_id(), TypeId::of::<Wrapper>());
456 assert_eq!(outer.iter_sub_reports().count(), 1);
457 let child = outer.children().get(0).unwrap();
458 assert_eq!(
459 child.current_context_type_id(),
460 TypeId::of::<PreformattedContext>(),
461 );
462 let child_typed = child
463 .downcast_report::<PreformattedContext>()
464 .expect("child should be PreformattedContext");
465 assert_eq!(
466 child_typed.current_context().original_type_id(),
467 TypeId::of::<DemoError>(),
468 );
469 }
470
471 #[test]
472 fn test_context_transform_nested_on_result_ok_passes_through() {
473 let ok: Result<i32, Report<DemoError>> = Ok(42);
474 let mapped: Result<i32, Report<Wrapper>> = ok.context_transform_nested(Wrapper);
475 assert_eq!(mapped.unwrap(), 42);
476 }
477
478 #[test]
479 fn test_context_transform_nested_on_result_err_wraps() {
480 let err: Result<i32, Report<DemoError>> = Err(report!(DemoError(9)));
481 let mapped: Result<i32, Report<Wrapper>> = err.context_transform_nested(Wrapper);
482
483 let outer = mapped.unwrap_err();
484 assert_eq!(outer.current_context_type_id(), TypeId::of::<Wrapper>());
485 assert_eq!(outer.iter_sub_reports().count(), 1);
486 let child = outer.children().get(0).unwrap();
487 let child_typed = child
488 .downcast_report::<PreformattedContext>()
489 .expect("child should be PreformattedContext");
490 assert_eq!(
491 child_typed.current_context().original_type_id(),
492 TypeId::of::<DemoError>(),
493 );
494 }
495
496 #[test]
497 fn test_preformat_attachment_owned_ref_mut() {
498 let mut attachment = ReportAttachment::new_sendsync(42u32);
499 let display = format!("{}", attachment.format_inner());
500 let debug = format!("{:?}", attachment.format_inner());
501
502 let from_owned = attachment.preformat();
503 let from_ref = attachment.as_ref().preformat();
504 let from_mut = attachment.as_mut().preformat();
505
506 for preformatted in [&from_owned, &from_ref, &from_mut] {
507 assert_eq!(format!("{}", preformatted.format_inner()), display);
508 assert_eq!(format!("{:?}", preformatted.format_inner()), debug);
509 assert_eq!(preformatted.inner().original_type_id(), TypeId::of::<u32>(),);
510 }
511 }
512}