use std::convert::Infallible;
use std::fmt;
use std::time::Duration;
use ::http::{StatusCode, Uri};
pub type Result<T, E = Error> = std::result::Result<T, E>;
pub type BoxError = Box<dyn std::error::Error + Send + Sync>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ErrorKind {
Invalid,
Http,
Rejected,
Timeout,
Backend,
Worker,
Dataset,
Frontier,
Cancelled,
}
impl ErrorKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Invalid => "invalid",
Self::Http => "http",
Self::Rejected => "rejected",
Self::Timeout => "timeout",
Self::Backend => "backend",
Self::Worker => "worker",
Self::Dataset => "dataset",
Self::Frontier => "frontier",
Self::Cancelled => "cancelled",
}
}
#[must_use]
pub const fn is_retryable(self) -> bool {
matches!(self, Self::Http | Self::Timeout | Self::Backend)
}
}
impl fmt::Display for ErrorKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, thiserror::Error)]
pub struct Error {
kind: ErrorKind,
message: String,
status: Option<StatusCode>,
retry_after: Option<Duration>,
uri: Option<Box<Uri>>,
#[source]
source: Option<BoxError>,
}
impl Error {
#[must_use]
pub fn new(kind: ErrorKind) -> Self {
Self {
kind,
message: String::new(),
status: None,
retry_after: None,
uri: None,
source: None,
}
}
pub fn from_boxed(kind: ErrorKind, error: impl Into<BoxError>) -> Self {
match error.into().downcast::<Self>() {
Ok(error) => *error,
Err(error) => Self::new(kind).with_source(error),
}
}
pub fn invalid(message: impl Into<String>) -> Self {
Self::new(ErrorKind::Invalid).with_message(message)
}
pub fn backend(message: impl Into<String>) -> Self {
Self::new(ErrorKind::Backend).with_message(message)
}
pub fn worker(message: impl Into<String>) -> Self {
Self::new(ErrorKind::Worker).with_message(message)
}
#[must_use]
pub fn with_message(mut self, message: impl Into<String>) -> Self {
self.message = message.into();
self
}
#[must_use]
pub fn with_source(mut self, source: impl Into<BoxError>) -> Self {
self.source = Some(source.into());
self
}
#[must_use]
pub const fn with_status(mut self, status: StatusCode) -> Self {
self.status = Some(status);
self
}
#[must_use]
pub fn with_uri(mut self, uri: Uri) -> Self {
self.uri = Some(Box::new(uri));
self
}
#[must_use]
pub const fn with_retry_after(mut self, delay: Duration) -> Self {
self.retry_after = Some(delay);
self
}
#[must_use]
pub const fn kind(&self) -> ErrorKind {
self.kind
}
#[must_use]
pub fn message(&self) -> &str {
&self.message
}
#[must_use]
pub const fn status(&self) -> Option<StatusCode> {
self.status
}
#[must_use]
pub fn uri(&self) -> Option<&Uri> {
self.uri.as_deref()
}
#[must_use]
pub const fn retry_after(&self) -> Option<Duration> {
self.retry_after
}
#[must_use]
pub const fn is_retryable(&self) -> bool {
self.kind.is_retryable()
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match (self.message.is_empty(), &self.source) {
(false, _) => f.write_str(&self.message),
(true, Some(source)) => write!(f, "{}: {source}", self.kind),
(true, None) => write!(f, "{} error", self.kind),
}
}
}
impl From<Infallible> for Error {
fn from(never: Infallible) -> Self {
match never {}
}
}
impl From<::http::Error> for Error {
fn from(error: ::http::Error) -> Self {
Self::invalid("invalid request").with_source(error)
}
}
impl From<::http::uri::InvalidUri> for Error {
fn from(error: ::http::uri::InvalidUri) -> Self {
Self::invalid("invalid uri").with_source(error)
}
}
#[cfg(test)]
mod tests {
use std::error::Error as _;
use super::*;
#[test]
fn display_prefers_message() {
let error = Error::backend("pool exhausted");
assert_eq!(error.to_string(), "pool exhausted");
assert_eq!(Error::new(ErrorKind::Timeout).to_string(), "timeout error");
}
#[test]
fn from_boxed_keeps_existing_kind() {
let inner = Error::new(ErrorKind::Timeout);
let error = Error::from_boxed(ErrorKind::Backend, inner);
assert_eq!(error.kind(), ErrorKind::Timeout);
let error = Error::from_boxed(ErrorKind::Backend, std::io::Error::other("io"));
assert_eq!(error.kind(), ErrorKind::Backend);
assert!(error.source().is_some());
}
#[test]
fn retryable_kinds() {
assert!(Error::new(ErrorKind::Http).is_retryable());
assert!(!Error::new(ErrorKind::Rejected).is_retryable());
assert!(!Error::invalid("bad").is_retryable());
}
}