Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 59 additions & 1 deletion examples/formatting_hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -144,6 +145,49 @@ impl ContextFormatterHook<ValidationError> 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<RequestId> for RequestIdFormatter {
fn display(
&self,
attachment: ReportAttachmentRef<'_, RequestId>,
parent: Option<AttachmentParent<'_>>,
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
Expand Down Expand Up @@ -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::<DatabaseQuery, _>(DatabaseQueryFormatter)
.attachment_formatter::<ActionRequired, _>(ActionRequiredFormatter)
.attachment_formatter::<RequestId, _>(RequestIdFormatter)
.context_formatter::<ValidationError, _>(ValidationErrorFormatter)
.install()
.expect("failed to install hooks");
Expand All @@ -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}"),
}
Expand Down
5 changes: 5 additions & 0 deletions src/compat/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
7 changes: 7 additions & 0 deletions src/hooks/attachment_formatter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,13 @@ pub trait AttachmentFormatterHook<A>: '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
///
/// ```
Expand Down
56 changes: 40 additions & 16 deletions src/hooks/builtin_hooks/report_formatter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -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,
>;

Expand All @@ -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(
Expand Down Expand Up @@ -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,
)?;
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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,
)?;
}
Expand All @@ -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) =
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -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,
)?;
}
Expand Down
3 changes: 2 additions & 1 deletion src/markers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@
//! let local_report: Report<Rc<String>, 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.
Expand Down Expand Up @@ -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.
///
Expand Down
27 changes: 27 additions & 0 deletions src/report_attachment/ref_.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down