Skip to main content

visi_core/
error.rs

1#![allow(missing_docs)]
2use crate::core::engine::EngineError;
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
5#[non_exhaustive]
6pub enum ObjectKind {
7    Sheet,
8
9    Table,
10
11    TableColumn,
12
13    PivotTable,
14
15    PivotField,
16
17    Chart,
18
19    VbaModule,
20}
21
22impl ObjectKind {
23    pub fn as_str(self) -> &'static str {
24        match self {
25            ObjectKind::Sheet => "sheet",
26            ObjectKind::Table => "table",
27            ObjectKind::TableColumn => "table column",
28            ObjectKind::PivotTable => "pivot table",
29            ObjectKind::PivotField => "pivot field",
30            ObjectKind::Chart => "chart",
31            ObjectKind::VbaModule => "VBA module",
32        }
33    }
34}
35
36impl std::fmt::Display for ObjectKind {
37    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38        f.write_str(self.as_str())
39    }
40}
41
42#[derive(Debug, Clone, PartialEq)]
43#[non_exhaustive]
44pub enum Error {
45    NotFound {
46        kind: ObjectKind,
47
48        name: String,
49
50        available: Vec<String>,
51    },
52
53    AlreadyExists {
54        kind: ObjectKind,
55
56        name: String,
57    },
58
59    NameTaken {
60        kind: ObjectKind,
61
62        name: String,
63    },
64
65    InvalidName {
66        kind: ObjectKind,
67
68        name: String,
69
70        reason: String,
71    },
72
73    OutOfBounds {
74        what: &'static str,
75
76        index: usize,
77
78        len: usize,
79    },
80
81    InvalidRange(String),
82
83    EmptyWorkbook,
84
85    LastSheetInWorkbook,
86
87    DocumentModuleExists,
88
89    InvalidArgument(String),
90
91    Xlsx(String),
92
93    Vba(String),
94
95    VbaSyntax {
96        message: String,
97
98        module: Option<String>,
99
100        line: u32,
101
102        column: u32,
103    },
104
105    VbaRuntime {
106        message: String,
107
108        number: i32,
109    },
110
111    Eval(EngineError),
112}
113
114impl Error {
115    pub fn not_found(kind: ObjectKind, name: impl Into<String>) -> Self {
116        Error::NotFound {
117            kind,
118            name: name.into(),
119            available: Vec::new(),
120        }
121    }
122
123    pub fn not_found_among(
124        kind: ObjectKind,
125        name: impl Into<String>,
126        available: Vec<String>,
127    ) -> Self {
128        Error::NotFound {
129            kind,
130            name: name.into(),
131            available,
132        }
133    }
134}
135
136impl std::fmt::Display for Error {
137    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
138        match self {
139            Error::NotFound {
140                kind,
141                name,
142                available,
143            } => {
144                write!(f, "{kind} '{name}' not found")?;
145                if !available.is_empty() {
146                    write!(f, ". Available {kind}s: {}", available.join(", "))?;
147                }
148                Ok(())
149            }
150            Error::AlreadyExists { kind, name } => write!(f, "{kind} '{name}' already exists"),
151            Error::NameTaken { kind, name } => {
152                write!(f, "{kind} name '{name}' is already taken")
153            }
154            Error::InvalidName { kind, name, reason } => {
155                write!(f, "invalid {kind} name '{name}': {reason}")
156            }
157            Error::OutOfBounds { what, index, len } => {
158                write!(f, "{what} index {index} is out of bounds (sheet has {len})")
159            }
160            Error::InvalidRange(msg) => write!(f, "invalid range: {msg}"),
161            Error::EmptyWorkbook => f.write_str("workbook contains no sheets"),
162            Error::LastSheetInWorkbook => {
163                f.write_str("cannot delete the only sheet in the workbook")
164            }
165            Error::DocumentModuleExists => {
166                f.write_str("that sheet already has a bound document module")
167            }
168            Error::InvalidArgument(msg) => f.write_str(msg),
169            Error::Xlsx(msg) => write!(f, "xlsx error: {msg}"),
170            Error::Vba(msg) => write!(f, "VBA error: {msg}"),
171            Error::VbaRuntime { message, number } => {
172                write!(f, "run-time error {number}: {message}")
173            }
174            Error::VbaSyntax {
175                message,
176                module,
177                line,
178                column,
179            } => match module {
180                Some(m) => write!(f, "{m}({line},{column}): {message}"),
181                None => write!(f, "line {line}, column {column}: {message}"),
182            },
183            Error::Eval(err) => write!(f, "evaluation error: {err}"),
184        }
185    }
186}
187
188impl std::error::Error for Error {
189    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
190        match self {
191            Error::Eval(err) => Some(err),
192            _ => None,
193        }
194    }
195}
196
197impl From<EngineError> for Error {
198    fn from(err: EngineError) -> Self {
199        Error::Eval(err)
200    }
201}
202
203pub type Result<T> = std::result::Result<T, Error>;
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208
209    #[test]
210    fn display_reads_naturally() {
211        let e = Error::not_found(ObjectKind::PivotTable, "Sales");
212        assert_eq!(e.to_string(), "pivot table 'Sales' not found");
213
214        let e = Error::NameTaken {
215            kind: ObjectKind::Sheet,
216            name: "Data".into(),
217        };
218        assert_eq!(e.to_string(), "sheet name 'Data' is already taken");
219    }
220
221    #[test]
222    fn is_a_std_error() {
223        fn assert_std_error<E: std::error::Error>(_: &E) {}
224        assert_std_error(&Error::EmptyWorkbook);
225        let boxed: Box<dyn std::error::Error> = Box::new(Error::EmptyWorkbook);
226        assert_eq!(boxed.to_string(), "workbook contains no sheets");
227    }
228
229    #[test]
230    fn callers_can_match_on_kind_without_parsing_text() {
231        let e = Error::not_found(ObjectKind::Table, "Q1");
232        assert!(matches!(
233            e,
234            Error::NotFound {
235                kind: ObjectKind::Table,
236                ..
237            }
238        ));
239    }
240}