use std::error::Error as StdError;
use std::fmt;
pub(crate) const MAX_MESSAGE_LENGTH: usize = 512;
#[derive(Debug)]
#[non_exhaustive]
pub enum Error {
Validation(ValidationError),
Api(ApiError),
Transport(TransportError),
}
#[derive(Debug, Clone)]
pub struct ApiError {
pub kind: ApiErrorKind,
pub status: u16,
pub message: String,
pub request_id: Option<String>,
pub raw_body: String,
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum ApiErrorKind {
BadRequest,
Unauthorized,
Forbidden {
is_paid_feature: bool,
},
NotFound,
PayloadTooLarge,
RateLimited {
retry_after_seconds: Option<f64>,
},
ServerError,
Other,
}
impl ApiError {
pub fn is_paid_feature(&self) -> bool {
matches!(
self.kind,
ApiErrorKind::Forbidden {
is_paid_feature: true
}
)
}
pub fn retry_after_seconds(&self) -> Option<f64> {
match self.kind {
ApiErrorKind::RateLimited {
retry_after_seconds,
} => retry_after_seconds,
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub struct ValidationError {
pub parameter: String,
pub message: String,
}
#[derive(Debug)]
pub struct TransportError {
pub message: String,
source: Option<Box<dyn StdError + Send + Sync>>,
}
impl TransportError {
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
source: None,
}
}
pub fn with_source(
message: impl Into<String>,
source: impl StdError + Send + Sync + 'static,
) -> Self {
Self {
message: message.into(),
source: Some(Box::new(source)),
}
}
}
impl Error {
pub(crate) fn validation(parameter: impl Into<String>, message: impl Into<String>) -> Self {
Self::Validation(ValidationError {
parameter: parameter.into(),
message: message.into(),
})
}
pub fn as_api(&self) -> Option<&ApiError> {
match self {
Self::Api(error) => Some(error),
_ => None,
}
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Validation(error) => write!(f, "{error}"),
Self::Api(error) => write!(f, "{error}"),
Self::Transport(error) => write!(f, "{error}"),
}
}
}
impl fmt::Display for ApiError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.request_id {
Some(id) => write!(f, "HTTP {}: {} (request {id})", self.status, self.message),
None => write!(f, "HTTP {}: {}", self.status, self.message),
}
}
}
impl fmt::Display for ValidationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "invalid {}: {}", self.parameter, self.message)
}
}
impl fmt::Display for TransportError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.message)
}
}
impl StdError for Error {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
match self {
Self::Transport(error) => error.source(),
_ => None,
}
}
}
impl StdError for ApiError {}
impl StdError for ValidationError {}
impl StdError for TransportError {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
self.source.as_ref().map(|source| source.as_ref() as _)
}
}
impl From<ApiError> for Error {
fn from(error: ApiError) -> Self {
Self::Api(error)
}
}
impl From<TransportError> for Error {
fn from(error: TransportError) -> Self {
Self::Transport(error)
}
}
pub(crate) fn error_for_status(
status: u16,
message: String,
request_id: Option<String>,
raw_body: String,
retry_after_seconds: Option<f64>,
) -> ApiError {
let kind = match status {
400 => ApiErrorKind::BadRequest,
401 => ApiErrorKind::Unauthorized,
403 => ApiErrorKind::Forbidden {
is_paid_feature: message.to_lowercase().contains("paid feature"),
},
404 => ApiErrorKind::NotFound,
413 => ApiErrorKind::PayloadTooLarge,
429 => ApiErrorKind::RateLimited {
retry_after_seconds,
},
500..=599 => ApiErrorKind::ServerError,
_ => ApiErrorKind::Other,
};
ApiError {
kind,
status,
message,
request_id,
raw_body,
}
}
pub(crate) fn extract_message(raw_body: &str, reason_phrase: Option<&str>) -> String {
if !raw_body.trim().is_empty() {
if let Ok(serde_json::Value::Object(parsed)) =
serde_json::from_str::<serde_json::Value>(raw_body)
{
if let Some(detail) = parsed.get("message").and_then(serde_json::Value::as_str) {
if !detail.trim().is_empty() {
return truncate(detail);
}
}
}
return truncate(raw_body.trim());
}
match reason_phrase {
Some(phrase) if !phrase.trim().is_empty() => phrase.to_owned(),
_ => "The LabelZoom API returned an error with no response body.".to_owned(),
}
}
fn truncate(value: &str) -> String {
match value.char_indices().nth(MAX_MESSAGE_LENGTH) {
Some((index, _)) => value[..index].to_owned(),
None => value.to_owned(),
}
}
pub(crate) fn reason_phrase(status: u16) -> Option<&'static str> {
Some(match status {
400 => "Bad Request",
401 => "Unauthorized",
403 => "Forbidden",
404 => "Not Found",
405 => "Method Not Allowed",
406 => "Not Acceptable",
408 => "Request Timeout",
409 => "Conflict",
413 => "Payload Too Large",
415 => "Unsupported Media Type",
422 => "Unprocessable Entity",
429 => "Too Many Requests",
500 => "Internal Server Error",
501 => "Not Implemented",
502 => "Bad Gateway",
503 => "Service Unavailable",
504 => "Gateway Timeout",
_ => return None,
})
}