Skip to main content

krabka_app_sdk/
error.rs

1//! Public SDK error taxonomy.
2
3/// Error taxonomy shared by the app SDK contract.
4#[derive(Debug, thiserror::Error, PartialEq, Eq)]
5#[non_exhaustive]
6pub enum KrabkaError {
7    /// Transport or endpoint reachability failure.
8    #[error("transport: {0}")]
9    Transport(String),
10    /// Authentication failed or credentials were absent.
11    #[error("unauthenticated: {0}")]
12    Unauthenticated(String),
13    /// Caller supplied an invalid argument.
14    #[error("invalid argument: {0}")]
15    InvalidArgument(String),
16    /// Target resource was not found.
17    #[error("not found: {0}")]
18    NotFound(String),
19    /// Server-side failure outside the narrower classes.
20    #[error("server error: {0}")]
21    ServerError(String),
22    /// SDK module is intentionally gated on later work.
23    #[error("{module} is unimplemented; gated on {gated_on}")]
24    Unimplemented {
25        /// SDK module name.
26        module: &'static str,
27        /// Plan or spec slug gating the module.
28        gated_on: &'static str,
29    },
30}
31
32impl KrabkaError {
33    /// Convert a Connect code string into the SDK taxonomy.
34    #[must_use]
35    pub fn from_connect_code(code: &str, message: impl Into<String>) -> Self {
36        let message = message.into();
37        match code {
38            "not_found" => Self::NotFound(message),
39            "invalid_argument" | "failed_precondition" | "out_of_range" => {
40                Self::InvalidArgument(message)
41            }
42            "unauthenticated" => Self::Unauthenticated(message),
43            "unavailable" | "deadline_exceeded" => Self::Transport(message),
44            _ => Self::ServerError(message),
45        }
46    }
47}
48
49impl From<crate::connect_client::ConnectClientError> for KrabkaError {
50    fn from(value: crate::connect_client::ConnectClientError) -> Self {
51        match value {
52            crate::connect_client::ConnectClientError::Connect { code, message } => {
53                Self::from_connect_code(&code, message)
54            }
55            crate::connect_client::ConnectClientError::HttpStatus { status, message } => {
56                match status {
57                    400 => Self::InvalidArgument(message),
58                    401 => Self::Unauthenticated(message),
59                    404 => Self::NotFound(message),
60                    408 | 429 | 502..=504 => Self::Transport(message),
61                    _ => Self::ServerError(message),
62                }
63            }
64            other => Self::Transport(other.to_string()),
65        }
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72
73    #[test]
74    fn connect_codes_map_to_taxonomy() {
75        assert!(matches!(
76            KrabkaError::from_connect_code("not_found", "x"),
77            KrabkaError::NotFound(_)
78        ));
79        assert!(matches!(
80            KrabkaError::from_connect_code("unavailable", "x"),
81            KrabkaError::Transport(_)
82        ));
83        assert!(matches!(
84            KrabkaError::from_connect_code("invalid_argument", "x"),
85            KrabkaError::InvalidArgument(_)
86        ));
87    }
88
89    #[test]
90    fn http_statuses_map_to_taxonomy_when_connect_body_is_unusable() {
91        let invalid = KrabkaError::from(crate::connect_client::ConnectClientError::HttpStatus {
92            status: 400,
93            message: "bad request".into(),
94        });
95        let unavailable =
96            KrabkaError::from(crate::connect_client::ConnectClientError::HttpStatus {
97                status: 503,
98                message: "down".into(),
99            });
100
101        assert2::assert!(let KrabkaError::InvalidArgument(message) = invalid);
102        assert_eq!(message, "bad request");
103        assert2::assert!(let KrabkaError::Transport(message) = unavailable);
104        assert_eq!(message, "down");
105    }
106}