cliban_core/error.rs
1//! Error type for the store + contexts.
2//!
3//! Mirrors the Elixir contexts' error vocabulary: changeset-style validation
4//! failures become [`Error::Validation`] (a list of `(field, message)`
5//! pairs, matching `Ecto.Changeset.errors`), domain rejections become typed
6//! variants (e.g. [`Error::ProjectNotFound`], [`Error::NotFound`]), and
7//! anything from rusqlite/serde bubbles up untyped.
8
9use thiserror::Error;
10
11/// A single changeset-style validation error: the field name and a message.
12/// Mirrors one entry of `Ecto.Changeset.errors`.
13pub type FieldError = (String, String);
14
15#[derive(Debug, Error)]
16pub enum Error {
17 /// One or more changeset-style validation failures. The Elixir side
18 /// returns `{:error, %Ecto.Changeset{errors: [...]}}`; we carry the
19 /// equivalent `(field, message)` pairs so callers (and tests) can assert
20 /// on the same field/message contract.
21 #[error("validation failed: {0:?}")]
22 Validation(Vec<FieldError>),
23
24 /// `{:error, :project_not_found}` / `Repo.rollback(:project_not_found)`.
25 #[error("not found: {0}")]
26 ProjectNotFound(String),
27
28 /// A lookup that can name the missing identity.
29 #[error("not found: {0}")]
30 NamedNotFound(String),
31
32 /// `{:error, :not_found}`.
33 #[error("not found")]
34 NotFound,
35
36 /// The writer task is gone (channel closed). Should not happen in normal
37 /// operation; surfaced so callers can fail loudly rather than hang.
38 #[error("store writer task unavailable")]
39 WriterGone,
40
41 /// The `schema_migrations` ledger holds a version this build has never
42 /// heard of and that is *older* than its own — the database was not
43 /// written by any cliban we recognize. (A *newer* version is fine; see
44 /// [`crate::migrations::run`].)
45 #[error(
46 "unrecognized database schema version(s) {found:?} (this build expects {expected}); \
47 the database at this path was not written by a known cliban"
48 )]
49 SchemaUnknown { found: Vec<i64>, expected: i64 },
50
51 #[error(transparent)]
52 Sqlite(#[from] rusqlite::Error),
53
54 #[error(transparent)]
55 Json(#[from] serde_json::Error),
56}
57
58impl Error {
59 /// Construct a single-field validation error, mirroring
60 /// `Ecto.Changeset.add_error(cs, field, msg)`.
61 pub fn validation(field: &str, message: &str) -> Self {
62 Error::Validation(vec![(field.to_string(), message.to_string())])
63 }
64
65 /// True if this is a validation error mentioning `field` with a message
66 /// that contains `needle`. Test convenience that mirrors the Elixir tests'
67 /// `cs.errors[:field] =~ needle` assertions.
68 pub fn has_validation(&self, field: &str, needle: &str) -> bool {
69 match self {
70 Error::Validation(errs) => errs.iter().any(|(f, m)| f == field && m.contains(needle)),
71 _ => false,
72 }
73 }
74}
75
76pub type Result<T> = std::result::Result<T, Error>;