Skip to main content

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}