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#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
134#[derive(Clone, Debug)]
135pub struct TableDefinition {
136    pub name: String,
137    pub range: (u32, u32, u32, u32),
138    pub headers: Vec<String>,
139    pub totals_row: bool,
140}
141
142#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
143#[derive(Clone, Debug)]
144pub struct MergedRange {
145    pub start_row: u32,
146    pub start_col: u32,
147    pub end_row: u32,
148    pub end_col: u32,
149}
150
151impl MergedRange {
152    pub fn contains(&self, row: u32, col: u32) -> bool {
153        row >= self.start_row && row <= self.end_row && col >= self.start_col && col <= self.end_col
154    }
155}
156
157#[derive(Clone, Copy, Debug)]
158pub enum AccessGranularity {
159    Cell,     // Random cell access (mmap)
160    Range,    // Range-based access (columnar)
161    Sheet,    // Sheet-at-a-time (umya, Calamine)
162    Workbook, // All-or-nothing (JSON)
163}
164
165#[derive(Clone, Debug)]
166pub enum LoadStrategy {
167    /// Load entire workbook immediately (small files, testing)
168    EagerAll,
169
170    /// Load sheet when first accessed (Calamine, umya default)
171    EagerSheet,
172
173    /// Load row/column chunks on access (columnar formats)
174    LazyRange { row_chunk: usize, col_chunk: usize },
175
176    /// Load individual cells on access (mmap, remote APIs)
177    LazyCell,
178
179    /// Never load - write-only mode
180    WriteOnly,
181}
182
183pub trait SpreadsheetReader: Send + Sync {
184    type Error: std::error::Error + Send + Sync + 'static;
185
186    fn access_granularity(&self) -> AccessGranularity;
187    fn capabilities(&self) -> BackendCaps;
188    fn sheet_names(&self) -> Result<Vec<String>, Self::Error>;
189
190    /// Constructor variants for different environments
191    fn open_path<P: AsRef<Path>>(path: P) -> Result<Self, Self::Error>
192    where
193        Self: Sized;
194
195    fn open_reader(reader: Box<dyn Read + Send + Sync>) -> Result<Self, Self::Error>
196    where
197        Self: Sized;
198
199    fn open_bytes(data: Vec<u8>) -> Result<Self, Self::Error>
200    where
201        Self: Sized;
202
203    fn read_cell(
204        &mut self,
205        sheet: &str,
206        row: u32,
207        col: u32,
208    ) -> Result<Option<CellData>, Self::Error> {
209        // Default: fallback to range read
210        let mut range = self.read_range(sheet, (row, col), (row, col))?;
211        Ok(range.remove(&(row, col)))
212    }
213
214    fn read_range(
215        &mut self,
216        sheet: &str,
217        start: (u32, u32),
218        end: (u32, u32),
219    ) -> Result<BTreeMap<(u32, u32), CellData>, Self::Error>;
220
221    fn read_sheet(&mut self, sheet: &str) -> Result<SheetData, Self::Error>;
222
223    fn sheet_bounds(&self, sheet: &str) -> Option<(u32, u32)>;
224    fn is_loaded(&self, sheet: &str, row: Option<u32>, col: Option<u32>) -> bool;
225}
226
227pub trait SpreadsheetWriter: Send + Sync {
228    type Error: std::error::Error + Send + Sync + 'static;
229
230    fn write_cell(
231        &mut self,
232        sheet: &str,
233        row: u32,
234        col: u32,
235        data: CellData,
236    ) -> Result<(), Self::Error>;
237
238    fn write_range(
239        &mut self,
240        sheet: &str,
241        cells: BTreeMap<(u32, u32), CellData>,
242    ) -> Result<(), Self::Error>;
243
244    fn clear_range(
245        &mut self,
246        sheet: &str,
247        start: (u32, u32),
248        end: (u32, u32),
249    ) -> Result<(), Self::Error>;
250
251    fn create_sheet(&mut self, name: &str) -> Result<(), Self::Error>;
252    fn delete_sheet(&mut self, name: &str) -> Result<(), Self::Error>;
253    fn rename_sheet(&mut self, old: &str, new: &str) -> Result<(), Self::Error>;
254
255    fn flush(&mut self) -> Result<(), Self::Error>;
256    fn save(&mut self) -> Result<(), Self::Error> {
257        self.save_to(SaveDestination::InPlace).map(|_| ())
258    }
259
260    /// Advanced save: specify destination (in place, path, writer, or bytes in memory).
261    /// Returns Ok(Some(bytes)) only for Bytes destination, else Ok(None).
262    fn save_to<'a>(&mut self, dest: SaveDestination<'a>) -> Result<Option<Vec<u8>>, Self::Error> {
263        let _ = dest;
264        unreachable!("save_to must be implemented by writer backends that expose persistence");
265    }
266
267    fn save_as_path<P: AsRef<std::path::Path>>(&mut self, path: P) -> Result<(), Self::Error> {
268        self.save_to(SaveDestination::Path(path.as_ref()))
269            .map(|_| ())
270    }
271
272    fn save_to_bytes(&mut self) -> Result<Vec<u8>, Self::Error> {
273        self.save_to(SaveDestination::Bytes)
274            .map(|opt| opt.unwrap_or_default())
275    }
276
277    fn write_to<W: Write>(&mut self, writer: &mut W) -> Result<(), Self::Error> {
278        self.save_to(SaveDestination::Writer(writer)).map(|_| ())
279    }
280}
281
282/// Enum describing where a workbook should be saved.
283pub enum SaveDestination<'a> {
284    InPlace,                   // Use original path, if known
285    Path(&'a std::path::Path), // Write to provided filesystem path
286    Writer(&'a mut dyn Write), // Stream to arbitrary writer
287    Bytes,                     // Return bytes in memory
288}
289
290pub trait SpreadsheetIO: SpreadsheetReader + SpreadsheetWriter {}