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}