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!(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}