Skip to main content

sheets_diff/
lib.rs

1#![forbid(unsafe_code)]
2// M5 Handoff 01/04 (RFC-016, RFC-005, RFC-013): library core must not write
3// to stdout/stderr. `forbid`, not `deny` -- `deny` can be overridden by an
4// inner `#[allow(clippy::disallowed_macros)]`, which is exactly the bypass
5// this closes (clippy's own `-D warnings` failure message suggests that
6// override; `forbid` turns the attempt into a compile error instead). The
7// scoped gate (`.github/clippy-no-stdout/clippy.toml`, loaded via
8// `CLIPPY_CONF_DIR` for `cargo clippy --lib`) supplies the actual macro/method
9// paths; with no config loaded elsewhere, these two lints have nothing
10// configured and are free everywhere else in the tree.
11#![forbid(clippy::disallowed_macros, clippy::disallowed_methods)]
12
13//! # sheets-diff
14//!
15//! Structured diff engine for Microsoft Excel `.xlsx` workbooks.
16//!
17//! ## Quick start
18//!
19//! ```rust,no_run
20//! use sheets_diff::compare_paths;
21//!
22//! let diff = compare_paths("old.xlsx", "new.xlsx")?;
23//! println!("changed cells: {}", diff.summary.cells_changed);
24//! # Ok::<(), sheets_diff::SheetsDiffError>(())
25//! ```
26//!
27//! ## Input sources
28//!
29//! | Function | When to use |
30//! |---|---|
31//! | [`compare_paths`] | Simplest; caller provides file paths |
32//! | [`compare_bytes`] | You already have the bytes (e.g. from a cache or repo) |
33//! | [`compare_readers`] | You have open `Read + Seek` handles |
34//! | `compare_*_with_options` variants | Any of the above plus [`DiffOptions`] |
35//!
36//! See [`DiffOptions`] / [`DiffOptionsBuilder`] for all configuration knobs.
37
38// ---------------------------------------------------------------------------
39// Internal modules (not pub)
40// ---------------------------------------------------------------------------
41
42pub mod address;
43mod align;
44mod diff;
45mod error;
46mod matcher;
47mod meta;
48mod normalize;
49mod objects;
50mod open;
51
52pub(crate) mod compare;
53
54// ---------------------------------------------------------------------------
55// Public modules
56// ---------------------------------------------------------------------------
57
58/// Typed result model (`WorkbookDiff`, `SheetDiff`, `CellDiff`, `CellValue`, …).
59pub mod model;
60
61/// Comparison options and builder (`DiffOptions`, `DiffOptionsBuilder`, …).
62pub mod options;
63
64/// Output formatters (text summary, unified diff).
65pub mod output;
66
67// ---------------------------------------------------------------------------
68// Re-exports — the stable public API surface (RFC-002, RFC-031)
69// ---------------------------------------------------------------------------
70
71// Error types
72pub use error::{LimitKind, OpenErrorKind, ReadErrorKind, SheetsDiffError};
73
74// Model
75pub use model::{
76    AlignmentSummary, CellChangeKind, CellDateTime, CellDiff, CellDisplay, CellDuration, CellError,
77    CellNumberFormat, CellSnapshot, CellValue, DateTimeKind, Diagnostic, DiagnosticKind,
78    DiagnosticLocation, DiagnosticSummary, DiffMetrics, DiffStage, DiffSummary, DisplaySource,
79    FormatChange, FormulaChange, FormulaText, MatchConfidence, Severity, SheetChange, SheetDiff,
80    SheetMatchReason, SheetRef, SheetSummary, Side, SourceDescription, SourceKind, ValueChange,
81    ValueDifferenceKind, WorkbookChange, WorkbookDiff, WorkbookObjectChange, WorkbookSideInfo,
82};
83
84// Address
85pub use address::{CellAddress, ComparedRange, MAX_COL, MAX_COL_LABEL, MAX_ROW};
86
87// Options
88pub use objects::ObjectCompareMode;
89pub use options::{
90    AlignmentMode, Cancellation, ComparisonOptions, DateComparePolicy, DiagnosticOptions,
91    DiffEvent, DiffOptions, DiffOptionsBuilder, ExecutionMode, ExecutionOptions, FormatCompareMode,
92    FormulaCompareMode, Limits, MatchingOptions, NumberComparePolicy, NumericTypePolicy,
93    OutputOptions, ProgressSink, SheetMatchingMode, TypeMismatchPolicy, ValueCompareOptions,
94};
95
96// ---------------------------------------------------------------------------
97// Public entry points (RFC-033 §12)
98// ---------------------------------------------------------------------------
99
100use std::io::{Read, Seek};
101use std::path::Path;
102
103/// Compare two workbooks given their filesystem paths.
104///
105/// Uses [`DiffOptions::default()`].
106/// Compare two workbooks given their filesystem paths.
107///
108/// # Path handling
109///
110/// `old` and `new` accept any `AsRef<Path>`, and the raw `Path` is passed to
111/// `std::fs::read` unchanged — there is **no internal `to_str()`/`unwrap()` on
112/// the path**, so non-UTF-8 paths (common on Linux) are fully supported and
113/// never cause a panic. The only UTF-8-dependent step is the cosmetic
114/// `SourceDescription.display_name`, which is set to `None` for a non-UTF-8
115/// file name rather than failing.
116pub fn compare_paths(
117    old: impl AsRef<Path>,
118    new: impl AsRef<Path>,
119) -> Result<WorkbookDiff, SheetsDiffError> {
120    diff::run_compare_paths(old, new, DiffOptions::default())
121}
122
123/// Compare two workbooks given their filesystem paths, with explicit options.
124pub fn compare_paths_with_options(
125    old: impl AsRef<Path>,
126    new: impl AsRef<Path>,
127    opts: DiffOptions,
128) -> Result<WorkbookDiff, SheetsDiffError> {
129    diff::run_compare_paths(old, new, opts)
130}
131
132/// Compare two workbooks given byte slices.
133pub fn compare_bytes(
134    old: impl AsRef<[u8]>,
135    new: impl AsRef<[u8]>,
136) -> Result<WorkbookDiff, SheetsDiffError> {
137    diff::run_compare_bytes(old, new, DiffOptions::default())
138}
139
140/// Compare two workbooks given byte slices, with explicit options.
141pub fn compare_bytes_with_options(
142    old: impl AsRef<[u8]>,
143    new: impl AsRef<[u8]>,
144    opts: DiffOptions,
145) -> Result<WorkbookDiff, SheetsDiffError> {
146    diff::run_compare_bytes(old, new, opts)
147}
148
149/// Compare two workbooks given `Read + Seek` readers.
150///
151/// `.xlsx` is ZIP-based and requires seeking.
152pub fn compare_readers<R1, R2>(old: R1, new: R2) -> Result<WorkbookDiff, SheetsDiffError>
153where
154    R1: Read + Seek,
155    R2: Read + Seek,
156{
157    diff::run_compare_readers(old, new, DiffOptions::default())
158}
159
160/// Compare two workbooks given `Read + Seek` readers, with explicit options.
161pub fn compare_readers_with_options<R1, R2>(
162    old: R1,
163    new: R2,
164    opts: DiffOptions,
165) -> Result<WorkbookDiff, SheetsDiffError>
166where
167    R1: Read + Seek,
168    R2: Read + Seek,
169{
170    diff::run_compare_readers(old, new, opts)
171}
172
173// ---------------------------------------------------------------------------
174// Documentation doctest harness (M6 Handoff 01, NF-025)
175// ---------------------------------------------------------------------------
176//
177// `include_str!`-ing a `docs/` page as this item's doc comment turns every
178// ```rust fence in it into a doctest `cargo test --doc` compiles — the same
179// mechanism, and the same guarantee, as any other doc example in this file.
180// `#[cfg(doctest)]` means the item exists only while doctests are being
181// collected, so it is absent from every normal build (`cargo build`,
182// `cargo doc`, `cargo clippy`) and costs nothing there.
183//
184// To add a future page to this harness: add one more
185// `#[doc = include_str!("../docs/src/<path>.md")] #[cfg(doctest)]` item
186// below, following this one.
187#[doc = include_str!("../docs/src/migration/v1-to-v2.md")]
188#[cfg(doctest)]
189pub struct MigrationGuideDoctests;
190
191#[doc = include_str!("../docs/src/api-guide.md")]
192#[cfg(doctest)]
193pub struct ApiGuideDoctests;
194
195#[doc = include_str!("../docs/src/semantics.md")]
196#[cfg(doctest)]
197pub struct SemanticsDoctests;
198
199#[doc = include_str!("../docs/src/non-goals.md")]
200#[cfg(doctest)]
201pub struct NonGoalsDoctests;