1use std::fmt;
4
5pub type Result<T, E = Error> = std::result::Result<T, E>;
7
8#[derive(Debug, thiserror::Error)]
31#[non_exhaustive]
32pub enum Error {
33 #[error(transparent)]
36 Api(#[from] ApiError),
37
38 #[error("ipregistry: transport error: {0}")]
42 Transport(#[from] reqwest::Error),
43
44 #[error("ipregistry: failed to decode response: {0}")]
46 Decode(#[from] serde_json::Error),
47
48 #[error("ipregistry: invalid configuration: {0}")]
51 Config(String),
52}
53
54impl Error {
55 pub fn as_api(&self) -> Option<&ApiError> {
57 match self {
58 Error::Api(err) => Some(err),
59 _ => None,
60 }
61 }
62
63 pub fn code(&self) -> Option<&ErrorCode> {
66 self.as_api().and_then(|err| err.code.as_ref())
67 }
68}
69
70#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
77#[non_exhaustive]
78pub struct ApiError {
79 pub code: Option<ErrorCode>,
83 pub message: String,
85 pub resolution: String,
87 pub status: Option<u16>,
90}
91
92impl fmt::Display for ApiError {
93 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94 f.write_str("ipregistry: ")?;
95 if self.message.is_empty() {
96 f.write_str("API error")?;
97 } else {
98 f.write_str(&self.message)?;
99 }
100 if let Some(code) = &self.code {
101 write!(f, " ({code})")?;
102 }
103 if !self.resolution.is_empty() {
104 write!(f, ": {}", self.resolution)?;
105 }
106 Ok(())
107 }
108}
109
110#[derive(Debug, Clone, PartialEq, Eq, Hash)]
117#[non_exhaustive]
118pub enum ErrorCode {
119 BadRequest,
121 DisabledApiKey,
123 ForbiddenIp,
125 ForbiddenOrigin,
127 ForbiddenIpOrigin,
129 Internal,
131 InsufficientCredits,
133 InvalidApiKey,
135 InvalidAsn,
137 InvalidFilterSyntax,
139 InvalidIpAddress,
141 MissingApiKey,
143 ReservedAsn,
145 ReservedIpAddress,
147 TooManyAsns,
149 TooManyIps,
151 TooManyRequests,
153 TooManyUserAgents,
155 UnknownAsn,
157 Other(String),
160}
161
162impl ErrorCode {
163 pub fn from_raw(raw: &str) -> Self {
166 match raw.trim().to_ascii_uppercase().as_str() {
167 "BAD_REQUEST" => Self::BadRequest,
168 "DISABLED_API_KEY" => Self::DisabledApiKey,
169 "FORBIDDEN_IP" => Self::ForbiddenIp,
170 "FORBIDDEN_ORIGIN" => Self::ForbiddenOrigin,
171 "FORBIDDEN_IP_ORIGIN" => Self::ForbiddenIpOrigin,
172 "INTERNAL" => Self::Internal,
173 "INSUFFICIENT_CREDITS" => Self::InsufficientCredits,
174 "INVALID_API_KEY" => Self::InvalidApiKey,
175 "INVALID_ASN" => Self::InvalidAsn,
176 "INVALID_FILTER_SYNTAX" => Self::InvalidFilterSyntax,
177 "INVALID_IP_ADDRESS" => Self::InvalidIpAddress,
178 "MISSING_API_KEY" => Self::MissingApiKey,
179 "RESERVED_ASN" => Self::ReservedAsn,
180 "RESERVED_IP_ADDRESS" => Self::ReservedIpAddress,
181 "TOO_MANY_ASNS" => Self::TooManyAsns,
182 "TOO_MANY_IPS" => Self::TooManyIps,
183 "TOO_MANY_REQUESTS" => Self::TooManyRequests,
184 "TOO_MANY_USER_AGENTS" => Self::TooManyUserAgents,
185 "UNKNOWN_ASN" => Self::UnknownAsn,
186 _ => Self::Other(raw.to_owned()),
187 }
188 }
189
190 pub fn as_str(&self) -> &str {
192 match self {
193 Self::BadRequest => "BAD_REQUEST",
194 Self::DisabledApiKey => "DISABLED_API_KEY",
195 Self::ForbiddenIp => "FORBIDDEN_IP",
196 Self::ForbiddenOrigin => "FORBIDDEN_ORIGIN",
197 Self::ForbiddenIpOrigin => "FORBIDDEN_IP_ORIGIN",
198 Self::Internal => "INTERNAL",
199 Self::InsufficientCredits => "INSUFFICIENT_CREDITS",
200 Self::InvalidApiKey => "INVALID_API_KEY",
201 Self::InvalidAsn => "INVALID_ASN",
202 Self::InvalidFilterSyntax => "INVALID_FILTER_SYNTAX",
203 Self::InvalidIpAddress => "INVALID_IP_ADDRESS",
204 Self::MissingApiKey => "MISSING_API_KEY",
205 Self::ReservedAsn => "RESERVED_ASN",
206 Self::ReservedIpAddress => "RESERVED_IP_ADDRESS",
207 Self::TooManyAsns => "TOO_MANY_ASNS",
208 Self::TooManyIps => "TOO_MANY_IPS",
209 Self::TooManyRequests => "TOO_MANY_REQUESTS",
210 Self::TooManyUserAgents => "TOO_MANY_USER_AGENTS",
211 Self::UnknownAsn => "UNKNOWN_ASN",
212 Self::Other(raw) => raw,
213 }
214 }
215}
216
217impl fmt::Display for ErrorCode {
218 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
219 f.write_str(self.as_str())
220 }
221}
222
223#[derive(Debug, serde::Deserialize)]
225pub(crate) struct ApiErrorPayload {
226 pub(crate) code: String,
227 #[serde(default)]
228 pub(crate) message: String,
229 #[serde(default)]
230 pub(crate) resolution: String,
231}
232
233impl ApiErrorPayload {
234 pub(crate) fn into_api_error(self, status: Option<u16>) -> ApiError {
236 ApiError {
237 code: (!self.code.is_empty()).then(|| ErrorCode::from_raw(&self.code)),
238 message: self.message,
239 resolution: self.resolution,
240 status,
241 }
242 }
243}
244
245pub(crate) fn parse_api_error(data: &[u8], status: u16) -> ApiError {
248 match serde_json::from_slice::<ApiErrorPayload>(data) {
249 Ok(payload) if !payload.code.is_empty() => payload.into_api_error(Some(status)),
250 _ => ApiError {
251 code: None,
252 message: format!("unexpected HTTP status {status}"),
253 resolution: String::new(),
254 status: Some(status),
255 },
256 }
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262
263 #[test]
264 fn error_code_roundtrip() {
265 let codes = [
266 "BAD_REQUEST",
267 "DISABLED_API_KEY",
268 "FORBIDDEN_IP",
269 "FORBIDDEN_ORIGIN",
270 "FORBIDDEN_IP_ORIGIN",
271 "INTERNAL",
272 "INSUFFICIENT_CREDITS",
273 "INVALID_API_KEY",
274 "INVALID_ASN",
275 "INVALID_FILTER_SYNTAX",
276 "INVALID_IP_ADDRESS",
277 "MISSING_API_KEY",
278 "RESERVED_ASN",
279 "RESERVED_IP_ADDRESS",
280 "TOO_MANY_ASNS",
281 "TOO_MANY_IPS",
282 "TOO_MANY_REQUESTS",
283 "TOO_MANY_USER_AGENTS",
284 "UNKNOWN_ASN",
285 ];
286 for raw in codes {
287 let code = ErrorCode::from_raw(raw);
288 assert!(!matches!(code, ErrorCode::Other(_)), "{raw} not recognized");
289 assert_eq!(code.as_str(), raw);
290 }
291 }
292
293 #[test]
294 fn error_code_is_case_and_space_insensitive() {
295 assert_eq!(
296 ErrorCode::from_raw(" invalid_api_key "),
297 ErrorCode::InvalidApiKey
298 );
299 }
300
301 #[test]
302 fn unknown_error_code_is_preserved() {
303 let code = ErrorCode::from_raw("SOMETHING_NEW");
304 assert_eq!(code, ErrorCode::Other("SOMETHING_NEW".to_owned()));
305 assert_eq!(code.as_str(), "SOMETHING_NEW");
306 }
307
308 #[test]
309 fn api_error_display() {
310 let err = ApiError {
311 code: Some(ErrorCode::InvalidIpAddress),
312 message: "the IP address is invalid".to_owned(),
313 resolution: "provide a valid IPv4 or IPv6 address".to_owned(),
314 status: Some(400),
315 };
316 assert_eq!(
317 err.to_string(),
318 "ipregistry: the IP address is invalid (INVALID_IP_ADDRESS): \
319 provide a valid IPv4 or IPv6 address"
320 );
321 }
322
323 #[test]
324 fn api_error_display_without_details() {
325 let err = ApiError {
326 code: None,
327 message: String::new(),
328 resolution: String::new(),
329 status: Some(502),
330 };
331 assert_eq!(err.to_string(), "ipregistry: API error");
332 }
333
334 #[test]
335 fn parse_api_error_with_payload() {
336 let body =
337 br#"{"code":"INVALID_API_KEY","message":"key is invalid","resolution":"fix it"}"#;
338 let err = parse_api_error(body, 403);
339 assert_eq!(err.code, Some(ErrorCode::InvalidApiKey));
340 assert_eq!(err.message, "key is invalid");
341 assert_eq!(err.resolution, "fix it");
342 assert_eq!(err.status, Some(403));
343 }
344
345 #[test]
346 fn parse_api_error_with_unrecognizable_body() {
347 let err = parse_api_error(b"<html>Bad Gateway</html>", 502);
348 assert_eq!(err.code, None);
349 assert_eq!(err.message, "unexpected HTTP status 502");
350 assert_eq!(err.status, Some(502));
351 }
352}