use std::fmt;
pub type Result<T, E = Error> = std::result::Result<T, E>;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error(transparent)]
Api(#[from] ApiError),
#[error("ipregistry: transport error: {0}")]
Transport(#[from] reqwest::Error),
#[error("ipregistry: failed to decode response: {0}")]
Decode(#[from] serde_json::Error),
#[error("ipregistry: invalid configuration: {0}")]
Config(String),
}
impl Error {
pub fn as_api(&self) -> Option<&ApiError> {
match self {
Error::Api(err) => Some(err),
_ => None,
}
}
pub fn code(&self) -> Option<&ErrorCode> {
self.as_api().and_then(|err| err.code.as_ref())
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub struct ApiError {
pub code: Option<ErrorCode>,
pub message: String,
pub resolution: String,
pub status: Option<u16>,
}
impl fmt::Display for ApiError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("ipregistry: ")?;
if self.message.is_empty() {
f.write_str("API error")?;
} else {
f.write_str(&self.message)?;
}
if let Some(code) = &self.code {
write!(f, " ({code})")?;
}
if !self.resolution.is_empty() {
write!(f, ": {}", self.resolution)?;
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ErrorCode {
BadRequest,
DisabledApiKey,
ForbiddenIp,
ForbiddenOrigin,
ForbiddenIpOrigin,
Internal,
InsufficientCredits,
InvalidApiKey,
InvalidAsn,
InvalidFilterSyntax,
InvalidIpAddress,
MissingApiKey,
ReservedAsn,
ReservedIpAddress,
TooManyAsns,
TooManyIps,
TooManyRequests,
TooManyUserAgents,
UnknownAsn,
Other(String),
}
impl ErrorCode {
pub fn from_raw(raw: &str) -> Self {
match raw.trim().to_ascii_uppercase().as_str() {
"BAD_REQUEST" => Self::BadRequest,
"DISABLED_API_KEY" => Self::DisabledApiKey,
"FORBIDDEN_IP" => Self::ForbiddenIp,
"FORBIDDEN_ORIGIN" => Self::ForbiddenOrigin,
"FORBIDDEN_IP_ORIGIN" => Self::ForbiddenIpOrigin,
"INTERNAL" => Self::Internal,
"INSUFFICIENT_CREDITS" => Self::InsufficientCredits,
"INVALID_API_KEY" => Self::InvalidApiKey,
"INVALID_ASN" => Self::InvalidAsn,
"INVALID_FILTER_SYNTAX" => Self::InvalidFilterSyntax,
"INVALID_IP_ADDRESS" => Self::InvalidIpAddress,
"MISSING_API_KEY" => Self::MissingApiKey,
"RESERVED_ASN" => Self::ReservedAsn,
"RESERVED_IP_ADDRESS" => Self::ReservedIpAddress,
"TOO_MANY_ASNS" => Self::TooManyAsns,
"TOO_MANY_IPS" => Self::TooManyIps,
"TOO_MANY_REQUESTS" => Self::TooManyRequests,
"TOO_MANY_USER_AGENTS" => Self::TooManyUserAgents,
"UNKNOWN_ASN" => Self::UnknownAsn,
_ => Self::Other(raw.to_owned()),
}
}
pub fn as_str(&self) -> &str {
match self {
Self::BadRequest => "BAD_REQUEST",
Self::DisabledApiKey => "DISABLED_API_KEY",
Self::ForbiddenIp => "FORBIDDEN_IP",
Self::ForbiddenOrigin => "FORBIDDEN_ORIGIN",
Self::ForbiddenIpOrigin => "FORBIDDEN_IP_ORIGIN",
Self::Internal => "INTERNAL",
Self::InsufficientCredits => "INSUFFICIENT_CREDITS",
Self::InvalidApiKey => "INVALID_API_KEY",
Self::InvalidAsn => "INVALID_ASN",
Self::InvalidFilterSyntax => "INVALID_FILTER_SYNTAX",
Self::InvalidIpAddress => "INVALID_IP_ADDRESS",
Self::MissingApiKey => "MISSING_API_KEY",
Self::ReservedAsn => "RESERVED_ASN",
Self::ReservedIpAddress => "RESERVED_IP_ADDRESS",
Self::TooManyAsns => "TOO_MANY_ASNS",
Self::TooManyIps => "TOO_MANY_IPS",
Self::TooManyRequests => "TOO_MANY_REQUESTS",
Self::TooManyUserAgents => "TOO_MANY_USER_AGENTS",
Self::UnknownAsn => "UNKNOWN_ASN",
Self::Other(raw) => raw,
}
}
}
impl fmt::Display for ErrorCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, serde::Deserialize)]
pub(crate) struct ApiErrorPayload {
pub(crate) code: String,
#[serde(default)]
pub(crate) message: String,
#[serde(default)]
pub(crate) resolution: String,
}
impl ApiErrorPayload {
pub(crate) fn into_api_error(self, status: Option<u16>) -> ApiError {
ApiError {
code: (!self.code.is_empty()).then(|| ErrorCode::from_raw(&self.code)),
message: self.message,
resolution: self.resolution,
status,
}
}
}
pub(crate) fn parse_api_error(data: &[u8], status: u16) -> ApiError {
match serde_json::from_slice::<ApiErrorPayload>(data) {
Ok(payload) if !payload.code.is_empty() => payload.into_api_error(Some(status)),
_ => ApiError {
code: None,
message: format!("unexpected HTTP status {status}"),
resolution: String::new(),
status: Some(status),
},
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn error_code_roundtrip() {
let codes = [
"BAD_REQUEST",
"DISABLED_API_KEY",
"FORBIDDEN_IP",
"FORBIDDEN_ORIGIN",
"FORBIDDEN_IP_ORIGIN",
"INTERNAL",
"INSUFFICIENT_CREDITS",
"INVALID_API_KEY",
"INVALID_ASN",
"INVALID_FILTER_SYNTAX",
"INVALID_IP_ADDRESS",
"MISSING_API_KEY",
"RESERVED_ASN",
"RESERVED_IP_ADDRESS",
"TOO_MANY_ASNS",
"TOO_MANY_IPS",
"TOO_MANY_REQUESTS",
"TOO_MANY_USER_AGENTS",
"UNKNOWN_ASN",
];
for raw in codes {
let code = ErrorCode::from_raw(raw);
assert!(!matches!(code, ErrorCode::Other(_)), "{raw} not recognized");
assert_eq!(code.as_str(), raw);
}
}
#[test]
fn error_code_is_case_and_space_insensitive() {
assert_eq!(
ErrorCode::from_raw(" invalid_api_key "),
ErrorCode::InvalidApiKey
);
}
#[test]
fn unknown_error_code_is_preserved() {
let code = ErrorCode::from_raw("SOMETHING_NEW");
assert_eq!(code, ErrorCode::Other("SOMETHING_NEW".to_owned()));
assert_eq!(code.as_str(), "SOMETHING_NEW");
}
#[test]
fn api_error_display() {
let err = ApiError {
code: Some(ErrorCode::InvalidIpAddress),
message: "the IP address is invalid".to_owned(),
resolution: "provide a valid IPv4 or IPv6 address".to_owned(),
status: Some(400),
};
assert_eq!(
err.to_string(),
"ipregistry: the IP address is invalid (INVALID_IP_ADDRESS): \
provide a valid IPv4 or IPv6 address"
);
}
#[test]
fn api_error_display_without_details() {
let err = ApiError {
code: None,
message: String::new(),
resolution: String::new(),
status: Some(502),
};
assert_eq!(err.to_string(), "ipregistry: API error");
}
#[test]
fn parse_api_error_with_payload() {
let body =
br#"{"code":"INVALID_API_KEY","message":"key is invalid","resolution":"fix it"}"#;
let err = parse_api_error(body, 403);
assert_eq!(err.code, Some(ErrorCode::InvalidApiKey));
assert_eq!(err.message, "key is invalid");
assert_eq!(err.resolution, "fix it");
assert_eq!(err.status, Some(403));
}
#[test]
fn parse_api_error_with_unrecognizable_body() {
let err = parse_api_error(b"<html>Bad Gateway</html>", 502);
assert_eq!(err.code, None);
assert_eq!(err.message, "unexpected HTTP status 502");
assert_eq!(err.status, Some(502));
}
}