visi_core/error.rs
1//! The error type returned by `visi-core`'s public API.
2
3use crate::core::engine::EngineError;
4
5/// The kind of workbook object an [`Error`] refers to.
6///
7/// Used by the [`Error::NotFound`] / [`Error::AlreadyExists`] /
8/// [`Error::NameTaken`] variants so callers can distinguish "no such sheet"
9/// from "no such table" without parsing the message text.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11#[non_exhaustive]
12pub enum ObjectKind {
13 /// A worksheet.
14 Sheet,
15 /// An Excel Table (ListObject) -- a named range with a header row, not a
16 /// worksheet. See [`crate::core::ExcelTable`].
17 Table,
18 /// A column within an Excel Table.
19 TableColumn,
20 /// A pivot table.
21 PivotTable,
22 /// A field within a pivot table.
23 PivotField,
24 /// A chart.
25 Chart,
26 /// A VBA module.
27 VbaModule,
28}
29
30impl ObjectKind {
31 /// The human-readable name used in error messages ("sheet", "table", ...).
32 pub fn as_str(self) -> &'static str {
33 match self {
34 ObjectKind::Sheet => "sheet",
35 ObjectKind::Table => "table",
36 ObjectKind::TableColumn => "table column",
37 ObjectKind::PivotTable => "pivot table",
38 ObjectKind::PivotField => "pivot field",
39 ObjectKind::Chart => "chart",
40 ObjectKind::VbaModule => "VBA module",
41 }
42 }
43}
44
45impl std::fmt::Display for ObjectKind {
46 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47 f.write_str(self.as_str())
48 }
49}
50
51/// Errors returned by `visi-core`'s public API.
52///
53/// This enum is `#[non_exhaustive]`: match with a `_` arm, since new variants
54/// may be added in a minor release.
55#[derive(Debug, Clone, PartialEq)]
56#[non_exhaustive]
57pub enum Error {
58 /// No object of this kind goes by this name (or id, for charts).
59 NotFound {
60 /// What was being looked up.
61 kind: ObjectKind,
62 /// The name that was not found.
63 name: String,
64 /// The names that *do* exist, when the call can supply them cheaply,
65 /// so callers can render a "did you mean" hint. Often empty.
66 available: Vec<String>,
67 },
68 /// An object of this kind already goes by this name, so it cannot be added.
69 AlreadyExists {
70 /// What was being added.
71 kind: ObjectKind,
72 /// The name that collided.
73 name: String,
74 },
75 /// A rename was rejected because the new name is already in use.
76 ///
77 /// Distinct from [`Error::AlreadyExists`], which is raised when *creating*.
78 NameTaken {
79 /// What was being renamed.
80 kind: ObjectKind,
81 /// The requested new name.
82 name: String,
83 },
84 /// A name was rejected as structurally invalid, independent of collisions.
85 InvalidName {
86 /// What was being named.
87 kind: ObjectKind,
88 /// The rejected name.
89 name: String,
90 /// Why it was rejected.
91 reason: String,
92 },
93 /// A row or column index fell outside the sheet.
94 OutOfBounds {
95 /// What was being indexed ("row" or "column").
96 what: &'static str,
97 /// The offending 0-based index.
98 index: usize,
99 /// The number of rows/columns that exist.
100 len: usize,
101 },
102 /// A cell range was malformed -- for example, an end before its start.
103 InvalidRange(String),
104 /// The operation needs at least one sheet and the workbook has none.
105 EmptyWorkbook,
106 /// The last remaining sheet cannot be deleted; a workbook needs one.
107 LastSheetInWorkbook,
108 /// A worksheet can carry only one bound VBA document module.
109 DocumentModuleExists,
110 /// The operation was rejected by a lower layer that does not yet report a
111 /// typed error -- currently the Excel Table and pivot internals.
112 ///
113 /// Carries message text only. Do not match on the string; variants will be
114 /// carved out of this one as those layers are typed, which is why [`Error`]
115 /// is `#[non_exhaustive]`.
116 InvalidArgument(String),
117 /// Reading or writing the `.xlsx` container failed.
118 Xlsx(String),
119 /// Reading or writing the VBA project failed.
120 Vba(String),
121 /// A VBA module failed to parse.
122 ///
123 /// Carries the position separately from the message so a caller can point
124 /// at the offending line -- an editor integration, or `visi macro check
125 /// --json` -- without parsing the text back out.
126 VbaSyntax {
127 /// What went wrong, phrased for someone reading it.
128 message: String,
129 /// The module the error is in, when the caller knew one.
130 module: Option<String>,
131 /// 1-based line number within that module's source.
132 line: u32,
133 /// 1-based column number, counted in characters.
134 column: u32,
135 },
136 /// A VBA procedure raised a run-time error.
137 ///
138 /// Carries VBA's own `Err.Number` so a caller can compare it against what
139 /// Excel would have raised, which is what the differential fuzzer does.
140 VbaRuntime {
141 /// `Err.Description`.
142 message: String,
143 /// `Err.Number`.
144 number: i32,
145 },
146 /// Formula evaluation failed.
147 Eval(EngineError),
148}
149
150impl Error {
151 /// A [`Error::NotFound`] with no "did you mean" candidates.
152 pub fn not_found(kind: ObjectKind, name: impl Into<String>) -> Self {
153 Error::NotFound {
154 kind,
155 name: name.into(),
156 available: Vec::new(),
157 }
158 }
159
160 /// A [`Error::NotFound`] that also carries the names that do exist.
161 pub fn not_found_among(
162 kind: ObjectKind,
163 name: impl Into<String>,
164 available: Vec<String>,
165 ) -> Self {
166 Error::NotFound {
167 kind,
168 name: name.into(),
169 available,
170 }
171 }
172}
173
174impl std::fmt::Display for Error {
175 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176 match self {
177 Error::NotFound {
178 kind,
179 name,
180 available,
181 } => {
182 write!(f, "{kind} '{name}' not found")?;
183 if !available.is_empty() {
184 write!(f, ". Available {kind}s: {}", available.join(", "))?;
185 }
186 Ok(())
187 }
188 Error::AlreadyExists { kind, name } => write!(f, "{kind} '{name}' already exists"),
189 Error::NameTaken { kind, name } => {
190 write!(f, "{kind} name '{name}' is already taken")
191 }
192 Error::InvalidName { kind, name, reason } => {
193 write!(f, "invalid {kind} name '{name}': {reason}")
194 }
195 Error::OutOfBounds { what, index, len } => {
196 write!(f, "{what} index {index} is out of bounds (sheet has {len})")
197 }
198 Error::InvalidRange(msg) => write!(f, "invalid range: {msg}"),
199 Error::EmptyWorkbook => f.write_str("workbook contains no sheets"),
200 Error::LastSheetInWorkbook => {
201 f.write_str("cannot delete the only sheet in the workbook")
202 }
203 Error::DocumentModuleExists => {
204 f.write_str("that sheet already has a bound document module")
205 }
206 Error::InvalidArgument(msg) => f.write_str(msg),
207 Error::Xlsx(msg) => write!(f, "xlsx error: {msg}"),
208 Error::Vba(msg) => write!(f, "VBA error: {msg}"),
209 Error::VbaRuntime { message, number } => {
210 write!(f, "run-time error {number}: {message}")
211 }
212 Error::VbaSyntax {
213 message,
214 module,
215 line,
216 column,
217 } => match module {
218 Some(m) => write!(f, "{m}({line},{column}): {message}"),
219 None => write!(f, "line {line}, column {column}: {message}"),
220 },
221 Error::Eval(err) => write!(f, "evaluation error: {err}"),
222 }
223 }
224}
225
226impl std::error::Error for Error {
227 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
228 match self {
229 Error::Eval(err) => Some(err),
230 _ => None,
231 }
232 }
233}
234
235impl From<EngineError> for Error {
236 fn from(err: EngineError) -> Self {
237 Error::Eval(err)
238 }
239}
240
241/// A `Result` whose error type is [`Error`].
242pub type Result<T> = std::result::Result<T, Error>;
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247
248 #[test]
249 fn display_reads_naturally() {
250 let e = Error::not_found(ObjectKind::PivotTable, "Sales");
251 assert_eq!(e.to_string(), "pivot table 'Sales' not found");
252
253 let e = Error::NameTaken {
254 kind: ObjectKind::Sheet,
255 name: "Data".into(),
256 };
257 assert_eq!(e.to_string(), "sheet name 'Data' is already taken");
258 }
259
260 #[test]
261 fn is_a_std_error() {
262 fn assert_std_error<E: std::error::Error>(_: &E) {}
263 assert_std_error(&Error::EmptyWorkbook);
264 let boxed: Box<dyn std::error::Error> = Box::new(Error::EmptyWorkbook);
265 assert_eq!(boxed.to_string(), "workbook contains no sheets");
266 }
267
268 #[test]
269 fn callers_can_match_on_kind_without_parsing_text() {
270 let e = Error::not_found(ObjectKind::Table, "Q1");
271 assert!(matches!(
272 e,
273 Error::NotFound {
274 kind: ObjectKind::Table,
275 ..
276 }
277 ));
278 }
279}