diff --git a/examples/formatting_hooks.rs b/examples/formatting_hooks.rs index efbd6b9..66e7746 100644 --- a/examples/formatting_hooks.rs +++ b/examples/formatting_hooks.rs @@ -16,7 +16,8 @@ use rootcause::{ ReportRef, handlers::{AttachmentFormattingPlacement, AttachmentFormattingStyle, FormattingFunction}, hooks::{ - Hooks, attachment_formatter::AttachmentFormatterHook, + Hooks, + attachment_formatter::{AttachmentFormatterHook, AttachmentParent}, context_formatter::ContextFormatterHook, }, markers::{Dynamic, Local, Uncloneable}, @@ -144,6 +145,49 @@ impl ContextFormatterHook for ValidationErrorFormatter { } } +// Example 4: Parent-aware formatting +// +// When the default report formatter walks an error tree, it hands every +// attachment formatter hook an `AttachmentParent` providing acces to the parent report +// and the attachment's position in that report's original attachment +// list. +// This lets a hook produce output that's contextually aware of where it +// sits in the tree. + +#[derive(Debug)] +struct RequestId(usize); + +impl core::fmt::Display for RequestId { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{}", self.0) + } +} + +struct RequestIdFormatter; + +impl AttachmentFormatterHook for RequestIdFormatter { + fn display( + &self, + attachment: ReportAttachmentRef<'_, RequestId>, + parent: Option>, + f: &mut core::fmt::Formatter<'_>, + ) -> core::fmt::Result { + let id = attachment.inner().0; + // The `parent` is `Some` when the hook is called form a `ReportFormatter` + // and `None` if the attachment is being formatted in isolation - + // such as `attachment.format_inner()` or `println!("{attachment}")` + match parent { + Some(parent) => write!( + f, + "request_id[{id}] (attachment #{} on Report<{}>)", + parent.attachment_index, + parent.report.current_context_type_name(), + ), + None => write!(f, "request {id}"), + } + } +} + // Example 1: Control attachment placement in output // Demonstrates placing verbose diagnostic data in the appendix section instead // of inline @@ -177,11 +221,19 @@ fn demo_context_formatting() -> Result<(), Report> { Err(report!(validation).into_dynamic()) } +fn demo_parent_aware_formatting() -> Result<(), Report> { + Err(report!("Outer failure") + .attach(RequestId(2)) + .attach("internal note") + .attach(RequestId(1))) +} + fn main() { // Install formatting hooks Hooks::new() .attachment_formatter::(DatabaseQueryFormatter) .attachment_formatter::(ActionRequiredFormatter) + .attachment_formatter::(RequestIdFormatter) .context_formatter::(ValidationErrorFormatter) .install() .expect("failed to install hooks"); @@ -200,6 +252,12 @@ fn main() { println!("Example 3: Context formatting\n"); match demo_context_formatting() { + Ok(()) => println!("Success"), + Err(error) => eprintln!("{error}\n"), + } + + println!("Example 4: Parent-aware attachment formatting\n"); + match demo_parent_aware_formatting() { Ok(()) => println!("Success"), Err(error) => eprintln!("{error}"), } diff --git a/src/compat/mod.rs b/src/compat/mod.rs index 054b05a..b2d73c7 100644 --- a/src/compat/mod.rs +++ b/src/compat/mod.rs @@ -68,6 +68,11 @@ //! //! See the individual module documentation for detailed integration guides and //! migration strategies. +//! +//! [`anyhow1`]: https://docs.rs/anyhow/1/anyhow +//! [`error_stack06`]: https://docs.rs/error-stack/0.6/error_stack +//! [`error_stack05`]: https://docs.rs/error-stack/0.5/error_stack +//! [`eyre06`]: https://docs.rs/eyre/0.6/eyre use crate::{ Report, ReportRef, diff --git a/src/hooks/attachment_formatter.rs b/src/hooks/attachment_formatter.rs index 428fccc..0a02a20 100644 --- a/src/hooks/attachment_formatter.rs +++ b/src/hooks/attachment_formatter.rs @@ -415,6 +415,13 @@ pub trait AttachmentFormatterHook: 'static + Send + Sync { /// * `attachment_parent` - Optional context about the parent report /// * `formatter` - The formatter to write output to /// + /// Note: `attachment_parent` is `Some` when [`Self::display`] is called from a + /// [`ReportFormatter`](crate::hooks::report_formatter::ReportFormatter) + /// (such as the built-in + /// [`DefaultReportFormatter`](crate::hooks::builtin_hooks::report_formatter::DefaultReportFormatter)) + /// that calls + /// [`format_inner_with_parent`](crate::report_attachment::ReportAttachmentRef::format_inner_with_parent)). + /// /// # Examples /// /// ``` diff --git a/src/hooks/builtin_hooks/report_formatter.rs b/src/hooks/builtin_hooks/report_formatter.rs index 6c29e5b..904a8be 100644 --- a/src/hooks/builtin_hooks/report_formatter.rs +++ b/src/hooks/builtin_hooks/report_formatter.rs @@ -64,7 +64,7 @@ use rootcause_internals::handlers::{ use crate::{ ReportRef, - hooks::report_formatter::ReportFormatter, + hooks::{attachment_formatter::AttachmentParent, report_formatter::ReportFormatter}, markers::{Dynamic, Local, Uncloneable}, report_attachment::ReportAttachmentRef, }; @@ -815,7 +815,11 @@ impl NodeConfig { } type Appendices<'a> = IndexMap< &'static str, - Vec<(ReportAttachmentRef<'a, Dynamic>, FormattingFunction)>, + Vec<( + ReportAttachmentRef<'a, Dynamic>, + AttachmentParent<'a>, + FormattingFunction, + )>, rustc_hash::FxBuildHasher, >; @@ -841,7 +845,11 @@ impl ReportFormatter for DefaultReportFormatter { } type TmpValueBuffer = String; -type TmpAttachmentsBuffer<'a> = Vec<(AttachmentFormattingStyle, ReportAttachmentRef<'a, Dynamic>)>; +type TmpAttachmentsBuffer<'a> = Vec<( + AttachmentFormattingStyle, + ReportAttachmentRef<'a, Dynamic>, + usize, +)>; impl<'a, 'b> DefaultFormatterState<'a, 'b> { fn new( @@ -1012,33 +1020,44 @@ impl<'a, 'b> DefaultFormatterState<'a, 'b> { report .attachments() .iter() - .map(|attachment| { + .enumerate() + .map(|(original_index, attachment)| { ( attachment.preferred_formatting_style(self.report_formatting_function), attachment, + original_index, ) }) - .filter( - |(formatting_style, _attachment)| match formatting_style.placement { + .filter(|(formatting_style, _attachment, _index)| { + match formatting_style.placement { AttachmentFormattingPlacement::Opaque => { opaque_attachment_count += 1; false } AttachmentFormattingPlacement::Hidden => false, _ => true, - }, - ), + } + }), ); tmp_attachments_buffer - .sort_by_key(|(style1, _attachment)| core::cmp::Reverse(style1.priority)); - for (attachment_index, &(attachment_formatting_style, attachment)) in + .sort_by_key(|(style1, _attachment, _index)| core::cmp::Reverse(style1.priority)); + for (display_index, &(attachment_formatting_style, attachment, original_index)) in tmp_attachments_buffer.iter().enumerate() { - let is_last_attachment = attachment_index + 1 == tmp_attachments_buffer.len(); + let is_last_attachment = display_index + 1 == tmp_attachments_buffer.len(); + // `original_index` reflects an attachment's position in the parent report's original + // attachment list. + // It is independent of any sorting or filtering the formatter may apply for display + // purposes. + let parent = AttachmentParent { + report, + attachment_index: original_index, + }; self.format_attachment( tmp_value_buffer, attachment_formatting_style, attachment, + parent, is_last_attachment && !has_children, )?; } @@ -1098,6 +1117,7 @@ impl<'a, 'b> DefaultFormatterState<'a, 'b> { tmp_value_buffer: &mut TmpValueBuffer, attachment_formatting_style: AttachmentFormattingStyle, attachment: ReportAttachmentRef<'a, Dynamic>, + attachment_parent: AttachmentParent<'a>, is_last: bool, ) -> fmt::Result { match attachment_formatting_style.placement { @@ -1110,7 +1130,7 @@ impl<'a, 'b> DefaultFormatterState<'a, 'b> { self.format_item( tmp_value_buffer, formatting, - attachment.format_inner(), + attachment.format_inner_with_parent(attachment_parent), attachment_formatting_style.function, )?; } @@ -1136,7 +1156,7 @@ impl<'a, 'b> DefaultFormatterState<'a, 'b> { this.format_item( tmp_value_buffer, &self.config.attachment_headered_formatting_data, - attachment.format_inner(), + attachment.format_inner_with_parent(attachment_parent), attachment_formatting_style.function, )?; if let Some(headered_attachment_data_suffix) = @@ -1151,7 +1171,11 @@ impl<'a, 'b> DefaultFormatterState<'a, 'b> { } AttachmentFormattingPlacement::Appendix { appendix_name } => { let appendices = self.appendices.entry(appendix_name).or_default(); - appendices.push((attachment, attachment_formatting_style.function)); + appendices.push(( + attachment, + attachment_parent, + attachment_formatting_style.function, + )); let formatting = if is_last { &self.config.notice_see_also_last_formatting } else { @@ -1270,7 +1294,7 @@ impl<'a, 'b> DefaultFormatterState<'a, 'b> { let mut is_first = true; for (appendix_name, appendices) in &appendices { - for (appendix_index, &(attachment, formatting_function)) in + for (appendix_index, &(attachment, attachment_parent, formatting_function)) in appendices.iter().enumerate() { if is_first { @@ -1285,7 +1309,7 @@ impl<'a, 'b> DefaultFormatterState<'a, 'b> { self.format_item( tmp_value_buffer, &self.config.appendix_body, - attachment.format_inner(), + attachment.format_inner_with_parent(attachment_parent), formatting_function, )?; } diff --git a/src/markers.rs b/src/markers.rs index f0c7936..fe2ff1b 100644 --- a/src/markers.rs +++ b/src/markers.rs @@ -102,7 +102,7 @@ //! let local_report: Report, markers::Mutable, markers::Local> = report!(local_data); //! // local_report cannot be sent to another thread - won't compile //! ``` - +/// [`anyhow1`]: https://docs.rs/anyhow/1/anyhow use crate::ReportMut; /// Marker type for reports with dynamic (type-erased) context. @@ -229,6 +229,7 @@ use crate::ReportMut; /// See [`examples/error_coercion.rs`] for a complete guide to type conversions. /// /// [`examples/error_coercion.rs`]: https://github.com/rootcause-rs/rootcause/blob/main/examples/error_coercion.rs +/// [`anyhow::Error`]: https://docs.rs/anyhow pub struct Dynamic { /// This field ensures `Dynamic` is an unsized type. /// diff --git a/src/report_attachment/ref_.rs b/src/report_attachment/ref_.rs index 1c18934..ed63857 100644 --- a/src/report_attachment/ref_.rs +++ b/src/report_attachment/ref_.rs @@ -3,6 +3,7 @@ use core::any::TypeId; use rootcause_internals::handlers::{AttachmentFormattingStyle, FormattingFunction}; use crate::{ + hooks::attachment_formatter::AttachmentParent, markers::{Dynamic, SendSync}, preformatted::{self, PreformattedAttachment}, report_attachment::ReportAttachment, @@ -225,6 +226,32 @@ impl<'a, A: ?Sized> ReportAttachmentRef<'a, A> { ) } + /// Formats the inner attachment data with formatting hooks applied + /// while passing in an [`AttachmentParent`] as well. + #[must_use] + pub fn format_inner_with_parent( + self, + parent: AttachmentParent<'_>, + ) -> impl core::fmt::Display + core::fmt::Debug { + format_helper( + (self.into_dynamic(), parent), + |(attachment, parent), formatter| { + crate::hooks::attachment_formatter::display_attachment( + attachment, + Some(parent), + formatter, + ) + }, + |(attachment, parent), formatter| { + crate::hooks::attachment_formatter::debug_attachment( + attachment, + Some(parent), + formatter, + ) + }, + ) + } + /// Formats the inner attachment data without applying any formatting hooks. /// /// This method provides direct access to the attachment's formatting