Skip to main content

videre_api/
error.rs

1/// Errors returned by videre-api operations. Each consumer maps these to its
2/// own convention (axum -> StatusCode, other embedders -> their own error type).
3#[derive(Debug)]
4pub enum Error {
5    /// The target row/label does not exist.
6    NotFound,
7    /// Caller-supplied input was rejected (e.g. an empty label after sanitizing).
8    Invalid,
9    /// Caller-supplied input was rejected for a stated reason, which the
10    /// gallery logs and shows (e.g. a face list that is not the cluster's).
11    Rejected(String),
12    /// The thing being mutated changed underneath the caller (stale question,
13    /// moved face, different active profile). Retry with fresh state.
14    Conflict,
15    /// Underlying database failure.
16    Db(rusqlite::Error),
17    /// The library is momentarily unavailable: its root no longer names the
18    /// library the server bound to, or exclusive maintenance holds it. Distinct
19    /// from a permanent failure so the caller can map it to a retryable status
20    /// (503) rather than a 500.
21    Unavailable(String),
22    /// Any other failure surfaced as a plain message (e.g. from videre-core
23    /// functions that return anyhow::Error, like pipeline_runs).
24    Other(String),
25}
26
27impl From<rusqlite::Error> for Error {
28    fn from(e: rusqlite::Error) -> Self {
29        Error::Db(e)
30    }
31}
32
33impl From<anyhow::Error> for Error {
34    fn from(e: anyhow::Error) -> Self {
35        Error::Other(e.to_string())
36    }
37}
38
39impl From<videre_core::face_learning::FeatureError> for Error {
40    fn from(e: videre_core::face_learning::FeatureError) -> Self {
41        Error::Other(e.to_string())
42    }
43}
44
45impl From<videre_core::face_learning::LearningEventError> for Error {
46    fn from(e: videre_core::face_learning::LearningEventError) -> Self {
47        Error::Other(e.to_string())
48    }
49}
50
51impl From<serde_json::Error> for Error {
52    fn from(e: serde_json::Error) -> Self {
53        Error::Other(e.to_string())
54    }
55}
56
57impl From<videre_core::face_learning::ProfileError> for Error {
58    fn from(e: videre_core::face_learning::ProfileError) -> Self {
59        Error::Other(e.to_string())
60    }
61}
62
63impl From<videre_core::face_learning::TrainingError> for Error {
64    fn from(e: videre_core::face_learning::TrainingError) -> Self {
65        Error::Other(e.to_string())
66    }
67}
68
69impl From<videre_core::face_learning::QuestionError> for Error {
70    fn from(e: videre_core::face_learning::QuestionError) -> Self {
71        Error::Other(e.to_string())
72    }
73}
74
75impl std::fmt::Display for Error {
76    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77        match self {
78            Error::NotFound => write!(f, "not found"),
79            Error::Invalid => write!(f, "invalid input"),
80            Error::Rejected(msg) => write!(f, "{msg}"),
81            Error::Conflict => write!(f, "stale state, refetch and retry"),
82            Error::Db(e) => write!(f, "database error: {e}"),
83            Error::Unavailable(msg) => write!(f, "library unavailable: {msg}"),
84            Error::Other(msg) => write!(f, "{msg}"),
85        }
86    }
87}
88
89impl std::error::Error for Error {}
90
91pub type Result<T> = std::result::Result<T, Error>;
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    #[test]
98    fn display_matches_each_variant() {
99        assert_eq!(Error::NotFound.to_string(), "not found");
100        assert_eq!(Error::Invalid.to_string(), "invalid input");
101        assert_eq!(Error::Rejected("why".into()).to_string(), "why");
102        assert_eq!(
103            Error::Unavailable("root gone".to_string()).to_string(),
104            "library unavailable: root gone"
105        );
106        assert_eq!(Error::Other("boom".to_string()).to_string(), "boom");
107    }
108
109    #[test]
110    fn conflict_variant_has_a_display_message() {
111        assert_eq!(
112            Error::Conflict.to_string(),
113            "stale state, refetch and retry"
114        );
115    }
116
117    #[test]
118    fn display_for_db_variant_includes_the_underlying_error() {
119        let e = Error::Db(rusqlite::Error::QueryReturnedNoRows);
120        assert!(e.to_string().starts_with("database error: "));
121        assert!(e.to_string().contains("Query returned no rows"));
122    }
123
124    #[test]
125    fn from_rusqlite_error_wraps_as_db_variant() {
126        let e: Error = rusqlite::Error::QueryReturnedNoRows.into();
127        assert!(matches!(e, Error::Db(_)));
128    }
129
130    #[test]
131    fn from_anyhow_error_wraps_message_as_other_variant() {
132        let e: Error = anyhow::anyhow!("something broke").into();
133        match e {
134            Error::Other(msg) => assert_eq!(msg, "something broke"),
135            other => panic!("expected Error::Other, got {other:?}"),
136        }
137    }
138}