Skip to main content

openapi_contract/
error.rs

1use std::fmt;
2
3#[derive(Debug)]
4pub enum ApiError {
5    Http(reqwest::Error),
6    Serialization(serde_json::Error),
7    Api {
8        status: u16,
9        message: String,
10    },
11    Defined {
12        status: u16,
13        code: String,
14        message: String,
15    },
16}
17
18#[derive(serde::Deserialize)]
19pub(crate) struct DefinedErrorBody {
20    #[serde(default)]
21    pub defined: bool,
22    #[serde(default)]
23    pub code: String,
24    #[serde(default)]
25    pub message: String,
26}
27
28impl ApiError {
29    pub fn code(&self) -> Option<&str> {
30        match self {
31            Self::Defined { code, .. } => Some(code),
32            _ => None,
33        }
34    }
35
36    pub fn status(&self) -> Option<u16> {
37        match self {
38            Self::Api { status, .. } | Self::Defined { status, .. } => Some(*status),
39            _ => None,
40        }
41    }
42
43    pub fn is_code(&self, expected: &str) -> bool {
44        self.code() == Some(expected)
45    }
46}
47
48impl fmt::Display for ApiError {
49    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50        match self {
51            Self::Http(e) => write!(f, "HTTP error: {e}"),
52            Self::Serialization(e) => write!(f, "serialization error: {e}"),
53            Self::Api { status, message } => write!(f, "API error {status}: {message}"),
54            Self::Defined {
55                status,
56                code,
57                message,
58            } => {
59                write!(f, "API error {status} [{code}]: {message}")
60            }
61        }
62    }
63}
64
65impl std::error::Error for ApiError {
66    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
67        match self {
68            Self::Http(e) => Some(e),
69            Self::Serialization(e) => Some(e),
70            Self::Api { .. } | Self::Defined { .. } => None,
71        }
72    }
73}
74
75impl From<reqwest::Error> for ApiError {
76    fn from(e: reqwest::Error) -> Self {
77        Self::Http(e)
78    }
79}
80
81impl From<serde_json::Error> for ApiError {
82    fn from(e: serde_json::Error) -> Self {
83        Self::Serialization(e)
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90    use std::error::Error;
91
92    fn make_reqwest_error() -> reqwest::Error {
93        reqwest::Client::new()
94            .get("http://localhost:1/x")
95            .header("bad\0header", "v")
96            .build()
97            .unwrap_err()
98    }
99
100    #[test]
101    fn display_and_from() {
102        // Http via From + Display
103        let e = ApiError::from(make_reqwest_error());
104        assert!(matches!(e, ApiError::Http(_)));
105        assert!(e.to_string().starts_with("HTTP error:"));
106
107        // Serialization via From + Display
108        let e = ApiError::from(serde_json::from_str::<i32>("x").unwrap_err());
109        assert!(matches!(e, ApiError::Serialization(_)));
110        assert!(e.to_string().starts_with("serialization error:"));
111
112        // Api Display
113        let e = ApiError::Api {
114            status: 404,
115            message: "not found".into(),
116        };
117        assert_eq!(e.to_string(), "API error 404: not found");
118
119        // Defined Display
120        let e = ApiError::Defined {
121            status: 404,
122            code: "TEAM_NOT_FOUND".into(),
123            message: "Team not found".into(),
124        };
125        assert_eq!(
126            e.to_string(),
127            "API error 404 [TEAM_NOT_FOUND]: Team not found"
128        );
129    }
130
131    #[test]
132    fn source_delegation() {
133        // Http and Serialization have sources
134        assert!(ApiError::Http(make_reqwest_error()).source().is_some());
135        assert!(
136            ApiError::from(serde_json::from_str::<i32>("x").unwrap_err())
137                .source()
138                .is_some()
139        );
140
141        // Api and Defined do not
142        assert!(ApiError::Api {
143            status: 500,
144            message: "oops".into()
145        }
146        .source()
147        .is_none());
148        assert!(ApiError::Defined {
149            status: 403,
150            code: "F".into(),
151            message: "f".into()
152        }
153        .source()
154        .is_none());
155    }
156
157    #[test]
158    fn code_status_is_code() {
159        let defined = ApiError::Defined {
160            status: 404,
161            code: "TEAM_NOT_FOUND".into(),
162            message: "not found".into(),
163        };
164        assert_eq!(defined.code(), Some("TEAM_NOT_FOUND"));
165        assert_eq!(defined.status(), Some(404));
166        assert!(defined.is_code("TEAM_NOT_FOUND"));
167        assert!(!defined.is_code("OTHER"));
168
169        let api = ApiError::Api {
170            status: 500,
171            message: "oops".into(),
172        };
173        assert_eq!(api.code(), None);
174        assert_eq!(api.status(), Some(500));
175        assert!(!api.is_code("ANYTHING"));
176
177        let http = ApiError::Http(make_reqwest_error());
178        assert_eq!(http.code(), None);
179        assert_eq!(http.status(), None);
180    }
181}