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}
114
115#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
116#[cfg_attr(feature = "json", serde(rename_all = "lowercase"))]
117#[derive(Clone, Debug, PartialEq, Eq, Hash, Default)]
118pub enum NamedRangeScope {
119    #[default]
120    Workbook,
121    Sheet,
122}
123
124#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
125#[derive(Clone, Debug)]
126pub struct NamedRange {
127    pub name: String,
128    #[cfg_attr(feature = "json", serde(default))]
129    pub scope: NamedRangeScope,
130    pub address: RangeAddress,
131}
132
133/// Stable representation of workbook/sheet scoped defined names.
134///
135/// Stage 1 supports only range-backed and literal-backed names.
136#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
137#[cfg_attr(feature = "json", serde(rename_all = "lowercase"))]
138#[derive(Clone, Debug, PartialEq, Eq, Hash, Default)]
139pub enum DefinedNameScope {
140    #[default]
141    Workbook,
142    Sheet,
143}
144
145#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
146#[cfg_attr(feature = "json", serde(tag = "type", rename_all = "lowercase"))]
147#[derive(Clone, Debug, PartialEq)]
148pub enum DefinedNameDefinition {
149    Range { address: RangeAddress },
150    Literal { value: LiteralValue },
151}
152
153#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
154#[derive(Clone, Debug, PartialEq)]
155pub struct DefinedName {
156    pub name: String,
157
158    #[cfg_attr(feature = "json", serde(default))]
159    pub scope: DefinedNameScope,
160
161    /// Sheet name for sheet-scoped names.
162    ///
163    /// For workbook-scoped names, this must be None.
164    #[cfg_attr(
165        feature = "json",
166        serde(default, skip_serializing_if = "Option::is_none")
167    )]
168    pub scope_sheet: Option<String>,
169
170    pub definition: DefinedNameDefinition,
171}
172
173#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
174#[derive(Clone, Debug)]
175pub struct TableDefinition {
176    pub name: String,
177    pub range: (u32, u32, u32, u32),
178    /// Whether the first row of `range` is a headers row.
179    ///
180    /// Deterministic resize rule:
181    /// - Tables are metadata-only; writing values just below/next to a table does NOT auto-expand
182    ///   the table. Callers must explicitly update table metadata (range/flags) if they want a
183    ///   resize.
184    #[cfg_attr(feature = "json", serde(default = "default_true"))]
185    pub header_row: bool,
186    pub headers: Vec<String>,
187    pub totals_row: bool,
188}
189
190#[cfg(feature = "json")]
191fn default_true() -> bool {
192    true
193}
194
195#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
196#[derive(Clone, Debug)]
197pub struct MergedRange {
198    pub start_row: u32,
199    pub start_col: u32,
200    pub end_row: u32,
201    pub end_col: u32,
202}
203
204impl MergedRange {
205    pub fn contains(&self, row: u32, col: u32) -> bool {
206        row >= self.start_row && row <= self.end_row && col >= self.start_col && col <= self.end_col
207    }
208}
209
210#[derive(Clone, Copy, Debug)]
211pub enum AccessGranularity {
212    Cell,     // Random cell access (mmap)
213    Range,    // Range-based access (columnar)
214    Sheet,    // Sheet-at-a-time (umya, Calamine)
215    Workbook, // All-or-nothing (JSON)
216}
217
218#[derive(Clone, Debug)]
219pub enum LoadStrategy {
220    /// Load entire workbook immediately (small files, testing)
221    EagerAll,
222
223    /// Load sheet when first accessed (Calamine, umya default)
224    EagerSheet,
225
226    /// Load row/column chunks on access (columnar formats)
227    LazyRange { row_chunk: usize, col_chunk: usize },
228
229    /// Load individual cells on access (mmap, remote APIs)
230    LazyCell,
231
232    /// Never load - write-only mode
233    WriteOnly,
234}
235
236pub trait SpreadsheetReader: Send + Sync {
237    type Error: std::error::Error + Send + Sync + 'static;
238
239    fn access_granularity(&self) -> AccessGranularity;
240    fn capabilities(&self) -> BackendCaps;
241    fn sheet_names(&self) -> Result<Vec<String>, Self::Error>;
242
243    /// Workbook-level defined names (workbook scoped or sheet scoped).
244    ///
245    /// Default: no defined names.
246    fn defined_names(&mut self) -> Result<Vec<DefinedName>, Self::Error> {
247        Ok(Vec::new())
248    }
249
250    /// Constructor variants for different environments
251    fn open_path<P: AsRef<Path>>(path: P) -> Result<Self, Self::Error>
252    where
253        Self: Sized;
254
255    fn open_reader(reader: Box<dyn Read + Send + Sync>) -> Result<Self, Self::Error>
256    where
257        Self: Sized;
258
259    fn open_bytes(data: Vec<u8>) -> Result<Self, Self::Error>
260    where
261        Self: Sized;
262
263    fn read_cell(
264        &mut self,
265        sheet: &str,
266        row: u32,
267        col: u32,
268    ) -> Result<Option<CellData>, Self::Error> {
269        // Default: fallback to range read
270        let mut range = self.read_range(sheet, (row, col), (row, col))?;
271        Ok(range.remove(&(row, col)))
272    }
273
274    fn read_range(
275        &mut self,
276        sheet: &str,
277        start: (u32, u32),
278        end: (u32, u32),
279    ) -> Result<BTreeMap<(u32, u32), CellData>, Self::Error>;
280
281    fn read_sheet(&mut self, sheet: &str) -> Result<SheetData, Self::Error>;
282
283    fn sheet_bounds(&self, sheet: &str) -> Option<(u32, u32)>;
284    fn is_loaded(&self, sheet: &str, row: Option<u32>, col: Option<u32>) -> bool;
285}
286
287pub trait SpreadsheetWriter: Send + Sync {
288    type Error: std::error::Error + Send + Sync + 'static;
289
290    fn write_cell(
291        &mut self,
292        sheet: &str,
293        row: u32,
294        col: u32,
295        data: CellData,
296    ) -> Result<(), Self::Error>;
297
298    fn write_range(
299        &mut self,
300        sheet: &str,
301        cells: BTreeMap<(u32, u32), CellData>,
302    ) -> Result<(), Self::Error>;
303
304    fn clear_range(
305        &mut self,
306        sheet: &str,
307        start: (u32, u32),
308        end: (u32, u32),
309    ) -> Result<(), Self::Error>;
310
311    fn create_sheet(&mut self, name: &str) -> Result<(), Self::Error>;
312    fn delete_sheet(&mut self, name: &str) -> Result<(), Self::Error>;
313    fn rename_sheet(&mut self, old: &str, new: &str) -> Result<(), Self::Error>;
314
315    fn flush(&mut self) -> Result<(), Self::Error>;
316    fn save(&mut self) -> Result<(), Self::Error> {
317        self.save_to(SaveDestination::InPlace).map(|_| ())
318    }
319
320    /// Advanced save: specify destination (in place, path, writer, or bytes in memory).
321    /// Returns Ok(Some(bytes)) only for Bytes destination, else Ok(None).
322    fn save_to<'a>(&mut self, dest: SaveDestination<'a>) -> Result<Option<Vec<u8>>, Self::Error> {
323        let _ = dest;
324        unreachable!("save_to must be implemented by writer backends that expose persistence");
325    }
326
327    fn save_as_path<P: AsRef<std::path::Path>>(&mut self, path: P) -> Result<(), Self::Error> {
328        self.save_to(SaveDestination::Path(path.as_ref()))
329            .map(|_| ())
330    }
331
332    fn save_to_bytes(&mut self) -> Result<Vec<u8>, Self::Error> {
333        self.save_to(SaveDestination::Bytes)
334            .map(|opt| opt.unwrap_or_default())
335    }
336
337    fn write_to<W: Write>(&mut self, writer: &mut W) -> Result<(), Self::Error> {
338        self.save_to(SaveDestination::Writer(writer)).map(|_| ())
339    }
340}
341
342/// Enum describing where a workbook should be saved.
343pub enum SaveDestination<'a> {
344    InPlace,                   // Use original path, if known
345    Path(&'a std::path::Path), // Write to provided filesystem path
346    Writer(&'a mut dyn Write), // Stream to arbitrary writer
347    Bytes,                     // Return bytes in memory
348}
349
350pub trait SpreadsheetIO: SpreadsheetReader + SpreadsheetWriter {}