Skip to main content

formualizer_workbook/
traits.rs

1use formualizer_common::{LiteralValue, RangeAddress};
2#[cfg(feature = "json")]
3use serde::{Deserialize, Serialize};
4use std::collections::BTreeMap;
5use std::io::{Read, Write};
6use std::path::Path;
7
8#[derive(Clone, Debug)]
9pub struct CellData {
10    pub value: Option<LiteralValue>,
11    pub formula: Option<String>,
12    pub style: Option<StyleId>,
13}
14
15impl CellData {
16    pub fn from_value<V: IntoLiteral>(value: V) -> Self {
17        Self {
18            value: Some(value.into_literal()),
19            formula: None,
20            style: None,
21        }
22    }
23
24    pub fn from_formula(formula: impl Into<String>) -> Self {
25        Self {
26            value: None,
27            formula: Some(formula.into()),
28            style: None,
29        }
30    }
31}
32
33/// Local conversion trait so tests and callers can pass primitives directly
34pub trait IntoLiteral {
35    fn into_literal(self) -> LiteralValue;
36}
37
38impl IntoLiteral for LiteralValue {
39    fn into_literal(self) -> LiteralValue {
40        self
41    }
42}
43
44impl IntoLiteral for f64 {
45    fn into_literal(self) -> LiteralValue {
46        LiteralValue::Number(self)
47    }
48}
49
50impl IntoLiteral for i64 {
51    fn into_literal(self) -> LiteralValue {
52        LiteralValue::Int(self)
53    }
54}
55
56impl IntoLiteral for i32 {
57    fn into_literal(self) -> LiteralValue {
58        LiteralValue::Int(self as i64)
59    }
60}
61
62impl IntoLiteral for bool {
63    fn into_literal(self) -> LiteralValue {
64        LiteralValue::Boolean(self)
65    }
66}
67
68impl IntoLiteral for String {
69    fn into_literal(self) -> LiteralValue {
70        LiteralValue::Text(self)
71    }
72}
73
74impl IntoLiteral for &str {
75    fn into_literal(self) -> LiteralValue {
76        LiteralValue::Text(self.to_string())
77    }
78}
79
80pub type StyleId = u32;
81
82#[derive(Clone, Debug, Default)]
83pub struct BackendCaps {
84    pub read: bool,
85    pub write: bool,
86    pub streaming: bool,
87    pub tables: bool,
88    pub named_ranges: bool,
89    pub formulas: bool,
90    pub styles: bool,
91    pub lazy_loading: bool,
92    pub random_access: bool,
93    pub bytes_input: bool,
94
95    // Excel-specific nuances
96    pub date_system_1904: bool,
97    pub merged_cells: bool,
98    pub rich_text: bool,
99    pub hyperlinks: bool,
100    pub data_validations: bool,
101    pub shared_formulas: bool,
102}
103
104#[derive(Clone, Debug)]
105pub struct SheetData {
106    pub cells: BTreeMap<(u32, u32), CellData>,
107    pub dimensions: Option<(u32, u32)>,
108    pub tables: Vec<TableDefinition>,
109    pub named_ranges: Vec<NamedRange>,
110    pub date_system_1904: bool,
111    pub merged_cells: Vec<MergedRange>,
112    pub hidden: bool,
113    pub row_hidden_manual: Vec<u32>,
114    pub row_hidden_filter: Vec<u32>,
115}
116
117#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
118#[cfg_attr(feature = "json", serde(rename_all = "lowercase"))]
119#[derive(Clone, Debug, PartialEq, Eq, Hash, Default)]
120pub enum NamedRangeScope {
121    #[default]
122    Workbook,
123    Sheet,
124}
125
126#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
127#[derive(Clone, Debug)]
128pub struct NamedRange {
129    pub name: String,
130    #[cfg_attr(feature = "json", serde(default))]
131    pub scope: NamedRangeScope,
132    pub address: RangeAddress,
133}
134
135/// Stable representation of workbook/sheet scoped defined names.
136///
137/// Stage 1 supports only range-backed and literal-backed names.
138#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
139#[cfg_attr(feature = "json", serde(rename_all = "lowercase"))]
140#[derive(Clone, Debug, PartialEq, Eq, Hash, Default)]
141pub enum DefinedNameScope {
142    #[default]
143    Workbook,
144    Sheet,
145}
146
147#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
148#[cfg_attr(feature = "json", serde(tag = "type", rename_all = "lowercase"))]
149#[derive(Clone, Debug, PartialEq)]
150pub enum DefinedNameDefinition {
151    Range { address: RangeAddress },
152    Literal { value: LiteralValue },
153}
154
155#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
156#[derive(Clone, Debug, PartialEq)]
157pub struct DefinedName {
158    pub name: String,
159
160    #[cfg_attr(feature = "json", serde(default))]
161    pub scope: DefinedNameScope,
162
163    /// Sheet name for sheet-scoped names.
164    ///
165    /// For workbook-scoped names, this must be None.
166    #[cfg_attr(
167        feature = "json",
168        serde(default, skip_serializing_if = "Option::is_none")
169    )]
170    pub scope_sheet: Option<String>,
171
172    pub definition: DefinedNameDefinition,
173}
174
175#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
176#[derive(Clone, Debug)]
177pub struct TableDefinition {
178    pub name: String,
179    pub range: (u32, u32, u32, u32),
180    /// Whether the first row of `range` is a headers row.
181    ///
182    /// Deterministic resize rule:
183    /// - Tables are metadata-only; writing values just below/next to a table does NOT auto-expand
184    ///   the table. Callers must explicitly update table metadata (range/flags) if they want a
185    ///   resize.
186    #[cfg_attr(feature = "json", serde(default = "default_true"))]
187    pub header_row: bool,
188    pub headers: Vec<String>,
189    pub totals_row: bool,
190}
191
192#[cfg(feature = "json")]
193fn default_true() -> bool {
194    true
195}
196
197#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
198#[derive(Clone, Debug)]
199pub struct MergedRange {
200    pub start_row: u32,
201    pub start_col: u32,
202    pub end_row: u32,
203    pub end_col: u32,
204}
205
206impl MergedRange {
207    pub fn contains(&self, row: u32, col: u32) -> bool {
208        row >= self.start_row && row <= self.end_row && col >= self.start_col && col <= self.end_col
209    }
210}
211
212#[derive(Clone, Copy, Debug)]
213pub enum AccessGranularity {
214    Cell,     // Random cell access (mmap)
215    Range,    // Range-based access (columnar)
216    Sheet,    // Sheet-at-a-time (umya, Calamine)
217    Workbook, // All-or-nothing (JSON)
218}
219
220#[derive(Clone, Debug)]
221pub enum LoadStrategy {
222    /// Load entire workbook immediately (small files, testing)
223    EagerAll,
224
225    /// Load sheet when first accessed (Calamine, umya default)
226    EagerSheet,
227
228    /// Load row/column chunks on access (columnar formats)
229    LazyRange { row_chunk: usize, col_chunk: usize },
230
231    /// Load individual cells on access (mmap, remote APIs)
232    LazyCell,
233
234    /// Never load - write-only mode
235    WriteOnly,
236}
237
238#[derive(Clone, Debug, Default, PartialEq, Eq)]
239pub struct AdapterLoadStats {
240    pub formula_cells_observed: Option<u64>,
241    pub value_cells_observed: Option<u64>,
242    pub value_slots_handed_to_engine: Option<u64>,
243    pub formula_cells_handed_to_engine: Option<u64>,
244    pub shared_formula_tags_observed: Option<u64>,
245}
246
247/// Workbook-level calculation properties parsed from `xl/workbook.xml`'s
248/// `<calcPr .../>` element (spec §9, RFC #113).
249///
250/// This mirrors the OOXML attributes verbatim — it is a *transport* struct
251/// (parsed values, not yet mapped to engine semantics). The mapping to
252/// [`formualizer_eval::engine::CycleConfig`] is applied during
253/// [`crate::Workbook::from_reader`] (see
254/// `CalcSettings::apply_to_cycle_config`), keeping the backend free of engine
255/// dependencies.
256///
257/// `calc_mode` / `full_calc_on_load` are captured for round-trip fidelity only
258/// and are not interpreted semantically (spec §9: "out of scope semantically").
259#[derive(Clone, Debug, Default, PartialEq)]
260pub struct CalcSettings {
261    /// `iterate` attribute: `true` when iterative calculation is enabled
262    /// (`iterate="1"` or `iterate="true"`).
263    pub iterate: bool,
264    /// `iterateCount` attribute (Excel default 100 when iterate is on but the
265    /// attribute is absent).
266    pub iterate_count: Option<u32>,
267    /// `iterateDelta` attribute (Excel default 0.001 when iterate is on but the
268    /// attribute is absent).
269    pub iterate_delta: Option<f64>,
270    /// `calcMode` attribute (e.g. "auto", "manual"). Preserved for round-trip
271    /// only.
272    pub calc_mode: Option<String>,
273    /// `fullCalcOnLoad` attribute. Preserved for round-trip only.
274    pub full_calc_on_load: Option<bool>,
275}
276
277pub trait SpreadsheetReader: Send + Sync {
278    type Error: std::error::Error + Send + Sync + 'static;
279
280    fn access_granularity(&self) -> AccessGranularity;
281    fn capabilities(&self) -> BackendCaps;
282    fn sheet_names(&self) -> Result<Vec<String>, Self::Error>;
283
284    fn load_stats(&self) -> Option<AdapterLoadStats> {
285        None
286    }
287
288    /// Workbook-level defined names (workbook scoped or sheet scoped).
289    ///
290    /// Default: no defined names.
291    fn defined_names(&mut self) -> Result<Vec<DefinedName>, Self::Error> {
292        Ok(Vec::new())
293    }
294
295    /// Workbook-level calculation properties (`<calcPr>`), spec §9.
296    ///
297    /// `None` means the backend does not surface calc settings (no `<calcPr>`
298    /// or no support); callers must leave the engine cycle config untouched in
299    /// that case. Only the XLSX backends populate this today.
300    fn calc_settings(&self) -> Option<CalcSettings> {
301        None
302    }
303
304    /// Constructor variants for different environments
305    fn open_path<P: AsRef<Path>>(path: P) -> Result<Self, Self::Error>
306    where
307        Self: Sized;
308
309    fn open_reader(reader: Box<dyn Read + Send + Sync>) -> Result<Self, Self::Error>
310    where
311        Self: Sized;
312
313    fn open_bytes(data: Vec<u8>) -> Result<Self, Self::Error>
314    where
315        Self: Sized;
316
317    fn read_cell(
318        &mut self,
319        sheet: &str,
320        row: u32,
321        col: u32,
322    ) -> Result<Option<CellData>, Self::Error> {
323        // Default: fallback to range read
324        let mut range = self.read_range(sheet, (row, col), (row, col))?;
325        Ok(range.remove(&(row, col)))
326    }
327
328    fn read_range(
329        &mut self,
330        sheet: &str,
331        start: (u32, u32),
332        end: (u32, u32),
333    ) -> Result<BTreeMap<(u32, u32), CellData>, Self::Error>;
334
335    fn read_sheet(&mut self, sheet: &str) -> Result<SheetData, Self::Error>;
336
337    fn sheet_bounds(&self, sheet: &str) -> Option<(u32, u32)>;
338    fn is_loaded(&self, sheet: &str, row: Option<u32>, col: Option<u32>) -> bool;
339}
340
341pub trait SpreadsheetWriter: Send + Sync {
342    type Error: std::error::Error + Send + Sync + 'static;
343
344    fn write_cell(
345        &mut self,
346        sheet: &str,
347        row: u32,
348        col: u32,
349        data: CellData,
350    ) -> Result<(), Self::Error>;
351
352    fn write_range(
353        &mut self,
354        sheet: &str,
355        cells: BTreeMap<(u32, u32), CellData>,
356    ) -> Result<(), Self::Error>;
357
358    fn clear_range(
359        &mut self,
360        sheet: &str,
361        start: (u32, u32),
362        end: (u32, u32),
363    ) -> Result<(), Self::Error>;
364
365    fn create_sheet(&mut self, name: &str) -> Result<(), Self::Error>;
366    fn delete_sheet(&mut self, name: &str) -> Result<(), Self::Error>;
367    fn rename_sheet(&mut self, old: &str, new: &str) -> Result<(), Self::Error>;
368
369    fn flush(&mut self) -> Result<(), Self::Error>;
370    fn save(&mut self) -> Result<(), Self::Error> {
371        self.save_to(SaveDestination::InPlace).map(|_| ())
372    }
373
374    /// Advanced save: specify destination (in place, path, writer, or bytes in memory).
375    /// Returns Ok(Some(bytes)) only for Bytes destination, else Ok(None).
376    fn save_to<'a>(&mut self, dest: SaveDestination<'a>) -> Result<Option<Vec<u8>>, Self::Error> {
377        let _ = dest;
378        unreachable!("save_to must be implemented by writer backends that expose persistence");
379    }
380
381    fn save_as_path<P: AsRef<std::path::Path>>(&mut self, path: P) -> Result<(), Self::Error> {
382        self.save_to(SaveDestination::Path(path.as_ref()))
383            .map(|_| ())
384    }
385
386    fn save_to_bytes(&mut self) -> Result<Vec<u8>, Self::Error> {
387        self.save_to(SaveDestination::Bytes)
388            .map(|opt| opt.unwrap_or_default())
389    }
390
391    fn write_to<W: Write>(&mut self, writer: &mut W) -> Result<(), Self::Error> {
392        self.save_to(SaveDestination::Writer(writer)).map(|_| ())
393    }
394}
395
396/// Enum describing where a workbook should be saved.
397pub enum SaveDestination<'a> {
398    InPlace,                   // Use original path, if known
399    Path(&'a std::path::Path), // Write to provided filesystem path
400    Writer(&'a mut dyn Write), // Stream to arbitrary writer
401    Bytes,                     // Return bytes in memory
402}
403
404pub trait SpreadsheetIO: SpreadsheetReader + SpreadsheetWriter {}