sheets-diff 2.4.0

Structured diff engine for Microsoft Excel .xlsx workbooks
Documentation
//! Non-cell workbook object detection and coverage diagnostics (RFC-023).
//!
//! `sheets-diff` does not compare non-cell workbook objects. Two different
//! reasons apply, and they matter to a consumer for different reasons:
//!   - Charts, images, comments, data validation, and conditional formatting
//!     are **not exposed by calamine 0.36's public API at all** — there is
//!     no data to compare, upstream or otherwise. (Cell styles and number
//!     formats are the same case: `calamine::formats` is a private module.)
//!   - Hyperlinks, merged regions, tables, and pivot tables **are** exposed
//!     by calamine 0.36 (`Xlsx::hyperlinks_by_sheet_name`,
//!     `Xlsx::merged_regions`, `Xlsx::table_by_name`, `Xlsx::pivot_tables` —
//!     confirmed in RFC-035 Handoff 01's spike) — the data is available
//!     upstream, this crate simply does not call those APIs yet.
//!
//! What calamine exposes that this module *does* use:
//!   - `Sheet.typ: SheetType` — distinguishes WorkSheet, ChartSheet, MacroSheet, Vba
//!   - `Sheet.visible: SheetVisible`
//!
//! The policy for v2.2 is `WarnIfPresent` for non-worksheet sheet types and
//! a single coverage diagnostic explaining what is NOT compared. This prevents
//! a misleading "no differences" result when meaningful objects are present.

use calamine::{Reader, SheetType};

use crate::model::{Diagnostic, DiagnosticKind, DiagnosticLocation, DiffStage, Severity};
use crate::open::OpenedWorkbook;

// ---------------------------------------------------------------------------
// ObjectCompareMode (RFC-023 §6)
// ---------------------------------------------------------------------------

/// Controls how the presence of non-cell objects is handled.
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum ObjectCompareMode {
    /// Ignore objects entirely — no diagnostics.
    Ignore,
    /// Emit a coverage warning when non-worksheet sheets or any object
    /// categories that cannot be compared are detected. Default.
    #[default]
    WarnIfPresent,
    /// Compare what is available; emit diagnostics for the rest.
    /// Currently behaves identically to `WarnIfPresent`: calamine 0.36 does
    /// expose hyperlinks, merged regions, tables, and pivot tables, but this
    /// crate does not yet call those APIs to compare them.
    CompareAvailable,
}

// ---------------------------------------------------------------------------
// Public entry point
// ---------------------------------------------------------------------------

/// Detect non-cell objects on both workbook sides and emit coverage diagnostics.
pub fn report_object_coverage(
    old_wb: &mut OpenedWorkbook,
    new_wb: &mut OpenedWorkbook,
    mode: ObjectCompareMode,
    diagnostics: &mut Vec<Diagnostic>,
) {
    if mode == ObjectCompareMode::Ignore {
        return;
    }

    detect_non_worksheet_sheets(old_wb, diagnostics);
    detect_non_worksheet_sheets(new_wb, diagnostics);

    // Emit a single blanket coverage note so consumers know what was NOT compared.
    emit_coverage_note(diagnostics);
}

// ---------------------------------------------------------------------------
// Non-worksheet sheet detection
// ---------------------------------------------------------------------------

fn detect_non_worksheet_sheets(wb: &mut OpenedWorkbook, diagnostics: &mut Vec<Diagnostic>) {
    for (index, sheet) in wb.reader.sheets_metadata().iter().enumerate() {
        let kind = match sheet.typ {
            SheetType::ChartSheet => Some("chart sheet"),
            SheetType::MacroSheet => Some("macro sheet"),
            SheetType::Vba => Some("VBA module"),
            SheetType::DialogSheet => Some("dialog sheet"),
            SheetType::WorkSheet => None, // ordinary — no warning needed
        };
        if let Some(kind_label) = kind {
            diagnostics.push(Diagnostic {
                severity: Severity::Warning,
                kind: DiagnosticKind::UnsupportedWorkbookFeature {
                    feature: kind_label.to_owned(),
                },
                location: DiagnosticLocation {
                    stage: DiffStage::Metadata,
                    sheet_order: Some(index),
                    sheet_name: Some(sheet.name.clone()),
                    address: None,
                },
                message: format!(
                    "sheet '{}' is a {} — content not compared \
                     (calamine 0.36 does not expose {} data)",
                    sheet.name, kind_label, kind_label
                ),
            });
        }
    }
}

// ---------------------------------------------------------------------------
// Blanket coverage note
// ---------------------------------------------------------------------------

fn emit_coverage_note(diagnostics: &mut Vec<Diagnostic>) {
    diagnostics.push(Diagnostic {
        severity: Severity::Info,
        kind: DiagnosticKind::UnsupportedWorkbookFeature {
            feature: "non-cell objects".to_owned(),
        },
        location: DiagnosticLocation {
            stage: DiffStage::Metadata,
            sheet_order: None,
            sheet_name: None,
            address: None,
        },
        message: "not compared: charts, images, comments, data validation, and \
                  conditional formatting (unavailable in calamine 0.36's API); \
                  hyperlinks, merged regions, tables, and pivot tables (available \
                  upstream, not yet used by this crate)"
            .into(),
    });
}