openapi_contract/
error.rs1use 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 let e = ApiError::from(make_reqwest_error());
104 assert!(matches!(e, ApiError::Http(_)));
105 assert!(e.to_string().starts_with("HTTP error:"));
106
107 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 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 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 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 assert!(
143 ApiError::Api {
144 status: 500,
145 message: "oops".into()
146 }
147 .source()
148 .is_none()
149 );
150 assert!(
151 ApiError::Defined {
152 status: 403,
153 code: "F".into(),
154 message: "f".into()
155 }
156 .source()
157 .is_none()
158 );
159 }
160
161 #[test]
162 fn code_status_is_code() {
163 let defined = ApiError::Defined {
164 status: 404,
165 code: "TEAM_NOT_FOUND".into(),
166 message: "not found".into(),
167 };
168 assert_eq!(defined.code(), Some("TEAM_NOT_FOUND"));
169 assert_eq!(defined.status(), Some(404));
170 assert!(defined.is_code("TEAM_NOT_FOUND"));
171 assert!(!defined.is_code("OTHER"));
172
173 let api = ApiError::Api {
174 status: 500,
175 message: "oops".into(),
176 };
177 assert_eq!(api.code(), None);
178 assert_eq!(api.status(), Some(500));
179 assert!(!api.is_code("ANYTHING"));
180
181 let http = ApiError::Http(make_reqwest_error());
182 assert_eq!(http.code(), None);
183 assert_eq!(http.status(), None);
184 }
185}