Skip to main content

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("project not found")]
26    ProjectNotFound,
27
28    /// `{:error, :not_found}`.
29    #[error("not found")]
30    NotFound,
31
32    /// The writer task is gone (channel closed). Should not happen in normal
33    /// operation; surfaced so callers can fail loudly rather than hang.
34    #[error("store writer task unavailable")]
35    WriterGone,
36
37    /// The `schema_migrations` ledger holds a version this build has never
38    /// heard of and that is *older* than its own — the database was not
39    /// written by any cliban we recognize. (A *newer* version is fine; see
40    /// [`crate::migrations::run`].)
41    #[error(
42        "unrecognized database schema version(s) {found:?} (this build expects {expected}); \
43         the database at this path was not written by a known cliban"
44    )]
45    SchemaUnknown { found: Vec<i64>, expected: i64 },
46
47    #[error(transparent)]
48    Sqlite(#[from] rusqlite::Error),
49
50    #[error(transparent)]
51    Json(#[from] serde_json::Error),
52}
53
54impl Error {
55    /// Construct a single-field validation error, mirroring
56    /// `Ecto.Changeset.add_error(cs, field, msg)`.
57    pub fn validation(field: &str, message: &str) -> Self {
58        Error::Validation(vec![(field.to_string(), message.to_string())])
59    }
60
61    /// True if this is a validation error mentioning `field` with a message
62    /// that contains `needle`. Test convenience that mirrors the Elixir tests'
63    /// `cs.errors[:field] =~ needle` assertions.
64    pub fn has_validation(&self, field: &str, needle: &str) -> bool {
65        match self {
66            Error::Validation(errs) => errs.iter().any(|(f, m)| f == field && m.contains(needle)),
67            _ => false,
68        }
69    }
70}
71
72pub type Result<T> = std::result::Result<T, Error>;