Skip to main content

omni_dev/jev/
error.rs

1//! Error types for TypeSafe Jev operations.
2
3use thiserror::Error;
4
5/// Whether `err` is Jev rejecting the credentials (HTTP 401/403).
6///
7/// No later call in the same run can get past either — shared by `route`
8/// and `verify-decision` so both end a batch run at the first auth failure
9/// instead of paying for every remaining call.
10#[must_use]
11pub fn is_auth_failure(err: &anyhow::Error) -> bool {
12    matches!(
13        err.downcast_ref::<JevError>(),
14        Some(JevError::ApiRequestFailed {
15            status: 401 | 403,
16            ..
17        })
18    )
19}
20
21/// Errors that can occur while calling TypeSafe's Jev System One API.
22#[derive(Error, Debug)]
23pub enum JevError {
24    /// No Jev API key was found in the environment or `settings.json`.
25    #[error(
26        "Jev credentials not configured. Set TYPESAFE_API_KEY (or OMNI_DEV_JEV_API_KEY, or \
27         either's _FILE companion), or add one to ~/.omni-dev/settings.json"
28    )]
29    CredentialsNotFound,
30
31    /// A Jev API request failed.
32    #[error("Jev API request failed: HTTP {status}: {body}")]
33    ApiRequestFailed {
34        /// HTTP status code.
35        status: u16,
36        /// Response body text.
37        body: String,
38    },
39
40    /// A question specification could not be built into a valid request.
41    #[error("Invalid question specification: {0}")]
42    InvalidQuestionSpec(String),
43}
44
45#[cfg(test)]
46mod tests {
47    use super::*;
48
49    #[test]
50    fn is_auth_failure_matches_401_and_403() {
51        for status in [401, 403] {
52            let err = anyhow::Error::new(JevError::ApiRequestFailed {
53                status,
54                body: "nope".to_string(),
55            });
56            assert!(is_auth_failure(&err), "status {status}");
57        }
58    }
59
60    #[test]
61    fn is_auth_failure_rejects_other_statuses_and_errors() {
62        let err = anyhow::Error::new(JevError::ApiRequestFailed {
63            status: 500,
64            body: "boom".to_string(),
65        });
66        assert!(!is_auth_failure(&err));
67        assert!(!is_auth_failure(&anyhow::anyhow!("unrelated")));
68    }
69
70    #[test]
71    fn credentials_not_found_display() {
72        let err = JevError::CredentialsNotFound;
73        let msg = err.to_string();
74        assert!(msg.contains("TYPESAFE_API_KEY"));
75        assert!(msg.contains("OMNI_DEV_JEV_API_KEY"));
76        assert!(msg.contains("settings.json"));
77    }
78
79    #[test]
80    fn api_request_failed_display() {
81        let err = JevError::ApiRequestFailed {
82            status: 401,
83            body: "Unauthorized".to_string(),
84        };
85        let msg = err.to_string();
86        assert!(msg.contains("401"));
87        assert!(msg.contains("Unauthorized"));
88    }
89
90    #[test]
91    fn invalid_question_spec_display() {
92        let err = JevError::InvalidQuestionSpec("missing name".to_string());
93        assert!(err.to_string().contains("missing name"));
94    }
95}