sheetkit_core/workbook/
open_options.rs

1/// Controls how worksheets and auxiliary parts are parsed during open.
2#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
3pub enum ReadMode {
4    /// Parse all parts eagerly. Equivalent to the old `Full` mode.
5    Eager,
6    /// Skip auxiliary parts (comments, drawings, charts, images, doc props,
7    /// pivot tables, slicers, threaded comments, VBA, tables, form controls).
8    /// These are stored as raw bytes for on-demand parsing or direct
9    /// round-trip preservation. Equivalent to the old `ReadFast` mode.
10    /// Will evolve into true lazy on-demand hydration in later workstreams.
11    /// This is the default mode.
12    #[default]
13    Lazy,
14    /// Forward-only streaming read mode (reserved for future use).
15    /// Currently behaves the same as `Lazy`.
16    Stream,
17}
18
19/// Controls when auxiliary parts (comments, charts, images, etc.) are parsed.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
21pub enum AuxParts {
22    /// Parse auxiliary parts only when accessed. This is the default.
23    #[default]
24    Deferred,
25    /// Parse all auxiliary parts during open.
26    EagerLoad,
27}
28
29/// Options for controlling how a workbook is opened and parsed.
30///
31/// All fields default to `None` (no limit). Read mode defaults to `Lazy`
32/// and auxiliary parts default to `Deferred`.
33/// Use the builder-style setter methods for convenience.
34#[derive(Debug, Clone, Default)]
35pub struct OpenOptions {
36    /// Maximum number of rows to read per sheet. Rows beyond this limit
37    /// are silently discarded during parsing.
38    pub sheet_rows: Option<u32>,
39
40    /// Only parse sheets whose names are in this list. Sheets not listed
41    /// are represented as empty worksheets (their XML is not parsed).
42    /// `None` means parse all sheets.
43    pub sheets: Option<Vec<String>>,
44
45    /// Maximum total decompressed size of all ZIP entries in bytes.
46    /// Exceeding this limit returns [`Error::ZipSizeExceeded`].
47    /// Default when `None`: no limit.
48    pub max_unzip_size: Option<u64>,
49
50    /// Maximum number of ZIP entries allowed.
51    /// Exceeding this limit returns [`Error::ZipEntryCountExceeded`].
52    /// Default when `None`: no limit.
53    pub max_zip_entries: Option<usize>,
54
55    /// Read mode: `Lazy` (default) skips auxiliary parts for faster
56    /// read-only workloads; `Eager` parses everything; `Stream` is
57    /// reserved for future streaming reads.
58    pub read_mode: ReadMode,
59
60    /// Controls when auxiliary parts are parsed.
61    pub aux_parts: AuxParts,
62}
63
64impl OpenOptions {
65    /// Create a new `OpenOptions` with defaults (no limits, lazy read, deferred aux parts).
66    pub fn new() -> Self {
67        Self::default()
68    }
69
70    /// Set the maximum number of rows to read per sheet.
71    pub fn sheet_rows(mut self, rows: u32) -> Self {
72        self.sheet_rows = Some(rows);
73        self
74    }
75
76    /// Only parse sheets whose names are in this list.
77    pub fn sheets(mut self, names: Vec<String>) -> Self {
78        self.sheets = Some(names);
79        self
80    }
81
82    /// Set the maximum total decompressed size in bytes.
83    pub fn max_unzip_size(mut self, size: u64) -> Self {
84        self.max_unzip_size = Some(size);
85        self
86    }
87
88    /// Set the maximum number of ZIP entries.
89    pub fn max_zip_entries(mut self, count: usize) -> Self {
90        self.max_zip_entries = Some(count);
91        self
92    }
93
94    /// Set the read mode. `Lazy` skips auxiliary parts for faster
95    /// read-only workloads. `Stream` is reserved for future use.
96    pub fn read_mode(mut self, mode: ReadMode) -> Self {
97        self.read_mode = mode;
98        self
99    }
100
101    /// Set the auxiliary parts parsing policy.
102    pub fn aux_parts(mut self, policy: AuxParts) -> Self {
103        self.aux_parts = policy;
104        self
105    }
106
107    /// Returns true when auxiliary parts should be skipped during open.
108    /// Lazy/Stream modes always skip. Eager mode respects `aux_parts`.
109    pub(crate) fn skip_aux_parts(&self) -> bool {
110        match self.read_mode {
111            ReadMode::Eager => self.aux_parts == AuxParts::Deferred,
112            ReadMode::Lazy | ReadMode::Stream => true,
113        }
114    }
115
116    /// Returns true when mode is `Eager`.
117    #[allow(dead_code)]
118    pub(crate) fn is_eager(&self) -> bool {
119        self.read_mode == ReadMode::Eager
120    }
121
122    /// Returns true when mode is `Lazy`.
123    #[allow(dead_code)]
124    pub(crate) fn is_lazy(&self) -> bool {
125        self.read_mode == ReadMode::Lazy
126    }
127
128    /// Returns true when mode is `Stream`.
129    #[allow(dead_code)]
130    pub(crate) fn is_stream(&self) -> bool {
131        self.read_mode == ReadMode::Stream
132    }
133
134    /// Check whether a given sheet name should be parsed based on the `sheets` filter.
135    pub(crate) fn should_parse_sheet(&self, name: &str) -> bool {
136        match &self.sheets {
137            None => true,
138            Some(names) => names.iter().any(|n| n == name),
139        }
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[test]
148    fn test_default_options() {
149        let opts = OpenOptions::default();
150        assert!(opts.sheet_rows.is_none());
151        assert!(opts.sheets.is_none());
152        assert!(opts.max_unzip_size.is_none());
153        assert!(opts.max_zip_entries.is_none());
154        assert_eq!(opts.read_mode, ReadMode::Lazy);
155        assert!(opts.skip_aux_parts());
156    }
157
158    #[test]
159    fn test_builder_methods() {
160        let opts = OpenOptions::new()
161            .sheet_rows(100)
162            .sheets(vec!["Sheet1".to_string()])
163            .max_unzip_size(1_000_000)
164            .max_zip_entries(500);
165        assert_eq!(opts.sheet_rows, Some(100));
166        assert_eq!(opts.sheets, Some(vec!["Sheet1".to_string()]));
167        assert_eq!(opts.max_unzip_size, Some(1_000_000));
168        assert_eq!(opts.max_zip_entries, Some(500));
169    }
170
171    #[test]
172    fn test_read_mode_builder() {
173        let opts = OpenOptions::new().read_mode(ReadMode::Lazy);
174        assert_eq!(opts.read_mode, ReadMode::Lazy);
175        assert!(opts.skip_aux_parts());
176    }
177
178    #[test]
179    fn test_read_mode_default_is_lazy() {
180        let mode = ReadMode::default();
181        assert_eq!(mode, ReadMode::Lazy);
182    }
183
184    #[test]
185    fn test_read_mode_combined_with_other_options() {
186        let opts = OpenOptions::new().sheet_rows(50).read_mode(ReadMode::Lazy);
187        assert_eq!(opts.sheet_rows, Some(50));
188        assert!(opts.skip_aux_parts());
189    }
190
191    #[test]
192    fn test_stream_mode_skips_aux_parts() {
193        let opts = OpenOptions::new().read_mode(ReadMode::Stream);
194        assert!(opts.skip_aux_parts());
195        assert!(opts.is_stream());
196        assert!(!opts.is_eager());
197        assert!(!opts.is_lazy());
198    }
199
200    #[test]
201    fn test_aux_parts_default_is_deferred() {
202        let opts = OpenOptions::default();
203        assert_eq!(opts.aux_parts, AuxParts::Deferred);
204    }
205
206    #[test]
207    fn test_aux_parts_deferred() {
208        let opts = OpenOptions::new().aux_parts(AuxParts::Deferred);
209        assert_eq!(opts.aux_parts, AuxParts::Deferred);
210    }
211
212    #[test]
213    fn test_eager_mode_with_deferred_aux_skips_aux() {
214        let opts = OpenOptions::new()
215            .read_mode(ReadMode::Eager)
216            .aux_parts(AuxParts::Deferred);
217        assert!(opts.skip_aux_parts());
218    }
219
220    #[test]
221    fn test_eager_mode_with_eager_aux_parses_all() {
222        let opts = OpenOptions::new()
223            .read_mode(ReadMode::Eager)
224            .aux_parts(AuxParts::EagerLoad);
225        assert!(!opts.skip_aux_parts());
226    }
227
228    #[test]
229    fn test_should_parse_sheet_no_filter() {
230        let opts = OpenOptions::default();
231        assert!(opts.should_parse_sheet("Sheet1"));
232        assert!(opts.should_parse_sheet("anything"));
233    }
234
235    #[test]
236    fn test_should_parse_sheet_with_filter() {
237        let opts = OpenOptions::new().sheets(vec!["Sales".to_string(), "Data".to_string()]);
238        assert!(opts.should_parse_sheet("Sales"));
239        assert!(opts.should_parse_sheet("Data"));
240        assert!(!opts.should_parse_sheet("Sheet1"));
241        assert!(!opts.should_parse_sheet("Other"));
242    }
243
244    #[test]
245    fn test_helper_methods() {
246        let eager = OpenOptions::new().read_mode(ReadMode::Eager);
247        assert!(eager.is_eager());
248        assert!(!eager.is_lazy());
249        assert!(!eager.is_stream());
250
251        let lazy = OpenOptions::new().read_mode(ReadMode::Lazy);
252        assert!(!lazy.is_eager());
253        assert!(lazy.is_lazy());
254        assert!(!lazy.is_stream());
255
256        let stream = OpenOptions::new().read_mode(ReadMode::Stream);
257        assert!(!stream.is_eager());
258        assert!(!stream.is_lazy());
259        assert!(stream.is_stream());
260    }
261}