1use serde::Serialize;
15use thiserror::Error;
16
17#[derive(Debug, Clone, Serialize)]
20pub struct MatchCandidate {
21 pub line: u32,
23 pub col: u32,
25 pub snippet: String,
27}
28
29pub mod codes {
33 pub const VALIDATION_ERROR: &str = "VALIDATION_ERROR";
35 pub const NOT_FOUND: &str = "NOT_FOUND";
37 pub const SCHEMA_ERROR: &str = "SCHEMA_ERROR";
39 pub const STORAGE_ERROR: &str = "STORAGE_ERROR";
41 pub const IO_ERROR: &str = "IO_ERROR";
43 pub const CONFIG_ERROR: &str = "CONFIG_ERROR";
45 pub const TABLE_NOT_FOUND: &str = "TABLE_NOT_FOUND";
47 pub const TABLE_REQUIRED: &str = "TABLE_REQUIRED";
52 pub const SCHEMA_EXISTS: &str = "SCHEMA_EXISTS";
55 pub const BACKUP_ERROR: &str = "BACKUP_ERROR";
57 pub const BATCH_ABORTED: &str = "BATCH_ABORTED";
59 pub const SNAPSHOT_ERROR: &str = "SNAPSHOT_ERROR";
61 pub const MATERIALIZE_DEST_RELATIVE: &str = "MATERIALIZE_DEST_RELATIVE";
63 pub const MATERIALIZE_DEST_INVALID: &str = "MATERIALIZE_DEST_INVALID";
65 pub const MATERIALIZE_IO_ERROR: &str = "MATERIALIZE_IO_ERROR";
67 pub const MATERIALIZE_SHA256_ERROR: &str = "MATERIALIZE_SHA256_ERROR";
69 pub const MATERIALIZE_ROW_NOT_FOUND: &str = "MATERIALIZE_ROW_NOT_FOUND";
71 pub const MATERIALIZE_EMPTY_RESULT: &str = "MATERIALIZE_EMPTY_RESULT";
73 pub const MATERIALIZE_FORMAT_ERROR: &str = "MATERIALIZE_FORMAT_ERROR";
75 pub const MATERIALIZE_FIELD_UNKNOWN: &str = "MATERIALIZE_FIELD_UNKNOWN";
77 pub const MATERIALIZE_INVALID_PARAM: &str = "MATERIALIZE_INVALID_PARAM";
79 pub const ALIAS_NOT_FOUND: &str = "ALIAS_NOT_FOUND";
81 pub const ALIAS_ALREADY_EXISTS: &str = "ALIAS_ALREADY_EXISTS";
84 pub const ALIAS_PARAMS_REQUIRED: &str = "ALIAS_PARAMS_REQUIRED";
87 pub const ALIAS_TEMPLATE_ERROR: &str = "ALIAS_TEMPLATE_ERROR";
90 pub const AMBIGUOUS_ID: &str = "AMBIGUOUS_ID";
93 pub const AGGREGATOR_ERROR: &str = "AGGREGATOR_ERROR";
99 pub const TYPE_ERROR: &str = "TYPE_ERROR";
102 pub const STRING_NOT_FOUND: &str = "STRING_NOT_FOUND";
105 pub const AMBIGUOUS_MATCH: &str = "AMBIGUOUS_MATCH";
109 pub const OUT_OF_RANGE: &str = "OUT_OF_RANGE";
112}
113
114#[derive(Error, Debug)]
116pub enum MiniAppError {
117 #[error("validation error on field '{field}': {reason}")]
119 Validation { field: String, reason: String },
120
121 #[error("row not found: {id}")]
123 NotFound { id: String },
124
125 #[error("schema parse error: {0}")]
127 Schema(String),
128
129 #[error("storage error: {0}")]
131 Storage(#[from] rusqlite::Error),
132
133 #[error("io error: {0}")]
135 Io(#[from] std::io::Error),
136
137 #[error("config error: {0}")]
139 Config(String),
140
141 #[error("table not found: {table}")]
143 TableNotFound { table: String },
144
145 #[error("table argument is required in multi-table mode")]
147 TableRequired,
148
149 #[error("schema already exists: {table}")]
151 SchemaExists { table: String },
152
153 #[error("backup error: {0}")]
155 Backup(String),
156
157 #[error("snapshot error: {0}")]
159 Snapshot(String),
160
161 #[error("batch aborted at op #{op_index}: {reason}")]
163 BatchAborted { op_index: usize, reason: String },
164
165 #[error("materialize dest must be absolute: {path}")]
167 MaterializeDestRelative { path: String },
168
169 #[error("materialize dest invalid '{path}': {reason}")]
171 MaterializeDestInvalid { path: String, reason: String },
172
173 #[error("materialize io error: {0}")]
175 MaterializeIo(String),
176
177 #[error("materialize sha256 error: {0}")]
179 MaterializeSha256(String),
180
181 #[error("materialize row not found: {id}")]
183 MaterializeRowNotFound { id: String },
184
185 #[error("materialize filter matched zero rows")]
187 MaterializeEmptyResult,
188
189 #[error("materialize format error: {0}")]
191 MaterializeFormatError(String),
192
193 #[error("materialize unknown field: {field}")]
195 MaterializeFieldUnknown { field: String },
196
197 #[error("materialize invalid param '{field}': {reason}")]
199 MaterializeInvalidParam { field: String, reason: String },
200
201 #[error("alias not found: {name}")]
203 AliasNotFound { name: String },
204
205 #[error("alias already exists: {name}")]
207 AliasAlreadyExists { name: String },
208
209 #[error("alias '{name}' requires params but none were provided")]
212 AliasParamsRequired { name: String },
213
214 #[error("alias template render error: {0}")]
216 AliasTemplateError(String),
217
218 #[error("ambiguous id prefix '{id_prefix}': {n} candidates", n = candidates.len())]
220 AmbiguousId {
221 id_prefix: String,
222 candidates: Vec<String>,
223 },
224
225 #[error("aggregator error: {0}")]
228 Aggregator(String),
229
230 #[error("field '{field}' is not a string field (type: {actual_type})")]
232 FieldTypeError { field: String, actual_type: String },
233
234 #[error("old_str not found in field '{field}'")]
236 StringNotFound { field: String },
237
238 #[error("ambiguous match: {matches} occurrences of old_str in field '{field}'")]
241 AmbiguousMatch {
242 field: String,
243 matches: u32,
244 candidates: Vec<MatchCandidate>,
245 },
246
247 #[error("line {line} is out of range (field has {total_lines} lines)")]
249 LineOutOfRange { line: u32, total_lines: u32 },
250}
251
252impl MiniAppError {
253 pub fn code(&self) -> &'static str {
255 match self {
256 MiniAppError::Validation { .. } => codes::VALIDATION_ERROR,
257 MiniAppError::NotFound { .. } => codes::NOT_FOUND,
258 MiniAppError::Schema(_) => codes::SCHEMA_ERROR,
259 MiniAppError::Storage(_) => codes::STORAGE_ERROR,
260 MiniAppError::Io(_) => codes::IO_ERROR,
261 MiniAppError::Config(_) => codes::CONFIG_ERROR,
262 MiniAppError::TableNotFound { .. } => codes::TABLE_NOT_FOUND,
263 MiniAppError::TableRequired => codes::TABLE_REQUIRED,
264 MiniAppError::SchemaExists { .. } => codes::SCHEMA_EXISTS,
265 MiniAppError::Backup(_) => codes::BACKUP_ERROR,
266 MiniAppError::Snapshot(_) => codes::SNAPSHOT_ERROR,
267 MiniAppError::BatchAborted { .. } => codes::BATCH_ABORTED,
268 MiniAppError::MaterializeDestRelative { .. } => codes::MATERIALIZE_DEST_RELATIVE,
269 MiniAppError::MaterializeDestInvalid { .. } => codes::MATERIALIZE_DEST_INVALID,
270 MiniAppError::MaterializeIo(_) => codes::MATERIALIZE_IO_ERROR,
271 MiniAppError::MaterializeSha256(_) => codes::MATERIALIZE_SHA256_ERROR,
272 MiniAppError::MaterializeRowNotFound { .. } => codes::MATERIALIZE_ROW_NOT_FOUND,
273 MiniAppError::MaterializeEmptyResult => codes::MATERIALIZE_EMPTY_RESULT,
274 MiniAppError::MaterializeFormatError(_) => codes::MATERIALIZE_FORMAT_ERROR,
275 MiniAppError::MaterializeFieldUnknown { .. } => codes::MATERIALIZE_FIELD_UNKNOWN,
276 MiniAppError::MaterializeInvalidParam { .. } => codes::MATERIALIZE_INVALID_PARAM,
277 MiniAppError::AliasNotFound { .. } => codes::ALIAS_NOT_FOUND,
278 MiniAppError::AliasAlreadyExists { .. } => codes::ALIAS_ALREADY_EXISTS,
279 MiniAppError::AliasParamsRequired { .. } => codes::ALIAS_PARAMS_REQUIRED,
280 MiniAppError::AliasTemplateError(_) => codes::ALIAS_TEMPLATE_ERROR,
281 MiniAppError::AmbiguousId { .. } => codes::AMBIGUOUS_ID,
282 MiniAppError::Aggregator(_) => codes::AGGREGATOR_ERROR,
283 MiniAppError::FieldTypeError { .. } => codes::TYPE_ERROR,
284 MiniAppError::StringNotFound { .. } => codes::STRING_NOT_FOUND,
285 MiniAppError::AmbiguousMatch { .. } => codes::AMBIGUOUS_MATCH,
286 MiniAppError::LineOutOfRange { .. } => codes::OUT_OF_RANGE,
287 }
288 }
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294
295 #[test]
296 fn error_code_all_variants() {
297 let cases: Vec<(&str, MiniAppError)> = vec![
298 (
299 codes::VALIDATION_ERROR,
300 MiniAppError::Validation {
301 field: "f".into(),
302 reason: "r".into(),
303 },
304 ),
305 (codes::NOT_FOUND, MiniAppError::NotFound { id: "x".into() }),
306 (codes::SCHEMA_ERROR, MiniAppError::Schema("s".into())),
307 (
308 codes::IO_ERROR,
309 MiniAppError::Io(std::io::Error::other("e")),
310 ),
311 (codes::CONFIG_ERROR, MiniAppError::Config("c".into())),
312 (
313 codes::TABLE_NOT_FOUND,
314 MiniAppError::TableNotFound { table: "t".into() },
315 ),
316 (codes::TABLE_REQUIRED, MiniAppError::TableRequired),
317 (
318 codes::SCHEMA_EXISTS,
319 MiniAppError::SchemaExists {
320 table: "my_table".into(),
321 },
322 ),
323 (
324 codes::BACKUP_ERROR,
325 MiniAppError::Backup("disk full".into()),
326 ),
327 (
328 codes::SNAPSHOT_ERROR,
329 MiniAppError::Snapshot("snapshot failed".into()),
330 ),
331 (
332 codes::BATCH_ABORTED,
333 MiniAppError::BatchAborted {
334 op_index: 2,
335 reason: "schema not found".into(),
336 },
337 ),
338 (
339 codes::MATERIALIZE_DEST_RELATIVE,
340 MiniAppError::MaterializeDestRelative {
341 path: "relative/path".into(),
342 },
343 ),
344 (
345 codes::MATERIALIZE_DEST_INVALID,
346 MiniAppError::MaterializeDestInvalid {
347 path: "/bad/path".into(),
348 reason: "parent dir not writable".into(),
349 },
350 ),
351 (
352 codes::MATERIALIZE_IO_ERROR,
353 MiniAppError::MaterializeIo("write failed".into()),
354 ),
355 (
356 codes::MATERIALIZE_SHA256_ERROR,
357 MiniAppError::MaterializeSha256("task panicked".into()),
358 ),
359 (
360 codes::MATERIALIZE_ROW_NOT_FOUND,
361 MiniAppError::MaterializeRowNotFound { id: "row-1".into() },
362 ),
363 (
364 codes::MATERIALIZE_EMPTY_RESULT,
365 MiniAppError::MaterializeEmptyResult,
366 ),
367 (
368 codes::MATERIALIZE_FORMAT_ERROR,
369 MiniAppError::MaterializeFormatError("yaml error".into()),
370 ),
371 (
372 codes::MATERIALIZE_FIELD_UNKNOWN,
373 MiniAppError::MaterializeFieldUnknown {
374 field: "unknown_field".into(),
375 },
376 ),
377 (
378 codes::MATERIALIZE_INVALID_PARAM,
379 MiniAppError::MaterializeInvalidParam {
380 field: "concat".into(),
381 reason: "concat=true requires ByFilter selector".into(),
382 },
383 ),
384 (
385 codes::ALIAS_NOT_FOUND,
386 MiniAppError::AliasNotFound {
387 name: "my_alias".into(),
388 },
389 ),
390 (
391 codes::ALIAS_ALREADY_EXISTS,
392 MiniAppError::AliasAlreadyExists {
393 name: "my_alias".into(),
394 },
395 ),
396 (
397 codes::ALIAS_PARAMS_REQUIRED,
398 MiniAppError::AliasParamsRequired {
399 name: "my_alias".into(),
400 },
401 ),
402 (
403 codes::ALIAS_TEMPLATE_ERROR,
404 MiniAppError::AliasTemplateError("template syntax error".into()),
405 ),
406 (
407 codes::AMBIGUOUS_ID,
408 MiniAppError::AmbiguousId {
409 id_prefix: "abc".into(),
410 candidates: vec!["abc-1".into(), "abc-2".into()],
411 },
412 ),
413 (
414 codes::AGGREGATOR_ERROR,
415 MiniAppError::Aggregator("empty sources".into()),
416 ),
417 (
418 codes::TYPE_ERROR,
419 MiniAppError::FieldTypeError {
420 field: "count".into(),
421 actual_type: "Number".into(),
422 },
423 ),
424 (
425 codes::STRING_NOT_FOUND,
426 MiniAppError::StringNotFound {
427 field: "body".into(),
428 },
429 ),
430 (
431 codes::AMBIGUOUS_MATCH,
432 MiniAppError::AmbiguousMatch {
433 field: "body".into(),
434 matches: 2,
435 candidates: vec![],
436 },
437 ),
438 (
439 codes::OUT_OF_RANGE,
440 MiniAppError::LineOutOfRange {
441 line: 100,
442 total_lines: 50,
443 },
444 ),
445 ];
446 for (expected_code, err) in cases {
447 assert_eq!(
448 err.code(),
449 expected_code,
450 "wrong code for variant containing code {}",
451 expected_code
452 );
453 }
454 }
455
456 #[test]
457 fn backup_error_code_is_not_storage_or_io() {
458 let err = MiniAppError::Backup("some rusqlite error".to_string());
459 assert_eq!(err.code(), codes::BACKUP_ERROR);
460 assert_ne!(err.code(), codes::STORAGE_ERROR);
461 assert_ne!(err.code(), codes::IO_ERROR);
462 }
463}