1use thiserror::Error;
15
16pub mod codes {
20 pub const VALIDATION_ERROR: &str = "VALIDATION_ERROR";
22 pub const NOT_FOUND: &str = "NOT_FOUND";
24 pub const SCHEMA_ERROR: &str = "SCHEMA_ERROR";
26 pub const STORAGE_ERROR: &str = "STORAGE_ERROR";
28 pub const IO_ERROR: &str = "IO_ERROR";
30 pub const CONFIG_ERROR: &str = "CONFIG_ERROR";
32 pub const TABLE_NOT_FOUND: &str = "TABLE_NOT_FOUND";
34 pub const TABLE_REQUIRED: &str = "TABLE_REQUIRED";
39 pub const SCHEMA_EXISTS: &str = "SCHEMA_EXISTS";
42 pub const BACKUP_ERROR: &str = "BACKUP_ERROR";
44 pub const BATCH_ABORTED: &str = "BATCH_ABORTED";
46 pub const SNAPSHOT_ERROR: &str = "SNAPSHOT_ERROR";
48 pub const MATERIALIZE_DEST_RELATIVE: &str = "MATERIALIZE_DEST_RELATIVE";
50 pub const MATERIALIZE_DEST_INVALID: &str = "MATERIALIZE_DEST_INVALID";
52 pub const MATERIALIZE_IO_ERROR: &str = "MATERIALIZE_IO_ERROR";
54 pub const MATERIALIZE_SHA256_ERROR: &str = "MATERIALIZE_SHA256_ERROR";
56 pub const MATERIALIZE_ROW_NOT_FOUND: &str = "MATERIALIZE_ROW_NOT_FOUND";
58 pub const MATERIALIZE_EMPTY_RESULT: &str = "MATERIALIZE_EMPTY_RESULT";
60 pub const MATERIALIZE_FORMAT_ERROR: &str = "MATERIALIZE_FORMAT_ERROR";
62 pub const MATERIALIZE_FIELD_UNKNOWN: &str = "MATERIALIZE_FIELD_UNKNOWN";
64 pub const MATERIALIZE_INVALID_PARAM: &str = "MATERIALIZE_INVALID_PARAM";
66 pub const ALIAS_NOT_FOUND: &str = "ALIAS_NOT_FOUND";
68 pub const ALIAS_ALREADY_EXISTS: &str = "ALIAS_ALREADY_EXISTS";
71 pub const ALIAS_PARAMS_REQUIRED: &str = "ALIAS_PARAMS_REQUIRED";
74 pub const ALIAS_TEMPLATE_ERROR: &str = "ALIAS_TEMPLATE_ERROR";
77 pub const AMBIGUOUS_ID: &str = "AMBIGUOUS_ID";
80}
81
82#[derive(Error, Debug)]
84pub enum MiniAppError {
85 #[error("validation error on field '{field}': {reason}")]
87 Validation { field: String, reason: String },
88
89 #[error("row not found: {id}")]
91 NotFound { id: String },
92
93 #[error("schema parse error: {0}")]
95 Schema(String),
96
97 #[error("storage error: {0}")]
99 Storage(#[from] rusqlite::Error),
100
101 #[error("io error: {0}")]
103 Io(#[from] std::io::Error),
104
105 #[error("config error: {0}")]
107 Config(String),
108
109 #[error("table not found: {table}")]
111 TableNotFound { table: String },
112
113 #[error("table argument is required in multi-table mode")]
115 TableRequired,
116
117 #[error("schema already exists: {table}")]
119 SchemaExists { table: String },
120
121 #[error("backup error: {0}")]
123 Backup(String),
124
125 #[error("snapshot error: {0}")]
127 Snapshot(String),
128
129 #[error("batch aborted at op #{op_index}: {reason}")]
131 BatchAborted { op_index: usize, reason: String },
132
133 #[error("materialize dest must be absolute: {path}")]
135 MaterializeDestRelative { path: String },
136
137 #[error("materialize dest invalid '{path}': {reason}")]
139 MaterializeDestInvalid { path: String, reason: String },
140
141 #[error("materialize io error: {0}")]
143 MaterializeIo(String),
144
145 #[error("materialize sha256 error: {0}")]
147 MaterializeSha256(String),
148
149 #[error("materialize row not found: {id}")]
151 MaterializeRowNotFound { id: String },
152
153 #[error("materialize filter matched zero rows")]
155 MaterializeEmptyResult,
156
157 #[error("materialize format error: {0}")]
159 MaterializeFormatError(String),
160
161 #[error("materialize unknown field: {field}")]
163 MaterializeFieldUnknown { field: String },
164
165 #[error("materialize invalid param '{field}': {reason}")]
167 MaterializeInvalidParam { field: String, reason: String },
168
169 #[error("alias not found: {name}")]
171 AliasNotFound { name: String },
172
173 #[error("alias already exists: {name}")]
175 AliasAlreadyExists { name: String },
176
177 #[error("alias '{name}' requires params but none were provided")]
180 AliasParamsRequired { name: String },
181
182 #[error("alias template render error: {0}")]
184 AliasTemplateError(String),
185
186 #[error("ambiguous id prefix '{id_prefix}': {n} candidates", n = candidates.len())]
188 AmbiguousId {
189 id_prefix: String,
190 candidates: Vec<String>,
191 },
192}
193
194impl MiniAppError {
195 pub fn code(&self) -> &'static str {
197 match self {
198 MiniAppError::Validation { .. } => codes::VALIDATION_ERROR,
199 MiniAppError::NotFound { .. } => codes::NOT_FOUND,
200 MiniAppError::Schema(_) => codes::SCHEMA_ERROR,
201 MiniAppError::Storage(_) => codes::STORAGE_ERROR,
202 MiniAppError::Io(_) => codes::IO_ERROR,
203 MiniAppError::Config(_) => codes::CONFIG_ERROR,
204 MiniAppError::TableNotFound { .. } => codes::TABLE_NOT_FOUND,
205 MiniAppError::TableRequired => codes::TABLE_REQUIRED,
206 MiniAppError::SchemaExists { .. } => codes::SCHEMA_EXISTS,
207 MiniAppError::Backup(_) => codes::BACKUP_ERROR,
208 MiniAppError::Snapshot(_) => codes::SNAPSHOT_ERROR,
209 MiniAppError::BatchAborted { .. } => codes::BATCH_ABORTED,
210 MiniAppError::MaterializeDestRelative { .. } => codes::MATERIALIZE_DEST_RELATIVE,
211 MiniAppError::MaterializeDestInvalid { .. } => codes::MATERIALIZE_DEST_INVALID,
212 MiniAppError::MaterializeIo(_) => codes::MATERIALIZE_IO_ERROR,
213 MiniAppError::MaterializeSha256(_) => codes::MATERIALIZE_SHA256_ERROR,
214 MiniAppError::MaterializeRowNotFound { .. } => codes::MATERIALIZE_ROW_NOT_FOUND,
215 MiniAppError::MaterializeEmptyResult => codes::MATERIALIZE_EMPTY_RESULT,
216 MiniAppError::MaterializeFormatError(_) => codes::MATERIALIZE_FORMAT_ERROR,
217 MiniAppError::MaterializeFieldUnknown { .. } => codes::MATERIALIZE_FIELD_UNKNOWN,
218 MiniAppError::MaterializeInvalidParam { .. } => codes::MATERIALIZE_INVALID_PARAM,
219 MiniAppError::AliasNotFound { .. } => codes::ALIAS_NOT_FOUND,
220 MiniAppError::AliasAlreadyExists { .. } => codes::ALIAS_ALREADY_EXISTS,
221 MiniAppError::AliasParamsRequired { .. } => codes::ALIAS_PARAMS_REQUIRED,
222 MiniAppError::AliasTemplateError(_) => codes::ALIAS_TEMPLATE_ERROR,
223 MiniAppError::AmbiguousId { .. } => codes::AMBIGUOUS_ID,
224 }
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231
232 #[test]
233 fn error_code_all_variants() {
234 let cases: Vec<(&str, MiniAppError)> = vec![
235 (
236 codes::VALIDATION_ERROR,
237 MiniAppError::Validation {
238 field: "f".into(),
239 reason: "r".into(),
240 },
241 ),
242 (codes::NOT_FOUND, MiniAppError::NotFound { id: "x".into() }),
243 (codes::SCHEMA_ERROR, MiniAppError::Schema("s".into())),
244 (
245 codes::IO_ERROR,
246 MiniAppError::Io(std::io::Error::other("e")),
247 ),
248 (codes::CONFIG_ERROR, MiniAppError::Config("c".into())),
249 (
250 codes::TABLE_NOT_FOUND,
251 MiniAppError::TableNotFound { table: "t".into() },
252 ),
253 (codes::TABLE_REQUIRED, MiniAppError::TableRequired),
254 (
255 codes::SCHEMA_EXISTS,
256 MiniAppError::SchemaExists {
257 table: "my_table".into(),
258 },
259 ),
260 (
261 codes::BACKUP_ERROR,
262 MiniAppError::Backup("disk full".into()),
263 ),
264 (
265 codes::SNAPSHOT_ERROR,
266 MiniAppError::Snapshot("snapshot failed".into()),
267 ),
268 (
269 codes::BATCH_ABORTED,
270 MiniAppError::BatchAborted {
271 op_index: 2,
272 reason: "schema not found".into(),
273 },
274 ),
275 (
276 codes::MATERIALIZE_DEST_RELATIVE,
277 MiniAppError::MaterializeDestRelative {
278 path: "relative/path".into(),
279 },
280 ),
281 (
282 codes::MATERIALIZE_DEST_INVALID,
283 MiniAppError::MaterializeDestInvalid {
284 path: "/bad/path".into(),
285 reason: "parent dir not writable".into(),
286 },
287 ),
288 (
289 codes::MATERIALIZE_IO_ERROR,
290 MiniAppError::MaterializeIo("write failed".into()),
291 ),
292 (
293 codes::MATERIALIZE_SHA256_ERROR,
294 MiniAppError::MaterializeSha256("task panicked".into()),
295 ),
296 (
297 codes::MATERIALIZE_ROW_NOT_FOUND,
298 MiniAppError::MaterializeRowNotFound { id: "row-1".into() },
299 ),
300 (
301 codes::MATERIALIZE_EMPTY_RESULT,
302 MiniAppError::MaterializeEmptyResult,
303 ),
304 (
305 codes::MATERIALIZE_FORMAT_ERROR,
306 MiniAppError::MaterializeFormatError("yaml error".into()),
307 ),
308 (
309 codes::MATERIALIZE_FIELD_UNKNOWN,
310 MiniAppError::MaterializeFieldUnknown {
311 field: "unknown_field".into(),
312 },
313 ),
314 (
315 codes::MATERIALIZE_INVALID_PARAM,
316 MiniAppError::MaterializeInvalidParam {
317 field: "concat".into(),
318 reason: "concat=true requires ByFilter selector".into(),
319 },
320 ),
321 (
322 codes::ALIAS_NOT_FOUND,
323 MiniAppError::AliasNotFound {
324 name: "my_alias".into(),
325 },
326 ),
327 (
328 codes::ALIAS_ALREADY_EXISTS,
329 MiniAppError::AliasAlreadyExists {
330 name: "my_alias".into(),
331 },
332 ),
333 (
334 codes::ALIAS_PARAMS_REQUIRED,
335 MiniAppError::AliasParamsRequired {
336 name: "my_alias".into(),
337 },
338 ),
339 (
340 codes::ALIAS_TEMPLATE_ERROR,
341 MiniAppError::AliasTemplateError("template syntax error".into()),
342 ),
343 (
344 codes::AMBIGUOUS_ID,
345 MiniAppError::AmbiguousId {
346 id_prefix: "abc".into(),
347 candidates: vec!["abc-1".into(), "abc-2".into()],
348 },
349 ),
350 ];
351 for (expected_code, err) in cases {
352 assert_eq!(
353 err.code(),
354 expected_code,
355 "wrong code for variant containing code {}",
356 expected_code
357 );
358 }
359 }
360
361 #[test]
362 fn backup_error_code_is_not_storage_or_io() {
363 let err = MiniAppError::Backup("some rusqlite error".to_string());
364 assert_eq!(err.code(), codes::BACKUP_ERROR);
365 assert_ne!(err.code(), codes::STORAGE_ERROR);
366 assert_ne!(err.code(), codes::IO_ERROR);
367 }
368}