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}