Skip to main content

sheets_diff/
objects.rs

1//! Non-cell workbook object detection and coverage diagnostics (RFC-023).
2//!
3//! `sheets-diff` does not compare non-cell workbook objects. Two different
4//! reasons apply, and they matter to a consumer for different reasons:
5//!   - Charts, images, comments, data validation, and conditional formatting
6//!     are **not exposed by calamine 0.36's public API at all** — there is
7//!     no data to compare, upstream or otherwise. (Cell styles and number
8//!     formats are the same case: `calamine::formats` is a private module.)
9//!   - Hyperlinks, merged regions, tables, and pivot tables **are** exposed
10//!     by calamine 0.36 (`Xlsx::hyperlinks_by_sheet_name`,
11//!     `Xlsx::merged_regions`, `Xlsx::table_by_name`, `Xlsx::pivot_tables` —
12//!     confirmed in RFC-035 Handoff 01's spike) — the data is available
13//!     upstream, this crate simply does not call those APIs yet.
14//!
15//! What calamine exposes that this module *does* use:
16//!   - `Sheet.typ: SheetType` — distinguishes WorkSheet, ChartSheet, MacroSheet, Vba
17//!   - `Sheet.visible: SheetVisible`
18//!
19//! The policy for v2.2 is `WarnIfPresent` for non-worksheet sheet types and
20//! a single coverage diagnostic explaining what is NOT compared. This prevents
21//! a misleading "no differences" result when meaningful objects are present.
22
23use calamine::{Reader, SheetType};
24
25use crate::model::{Diagnostic, DiagnosticKind, DiagnosticLocation, DiffStage, Severity};
26use crate::open::OpenedWorkbook;
27
28// ---------------------------------------------------------------------------
29// ObjectCompareMode (RFC-023 §6)
30// ---------------------------------------------------------------------------
31
32/// Controls how the presence of non-cell objects is handled.
33#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
34pub enum ObjectCompareMode {
35    /// Ignore objects entirely — no diagnostics.
36    Ignore,
37    /// Emit a coverage warning when non-worksheet sheets or any object
38    /// categories that cannot be compared are detected. Default.
39    #[default]
40    WarnIfPresent,
41    /// Compare what is available; emit diagnostics for the rest.
42    /// Currently behaves identically to `WarnIfPresent`: calamine 0.36 does
43    /// expose hyperlinks, merged regions, tables, and pivot tables, but this
44    /// crate does not yet call those APIs to compare them.
45    CompareAvailable,
46}
47
48// ---------------------------------------------------------------------------
49// Public entry point
50// ---------------------------------------------------------------------------
51
52/// Detect non-cell objects on both workbook sides and emit coverage diagnostics.
53pub fn report_object_coverage(
54    old_wb: &mut OpenedWorkbook,
55    new_wb: &mut OpenedWorkbook,
56    mode: ObjectCompareMode,
57    diagnostics: &mut Vec<Diagnostic>,
58) {
59    if mode == ObjectCompareMode::Ignore {
60        return;
61    }
62
63    detect_non_worksheet_sheets(old_wb, diagnostics);
64    detect_non_worksheet_sheets(new_wb, diagnostics);
65
66    // Emit a single blanket coverage note so consumers know what was NOT compared.
67    emit_coverage_note(diagnostics);
68}
69
70// ---------------------------------------------------------------------------
71// Non-worksheet sheet detection
72// ---------------------------------------------------------------------------
73
74fn detect_non_worksheet_sheets(wb: &mut OpenedWorkbook, diagnostics: &mut Vec<Diagnostic>) {
75    for (index, sheet) in wb.reader.sheets_metadata().iter().enumerate() {
76        let kind = match sheet.typ {
77            SheetType::ChartSheet => Some("chart sheet"),
78            SheetType::MacroSheet => Some("macro sheet"),
79            SheetType::Vba => Some("VBA module"),
80            SheetType::DialogSheet => Some("dialog sheet"),
81            SheetType::WorkSheet => None, // ordinary — no warning needed
82        };
83        if let Some(kind_label) = kind {
84            diagnostics.push(Diagnostic {
85                severity: Severity::Warning,
86                kind: DiagnosticKind::UnsupportedWorkbookFeature {
87                    feature: kind_label.to_owned(),
88                },
89                location: DiagnosticLocation {
90                    stage: DiffStage::Metadata,
91                    sheet_order: Some(index),
92                    sheet_name: Some(sheet.name.clone()),
93                    address: None,
94                },
95                message: format!(
96                    "sheet '{}' is a {} — content not compared \
97                     (calamine 0.36 does not expose {} data)",
98                    sheet.name, kind_label, kind_label
99                ),
100            });
101        }
102    }
103}
104
105// ---------------------------------------------------------------------------
106// Blanket coverage note
107// ---------------------------------------------------------------------------
108
109fn emit_coverage_note(diagnostics: &mut Vec<Diagnostic>) {
110    diagnostics.push(Diagnostic {
111        severity: Severity::Info,
112        kind: DiagnosticKind::UnsupportedWorkbookFeature {
113            feature: "non-cell objects".to_owned(),
114        },
115        location: DiagnosticLocation {
116            stage: DiffStage::Metadata,
117            sheet_order: None,
118            sheet_name: None,
119            address: None,
120        },
121        message: "not compared: charts, images, comments, data validation, and \
122                  conditional formatting (unavailable in calamine 0.36's API); \
123                  hyperlinks, merged regions, tables, and pivot tables (available \
124                  upstream, not yet used by this crate)"
125            .into(),
126    });
127}