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 pub const UPLOAD_NOT_CONFIGURED: &str = "UPLOAD_NOT_CONFIGURED";
116 pub const UPLOAD_FAILED: &str = "UPLOAD_FAILED";
119}
120
121#[derive(Error, Debug)]
123pub enum MiniAppError {
124 #[error("validation error on field '{field}': {reason}")]
126 Validation { field: String, reason: String },
127
128 #[error("row not found: {id}")]
130 NotFound { id: String },
131
132 #[error("schema parse error: {0}")]
134 Schema(String),
135
136 #[error("storage error: {0}")]
138 Storage(#[from] rusqlite::Error),
139
140 #[error("io error: {0}")]
142 Io(#[from] std::io::Error),
143
144 #[error("config error: {0}")]
146 Config(String),
147
148 #[error("table not found: {table}")]
150 TableNotFound { table: String },
151
152 #[error("table argument is required in multi-table mode")]
154 TableRequired,
155
156 #[error("schema already exists: {table}")]
158 SchemaExists { table: String },
159
160 #[error("backup error: {0}")]
162 Backup(String),
163
164 #[error("snapshot error: {0}")]
166 Snapshot(String),
167
168 #[error("batch aborted at op #{op_index}: {reason}")]
170 BatchAborted { op_index: usize, reason: String },
171
172 #[error("materialize dest must be absolute: {path}")]
174 MaterializeDestRelative { path: String },
175
176 #[error("materialize dest invalid '{path}': {reason}")]
178 MaterializeDestInvalid { path: String, reason: String },
179
180 #[error("materialize io error: {0}")]
182 MaterializeIo(String),
183
184 #[error("materialize sha256 error: {0}")]
186 MaterializeSha256(String),
187
188 #[error("materialize row not found: {id}")]
190 MaterializeRowNotFound { id: String },
191
192 #[error("materialize filter matched zero rows")]
194 MaterializeEmptyResult,
195
196 #[error("materialize format error: {0}")]
198 MaterializeFormatError(String),
199
200 #[error("materialize unknown field: {field}")]
202 MaterializeFieldUnknown { field: String },
203
204 #[error("materialize invalid param '{field}': {reason}")]
206 MaterializeInvalidParam { field: String, reason: String },
207
208 #[error("alias not found: {name}")]
210 AliasNotFound { name: String },
211
212 #[error("alias already exists: {name}")]
214 AliasAlreadyExists { name: String },
215
216 #[error("alias '{name}' requires params but none were provided")]
219 AliasParamsRequired { name: String },
220
221 #[error("alias template render error: {0}")]
223 AliasTemplateError(String),
224
225 #[error("ambiguous id prefix '{id_prefix}': {n} candidates", n = candidates.len())]
227 AmbiguousId {
228 id_prefix: String,
229 candidates: Vec<String>,
230 },
231
232 #[error("aggregator error: {0}")]
235 Aggregator(String),
236
237 #[error("field '{field}' is not a string field (type: {actual_type})")]
239 FieldTypeError { field: String, actual_type: String },
240
241 #[error("old_str not found in field '{field}'")]
243 StringNotFound { field: String },
244
245 #[error("ambiguous match: {matches} occurrences of old_str in field '{field}'")]
248 AmbiguousMatch {
249 field: String,
250 matches: u32,
251 candidates: Vec<MatchCandidate>,
252 },
253
254 #[error("line {line} is out of range (field has {total_lines} lines)")]
256 LineOutOfRange { line: u32, total_lines: u32 },
257
258 #[error("upload not configured: {0}")]
261 UploadNotConfigured(String),
262
263 #[error("upload failed: {0}")]
265 Upload(String),
266}
267
268impl MiniAppError {
269 pub fn code(&self) -> &'static str {
271 match self {
272 MiniAppError::Validation { .. } => codes::VALIDATION_ERROR,
273 MiniAppError::NotFound { .. } => codes::NOT_FOUND,
274 MiniAppError::Schema(_) => codes::SCHEMA_ERROR,
275 MiniAppError::Storage(_) => codes::STORAGE_ERROR,
276 MiniAppError::Io(_) => codes::IO_ERROR,
277 MiniAppError::Config(_) => codes::CONFIG_ERROR,
278 MiniAppError::TableNotFound { .. } => codes::TABLE_NOT_FOUND,
279 MiniAppError::TableRequired => codes::TABLE_REQUIRED,
280 MiniAppError::SchemaExists { .. } => codes::SCHEMA_EXISTS,
281 MiniAppError::Backup(_) => codes::BACKUP_ERROR,
282 MiniAppError::Snapshot(_) => codes::SNAPSHOT_ERROR,
283 MiniAppError::BatchAborted { .. } => codes::BATCH_ABORTED,
284 MiniAppError::MaterializeDestRelative { .. } => codes::MATERIALIZE_DEST_RELATIVE,
285 MiniAppError::MaterializeDestInvalid { .. } => codes::MATERIALIZE_DEST_INVALID,
286 MiniAppError::MaterializeIo(_) => codes::MATERIALIZE_IO_ERROR,
287 MiniAppError::MaterializeSha256(_) => codes::MATERIALIZE_SHA256_ERROR,
288 MiniAppError::MaterializeRowNotFound { .. } => codes::MATERIALIZE_ROW_NOT_FOUND,
289 MiniAppError::MaterializeEmptyResult => codes::MATERIALIZE_EMPTY_RESULT,
290 MiniAppError::MaterializeFormatError(_) => codes::MATERIALIZE_FORMAT_ERROR,
291 MiniAppError::MaterializeFieldUnknown { .. } => codes::MATERIALIZE_FIELD_UNKNOWN,
292 MiniAppError::MaterializeInvalidParam { .. } => codes::MATERIALIZE_INVALID_PARAM,
293 MiniAppError::AliasNotFound { .. } => codes::ALIAS_NOT_FOUND,
294 MiniAppError::AliasAlreadyExists { .. } => codes::ALIAS_ALREADY_EXISTS,
295 MiniAppError::AliasParamsRequired { .. } => codes::ALIAS_PARAMS_REQUIRED,
296 MiniAppError::AliasTemplateError(_) => codes::ALIAS_TEMPLATE_ERROR,
297 MiniAppError::AmbiguousId { .. } => codes::AMBIGUOUS_ID,
298 MiniAppError::Aggregator(_) => codes::AGGREGATOR_ERROR,
299 MiniAppError::FieldTypeError { .. } => codes::TYPE_ERROR,
300 MiniAppError::StringNotFound { .. } => codes::STRING_NOT_FOUND,
301 MiniAppError::AmbiguousMatch { .. } => codes::AMBIGUOUS_MATCH,
302 MiniAppError::LineOutOfRange { .. } => codes::OUT_OF_RANGE,
303 MiniAppError::UploadNotConfigured(_) => codes::UPLOAD_NOT_CONFIGURED,
304 MiniAppError::Upload(_) => codes::UPLOAD_FAILED,
305 }
306 }
307}
308
309#[cfg(test)]
310mod tests {
311 use super::*;
312
313 #[test]
314 fn error_code_all_variants() {
315 let cases: Vec<(&str, MiniAppError)> = vec![
316 (
317 codes::VALIDATION_ERROR,
318 MiniAppError::Validation {
319 field: "f".into(),
320 reason: "r".into(),
321 },
322 ),
323 (codes::NOT_FOUND, MiniAppError::NotFound { id: "x".into() }),
324 (codes::SCHEMA_ERROR, MiniAppError::Schema("s".into())),
325 (
326 codes::IO_ERROR,
327 MiniAppError::Io(std::io::Error::other("e")),
328 ),
329 (codes::CONFIG_ERROR, MiniAppError::Config("c".into())),
330 (
331 codes::TABLE_NOT_FOUND,
332 MiniAppError::TableNotFound { table: "t".into() },
333 ),
334 (codes::TABLE_REQUIRED, MiniAppError::TableRequired),
335 (
336 codes::SCHEMA_EXISTS,
337 MiniAppError::SchemaExists {
338 table: "my_table".into(),
339 },
340 ),
341 (
342 codes::BACKUP_ERROR,
343 MiniAppError::Backup("disk full".into()),
344 ),
345 (
346 codes::SNAPSHOT_ERROR,
347 MiniAppError::Snapshot("snapshot failed".into()),
348 ),
349 (
350 codes::BATCH_ABORTED,
351 MiniAppError::BatchAborted {
352 op_index: 2,
353 reason: "schema not found".into(),
354 },
355 ),
356 (
357 codes::MATERIALIZE_DEST_RELATIVE,
358 MiniAppError::MaterializeDestRelative {
359 path: "relative/path".into(),
360 },
361 ),
362 (
363 codes::MATERIALIZE_DEST_INVALID,
364 MiniAppError::MaterializeDestInvalid {
365 path: "/bad/path".into(),
366 reason: "parent dir not writable".into(),
367 },
368 ),
369 (
370 codes::MATERIALIZE_IO_ERROR,
371 MiniAppError::MaterializeIo("write failed".into()),
372 ),
373 (
374 codes::MATERIALIZE_SHA256_ERROR,
375 MiniAppError::MaterializeSha256("task panicked".into()),
376 ),
377 (
378 codes::MATERIALIZE_ROW_NOT_FOUND,
379 MiniAppError::MaterializeRowNotFound { id: "row-1".into() },
380 ),
381 (
382 codes::MATERIALIZE_EMPTY_RESULT,
383 MiniAppError::MaterializeEmptyResult,
384 ),
385 (
386 codes::MATERIALIZE_FORMAT_ERROR,
387 MiniAppError::MaterializeFormatError("yaml error".into()),
388 ),
389 (
390 codes::MATERIALIZE_FIELD_UNKNOWN,
391 MiniAppError::MaterializeFieldUnknown {
392 field: "unknown_field".into(),
393 },
394 ),
395 (
396 codes::MATERIALIZE_INVALID_PARAM,
397 MiniAppError::MaterializeInvalidParam {
398 field: "concat".into(),
399 reason: "concat=true requires ByFilter selector".into(),
400 },
401 ),
402 (
403 codes::ALIAS_NOT_FOUND,
404 MiniAppError::AliasNotFound {
405 name: "my_alias".into(),
406 },
407 ),
408 (
409 codes::ALIAS_ALREADY_EXISTS,
410 MiniAppError::AliasAlreadyExists {
411 name: "my_alias".into(),
412 },
413 ),
414 (
415 codes::ALIAS_PARAMS_REQUIRED,
416 MiniAppError::AliasParamsRequired {
417 name: "my_alias".into(),
418 },
419 ),
420 (
421 codes::ALIAS_TEMPLATE_ERROR,
422 MiniAppError::AliasTemplateError("template syntax error".into()),
423 ),
424 (
425 codes::AMBIGUOUS_ID,
426 MiniAppError::AmbiguousId {
427 id_prefix: "abc".into(),
428 candidates: vec!["abc-1".into(), "abc-2".into()],
429 },
430 ),
431 (
432 codes::AGGREGATOR_ERROR,
433 MiniAppError::Aggregator("empty sources".into()),
434 ),
435 (
436 codes::TYPE_ERROR,
437 MiniAppError::FieldTypeError {
438 field: "count".into(),
439 actual_type: "Number".into(),
440 },
441 ),
442 (
443 codes::STRING_NOT_FOUND,
444 MiniAppError::StringNotFound {
445 field: "body".into(),
446 },
447 ),
448 (
449 codes::AMBIGUOUS_MATCH,
450 MiniAppError::AmbiguousMatch {
451 field: "body".into(),
452 matches: 2,
453 candidates: vec![],
454 },
455 ),
456 (
457 codes::OUT_OF_RANGE,
458 MiniAppError::LineOutOfRange {
459 line: 100,
460 total_lines: 50,
461 },
462 ),
463 (
464 codes::UPLOAD_NOT_CONFIGURED,
465 MiniAppError::UploadNotConfigured("missing MINI_APP_S3_BUCKET".into()),
466 ),
467 (
468 codes::UPLOAD_FAILED,
469 MiniAppError::Upload("put rejected".into()),
470 ),
471 ];
472 for (expected_code, err) in cases {
473 assert_eq!(
474 err.code(),
475 expected_code,
476 "wrong code for variant containing code {}",
477 expected_code
478 );
479 }
480 }
481
482 #[test]
483 fn backup_error_code_is_not_storage_or_io() {
484 let err = MiniAppError::Backup("some rusqlite error".to_string());
485 assert_eq!(err.code(), codes::BACKUP_ERROR);
486 assert_ne!(err.code(), codes::STORAGE_ERROR);
487 assert_ne!(err.code(), codes::IO_ERROR);
488 }
489}