Skip to main content

sheetkit_core/
error.rs

1//! Error types for the SheetKit core library.
2//!
3//! Provides a comprehensive [`Error`] enum covering all failure modes
4//! encountered when reading, writing, and manipulating Excel workbooks.
5
6use thiserror::Error;
7
8/// The top-level error type for SheetKit.
9#[derive(Error, Debug)]
10pub enum Error {
11    /// The given string is not a valid A1-style cell reference.
12    #[error("invalid cell reference: {0}")]
13    InvalidCellReference(String),
14
15    /// The row number is out of the allowed range (1..=1_048_576).
16    #[error("invalid row number: {0}")]
17    InvalidRowNumber(u32),
18
19    /// The column number is out of the allowed range (1..=16_384).
20    #[error("invalid column number: {0}")]
21    InvalidColumnNumber(u32),
22
23    /// No sheet with the given name exists in the workbook.
24    #[error("sheet '{name}' does not exist")]
25    SheetNotFound { name: String },
26
27    /// A sheet with the given name already exists.
28    #[error("sheet '{name}' already exists")]
29    SheetAlreadyExists { name: String },
30
31    /// The sheet name violates Excel naming rules.
32    #[error("invalid sheet name: {0}")]
33    InvalidSheetName(String),
34
35    /// An underlying I/O error.
36    #[error("I/O error: {0}")]
37    Io(#[from] std::io::Error),
38
39    /// An error originating from the ZIP layer.
40    #[error("ZIP error: {0}")]
41    Zip(String),
42
43    /// An error encountered while parsing XML.
44    #[error("XML parse error: {0}")]
45    XmlParse(String),
46
47    /// An error encountered while deserializing XML into typed structures.
48    #[error("XML deserialization error: {0}")]
49    XmlDeserialize(String),
50
51    /// Column width exceeds the allowed maximum (255).
52    #[error("column width {width} exceeds maximum {max}")]
53    ColumnWidthExceeded { width: f64, max: f64 },
54
55    /// Row height exceeds the allowed maximum (409).
56    #[error("row height {height} exceeds maximum {max}")]
57    RowHeightExceeded { height: f64, max: f64 },
58
59    /// A cell value exceeds the maximum character limit.
60    #[error("cell value too long: {length} characters (max {max})")]
61    CellValueTooLong { length: usize, max: usize },
62
63    /// The style ID was not found in the stylesheet.
64    #[error("style not found: {id}")]
65    StyleNotFound { id: u32 },
66
67    /// Too many cell styles have been registered.
68    #[error("cell styles exceeded maximum ({max})")]
69    CellStylesExceeded { max: usize },
70
71    /// A row has already been written; rows must be written in ascending order.
72    #[error("row {row} has already been written (must write rows in ascending order)")]
73    StreamRowAlreadyWritten { row: u32 },
74
75    /// The stream writer has already been finished.
76    #[error("stream writer already finished")]
77    StreamAlreadyFinished,
78
79    /// Column widths cannot be set after rows have been written.
80    #[error("cannot set column width after rows have been written")]
81    StreamColumnsAfterRows,
82
83    /// Merge cell ranges overlap.
84    #[error("merge cell range '{new}' overlaps with existing range '{existing}'")]
85    MergeCellOverlap { new: String, existing: String },
86
87    /// The specified merge cell range was not found.
88    #[error("merge cell range '{0}' not found")]
89    MergeCellNotFound(String),
90
91    /// The defined name is invalid.
92    #[error("invalid defined name: {0}")]
93    InvalidDefinedName(String),
94
95    /// The specified defined name was not found.
96    #[error("defined name '{name}' not found")]
97    DefinedNameNotFound { name: String },
98
99    /// A circular reference was detected during formula evaluation.
100    #[error("circular reference detected at {cell}")]
101    CircularReference { cell: String },
102
103    /// The formula references an unknown function.
104    #[error("unknown function: {name}")]
105    UnknownFunction { name: String },
106
107    /// A function received the wrong number of arguments.
108    #[error("function {name} expects {expected} arguments, got {got}")]
109    WrongArgCount {
110        name: String,
111        expected: String,
112        got: usize,
113    },
114
115    /// A general formula evaluation error.
116    #[error("formula evaluation error: {0}")]
117    FormulaError(String),
118
119    /// The specified pivot table was not found.
120    #[error("pivot table '{name}' not found")]
121    PivotTableNotFound { name: String },
122
123    /// A pivot table with the given name already exists.
124    #[error("pivot table '{name}' already exists")]
125    PivotTableAlreadyExists { name: String },
126
127    /// The source data range for a pivot table is invalid.
128    #[error("invalid source range: {0}")]
129    InvalidSourceRange(String),
130
131    /// The image format is not supported.
132    #[error("unsupported image format: {format}")]
133    UnsupportedImageFormat { format: String },
134
135    /// The file is encrypted and requires a password to open.
136    #[error("file is encrypted, password required")]
137    FileEncrypted,
138
139    /// The provided password is incorrect.
140    #[error("incorrect password")]
141    IncorrectPassword,
142
143    /// The encryption method is not supported.
144    #[error("unsupported encryption method: {0}")]
145    UnsupportedEncryption(String),
146
147    /// The outline level exceeds the allowed maximum (7).
148    #[error("outline level {level} exceeds maximum {max}")]
149    OutlineLevelExceeded { level: u8, max: u8 },
150
151    /// A merge cell reference format is invalid.
152    #[error("invalid merge cell reference: {0}")]
153    InvalidMergeCellReference(String),
154
155    /// A cell range reference (sqref) is invalid.
156    #[error("invalid reference: {reference}")]
157    InvalidReference { reference: String },
158
159    /// A function argument or configuration value is invalid.
160    #[error("invalid argument: {0}")]
161    InvalidArgument(String),
162
163    /// An internal or otherwise unclassified error.
164    #[error("internal error: {0}")]
165    Internal(String),
166}
167
168/// A convenience alias used throughout the crate.
169pub type Result<T> = std::result::Result<T, Error>;
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174
175    #[test]
176    fn test_error_display_invalid_cell_reference() {
177        let err = Error::InvalidCellReference("XYZ0".to_string());
178        assert_eq!(err.to_string(), "invalid cell reference: XYZ0");
179    }
180
181    #[test]
182    fn test_error_display_sheet_not_found() {
183        let err = Error::SheetNotFound {
184            name: "Missing".to_string(),
185        };
186        assert_eq!(err.to_string(), "sheet 'Missing' does not exist");
187    }
188
189    #[test]
190    fn test_error_display_sheet_already_exists() {
191        let err = Error::SheetAlreadyExists {
192            name: "Sheet1".to_string(),
193        };
194        assert_eq!(err.to_string(), "sheet 'Sheet1' already exists");
195    }
196
197    #[test]
198    fn test_error_display_invalid_sheet_name() {
199        let err = Error::InvalidSheetName("bad[name".to_string());
200        assert_eq!(err.to_string(), "invalid sheet name: bad[name");
201    }
202
203    #[test]
204    fn test_error_display_invalid_row_number() {
205        let err = Error::InvalidRowNumber(0);
206        assert_eq!(err.to_string(), "invalid row number: 0");
207    }
208
209    #[test]
210    fn test_error_display_invalid_column_number() {
211        let err = Error::InvalidColumnNumber(99999);
212        assert_eq!(err.to_string(), "invalid column number: 99999");
213    }
214
215    #[test]
216    fn test_error_display_io() {
217        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "gone");
218        let err = Error::Io(io_err);
219        assert_eq!(err.to_string(), "I/O error: gone");
220    }
221
222    #[test]
223    fn test_error_display_zip() {
224        let err = Error::Zip("corrupted archive".to_string());
225        assert_eq!(err.to_string(), "ZIP error: corrupted archive");
226    }
227
228    #[test]
229    fn test_error_display_xml_parse() {
230        let err = Error::XmlParse("unexpected EOF".to_string());
231        assert_eq!(err.to_string(), "XML parse error: unexpected EOF");
232    }
233
234    #[test]
235    fn test_error_display_xml_deserialize() {
236        let err = Error::XmlDeserialize("missing attribute".to_string());
237        assert_eq!(
238            err.to_string(),
239            "XML deserialization error: missing attribute"
240        );
241    }
242
243    #[test]
244    fn test_error_display_cell_value_too_long() {
245        let err = Error::CellValueTooLong {
246            length: 40000,
247            max: 32767,
248        };
249        assert_eq!(
250            err.to_string(),
251            "cell value too long: 40000 characters (max 32767)"
252        );
253    }
254
255    #[test]
256    fn test_error_display_outline_level_exceeded() {
257        let err = Error::OutlineLevelExceeded { level: 8, max: 7 };
258        assert_eq!(err.to_string(), "outline level 8 exceeds maximum 7");
259    }
260
261    #[test]
262    fn test_error_display_invalid_merge_cell_reference() {
263        let err = Error::InvalidMergeCellReference("bad ref".to_string());
264        assert_eq!(err.to_string(), "invalid merge cell reference: bad ref");
265    }
266
267    #[test]
268    fn test_error_display_internal() {
269        let err = Error::Internal("something went wrong".to_string());
270        assert_eq!(err.to_string(), "internal error: something went wrong");
271    }
272
273    #[test]
274    fn test_from_io_error() {
275        let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied");
276        let err: Error = io_err.into();
277        assert!(matches!(err, Error::Io(_)));
278    }
279
280    #[test]
281    fn test_error_is_send_and_sync() {
282        fn assert_send<T: Send>() {}
283        fn assert_sync<T: Sync>() {}
284        assert_send::<Error>();
285        assert_sync::<Error>();
286    }
287}