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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added a compatibility module for error-stack v0.7 [#169](https://github.com/rootcause-rs/rootcause/pull/169).
- Implement `Deref<Target = dyn Error>` and `AsRef<dyn Error>` for `Report`, `ReportRef` and `ReportMut`; `Report<_, _, SendSync>` targets `dyn Error + Send + Sync`. May break type inference at some `Report::new*` call sites. [#147](https://github.com/rootcause-rs/rootcause/pull/147), [#149](https://github.com/rootcause-rs/rootcause/pull/149)
- Doc examples for nearly all public functions [#136](https://github.com/rootcause-rs/rootcause/pull/136).
- `ReportAttachmentRef::format_inner_with_parent` for formatting an attachment with parent-report context. [#163](https://github.com/rootcause-rs/rootcause/pull/163).

### Changed

- Moved the preformat functionality out of `rootcause` into the new `rootcause-preformat` companion crate. The `preformatted` module, the `preformat*` methods, and the `display_preformatted`/`debug_preformatted` formatter-hook methods are removed; use the `Preformat*Ext` and `ContextTransformNestedExt` traits from the new crate. `AttachmentFormatterHook::preferred_formatting_style` and `ContextFormatterHook::preferred_context_formatting_style` now take a concrete type instead of `Dynamic`. [#148](https://github.com/rootcause-rs/rootcause/pull/148)
- `DefaultReportFormatter` now populates the `AttachmentParent` argument when invoking `AttachmentFormatterHook::display`/`debug` (previously always `None`), exposing the parent report and the attachment's pre-sort index. [#163](https://github.com/rootcause-rs/rootcause/pull/163).

### Fixed

Expand Down
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::{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 access 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 from 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
26 changes: 23 additions & 3 deletions src/hooks/attachment_formatter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,17 @@ where
/// to understand the attachment's position or relationship to its containing
/// report.
///
/// An [`AttachmentFormatterHook`] receives `Some(AttachmentParent)` when the
/// attachment is being formatted via
/// [`ReportAttachmentRef::format_inner_with_parent`], which the built-in
/// [`DefaultReportFormatter`](crate::hooks::builtin_hooks::report_formatter::DefaultReportFormatter)
/// uses when walking an error tree. It is `None` when the attachment is being
/// formatted in isolation via [`ReportAttachmentRef::format_inner`] (for
/// example, `attachment.format_inner().to_string()`).
///
/// [`ReportAttachmentRef::format_inner`]: crate::report_attachment::ReportAttachmentRef::format_inner
/// [`ReportAttachmentRef::format_inner_with_parent`]: crate::report_attachment::ReportAttachmentRef::format_inner_with_parent
///
/// # Examples
///
/// ```
Expand Down Expand Up @@ -292,7 +303,14 @@ where
pub struct AttachmentParent<'a> {
/// Reference to the report that contains this attachment
pub report: ReportRef<'a, Dynamic, Uncloneable, Local>,
/// Index of this attachment within the parent report's attachment list
/// Index of this attachment within the parent report's attachment list.
///
/// By convention this is the attachment's stable position in
/// `report.attachments()`, independent of any priority sorting or
/// placement filtering the formatter applies for display purposes. The
/// built-in [`DefaultReportFormatter`](crate::hooks::builtin_hooks::report_formatter::DefaultReportFormatter)
/// upholds this; custom [`ReportFormatter`](crate::hooks::report_formatter::ReportFormatter)
/// implementations are encouraged to do the same.
pub attachment_index: usize,
}

Expand Down Expand Up @@ -402,7 +420,8 @@ pub trait AttachmentFormatterHook<A>: 'static + Send + Sync {
/// # Arguments
///
/// * `attachment` - Reference to the attachment being formatted
/// * `attachment_parent` - Optional context about the parent report
/// * `attachment_parent` - Optional context about the parent report. See
/// [`AttachmentParent`] for when this is `Some` vs `None`.
/// * `formatter` - The formatter to write output to
///
/// # Examples
Expand Down Expand Up @@ -448,7 +467,8 @@ pub trait AttachmentFormatterHook<A>: 'static + Send + Sync {
/// # Arguments
///
/// * `attachment` - Reference to the attachment being formatted
/// * `attachment_parent` - Optional context about the parent report
/// * `attachment_parent` - Optional context about the parent report. See
/// [`AttachmentParent`] for when this is `Some` vs `None`.
/// * `formatter` - The formatter to write output to
///
/// # 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(|(style, _attachment, _index)| core::cmp::Reverse(style.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
56 changes: 55 additions & 1 deletion src/report_attachment/ref_.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use core::any::{Any, TypeId};

use rootcause_internals::handlers::{AttachmentFormattingStyle, FormattingFunction};

use crate::{markers::Dynamic, util::format_helper};
use crate::{hooks::attachment_formatter::AttachmentParent, markers::Dynamic, util::format_helper};

/// FIXME: Once rust-lang/rust#132922 gets resolved, we can make the `raw` field
/// an unsafe field and remove this module.
Expand Down Expand Up @@ -267,6 +267,60 @@ impl<'a, A: ?Sized> ReportAttachmentRef<'a, A> {
)
}

/// Formats the inner attachment data with formatting hooks applied,
/// passing the supplied [`AttachmentParent`] to the hook.
///
/// This is the variant of [`format_inner`] used by report formatters
/// while walking an error tree: it lets the hook see the parent report
/// and the attachment's index in that report's attachment list. Use
/// [`format_inner`] instead when there is no parent context (for
/// example, formatting an attachment in isolation).
///
/// [`format_inner`]: Self::format_inner
///
/// # Examples
///
/// ```
/// # use rootcause::{
/// # hooks::attachment_formatter::AttachmentParent,
/// # prelude::*,
/// # report_attachment::ReportAttachment,
/// # };
/// let report: Report = report!("outer");
/// let attachment = ReportAttachment::new_sendsync(42i32);
/// let parent = AttachmentParent {
/// report: report.as_ref().into_dynamic().into_uncloneable().into_local(),
/// attachment_index: 0,
/// };
/// assert_eq!(
/// attachment.as_ref().format_inner_with_parent(parent).to_string(),
/// "42",
/// );
/// ```
#[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
Loading