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>;