facet_diff/report.rs
1//! Diff report with multi-format rendering capabilities.
2//!
3//! This module provides [`DiffReport`], which holds a computed diff along with
4//! references to the original values, enabling rendering in multiple output formats
5//! (Rust, JSON, XML) with or without ANSI colors.
6
7use crate::Diff;
8use facet_diff_core::layout::{
9 AnsiBackend, BuildOptions, ColorBackend, DiffFlavor, JsonFlavor, RenderOptions, RustFlavor,
10 XmlFlavor, build_layout, render_to_string,
11};
12use facet_reflect::Peek;
13
14/// A reusable diff plus its original inputs, allowing rendering in different output styles.
15///
16/// `DiffReport` holds a computed [`Diff`] along with [`Peek`] references to the original
17/// left and right values. This allows rendering the same diff in multiple formats without
18/// recomputing the diff tree.
19///
20/// # Example
21///
22/// ```
23/// use facet::Facet;
24/// use facet_diff::{DiffReport, diff_new_peek};
25/// use facet_reflect::Peek;
26///
27/// #[derive(Facet)]
28/// struct Point { x: i32, y: i32 }
29///
30/// let old = Point { x: 10, y: 20 };
31/// let new = Point { x: 10, y: 30 };
32///
33/// let left = Peek::new(&old);
34/// let right = Peek::new(&new);
35/// let diff = diff_new_peek(left, right);
36///
37/// let report = DiffReport::new(diff, left, right);
38///
39/// // Render in different formats
40/// println!("Rust format:\n{}", report.render_plain_rust());
41/// println!("JSON format:\n{}", report.render_plain_json());
42/// println!("XML format:\n{}", report.render_plain_xml());
43/// ```
44pub struct DiffReport<'mem, 'facet> {
45 diff: Diff<'mem, 'facet>,
46 left: Peek<'mem, 'facet>,
47 right: Peek<'mem, 'facet>,
48 /// Float tolerance used during comparison, stored to compute display precision.
49 float_tolerance: Option<f64>,
50}
51
52impl<'mem, 'facet> DiffReport<'mem, 'facet> {
53 /// Create a new diff report from a computed diff and the original values.
54 pub fn new(
55 diff: Diff<'mem, 'facet>,
56 left: Peek<'mem, 'facet>,
57 right: Peek<'mem, 'facet>,
58 ) -> Self {
59 Self {
60 diff,
61 left,
62 right,
63 float_tolerance: None,
64 }
65 }
66
67 /// Create a new diff report with float tolerance information.
68 ///
69 /// The tolerance is used to determine appropriate decimal precision when rendering
70 /// floating-point values in the diff output.
71 pub fn with_float_tolerance(mut self, tolerance: f64) -> Self {
72 self.float_tolerance = Some(tolerance);
73 self
74 }
75
76 /// Access the raw diff tree.
77 pub fn diff(&self) -> &Diff<'mem, 'facet> {
78 &self.diff
79 }
80
81 /// Peek into the left-hand value.
82 pub fn left(&self) -> Peek<'mem, 'facet> {
83 self.left
84 }
85
86 /// Peek into the right-hand value.
87 pub fn right(&self) -> Peek<'mem, 'facet> {
88 self.right
89 }
90
91 /// Format the diff using the legacy tree display (same output as `Display` impl).
92 pub fn legacy_string(&self) -> String {
93 format!("{}", self.diff)
94 }
95
96 /// Compute float precision from tolerance.
97 ///
98 /// If tolerance is 0.002, we need ~3 decimal places to see differences at that scale.
99 /// Formula: ceil(-log10(tolerance))
100 fn float_precision_from_tolerance(&self) -> Option<usize> {
101 self.float_tolerance.map(|tol| {
102 if tol <= 0.0 {
103 6 // fallback to reasonable precision
104 } else {
105 (-tol.log10()).ceil() as usize
106 }
107 })
108 }
109
110 /// Build options with float precision derived from tolerance.
111 fn build_opts_with_precision(&self) -> BuildOptions {
112 BuildOptions {
113 float_precision: self.float_precision_from_tolerance(),
114 ..Default::default()
115 }
116 }
117
118 /// Render the diff with a custom flavor and render/build options.
119 pub fn render_with_options<B: ColorBackend, F: DiffFlavor>(
120 &self,
121 flavor: &F,
122 build_opts: &BuildOptions,
123 render_opts: &RenderOptions<B>,
124 ) -> String {
125 let layout = build_layout(&self.diff, self.left, self.right, build_opts, flavor);
126 render_to_string(&layout, render_opts, flavor)
127 }
128
129 /// Render using ANSI colors with the provided flavor.
130 pub fn render_ansi_with<F: DiffFlavor>(&self, flavor: &F) -> String {
131 let build_opts = self.build_opts_with_precision();
132 let render_opts = RenderOptions::<AnsiBackend>::default();
133 self.render_with_options(flavor, &build_opts, &render_opts)
134 }
135
136 /// Render without colors using the provided flavor.
137 pub fn render_plain_with<F: DiffFlavor>(&self, flavor: &F) -> String {
138 let build_opts = self.build_opts_with_precision();
139 let render_opts = RenderOptions::plain();
140 self.render_with_options(flavor, &build_opts, &render_opts)
141 }
142
143 /// Render using the Rust flavor with ANSI colors.
144 pub fn render_ansi_rust(&self) -> String {
145 self.render_ansi_with(&RustFlavor)
146 }
147
148 /// Render using the Rust flavor without colors.
149 pub fn render_plain_rust(&self) -> String {
150 self.render_plain_with(&RustFlavor)
151 }
152
153 /// Render using the JSON flavor with ANSI colors.
154 pub fn render_ansi_json(&self) -> String {
155 self.render_ansi_with(&JsonFlavor)
156 }
157
158 /// Render using the JSON flavor without colors.
159 pub fn render_plain_json(&self) -> String {
160 self.render_plain_with(&JsonFlavor)
161 }
162
163 /// Render using the XML flavor with ANSI colors.
164 pub fn render_ansi_xml(&self) -> String {
165 self.render_ansi_with(&XmlFlavor)
166 }
167
168 /// Render using the XML flavor without colors.
169 pub fn render_plain_xml(&self) -> String {
170 self.render_plain_with(&XmlFlavor)
171 }
172}