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