use std::time::Duration;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error(transparent)]
Http(#[from] HttpError),
#[error("unexpected status {code}: {body}")]
#[non_exhaustive]
Status {
code: u16,
body: String,
},
#[error("unauthorized (401)")]
Unauthorized,
#[error("forbidden (403)")]
Forbidden,
#[error("rate limited (429)")]
#[non_exhaustive]
RateLimited {
retry_after: Option<Duration>,
},
#[error("operation requires an access token, but none is set")]
Unauthenticated,
#[error("deserialize error: {0}")]
Deserialize(#[from] serde_json::Error),
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("invalid utf-8 in response body: {0}")]
Utf8(#[from] std::string::FromUtf8Error),
#[error("oauth error: {error}")]
#[non_exhaustive]
Oauth {
error: String,
description: Option<String>,
},
}
impl Error {
pub fn is_timeout(&self) -> bool {
matches!(self, Error::Http(e) if e.is_timeout())
}
pub fn is_connect(&self) -> bool {
matches!(self, Error::Http(e) if e.is_connect())
}
}
#[derive(Debug)]
pub struct HttpError(reqwest::Error);
impl HttpError {
pub fn is_timeout(&self) -> bool {
self.0.is_timeout()
}
pub fn is_connect(&self) -> bool {
self.0.is_connect()
}
}
impl std::fmt::Display for HttpError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl std::error::Error for HttpError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.0.source()
}
}
impl HttpError {
pub(crate) fn new(e: reqwest::Error) -> Self {
HttpError(e)
}
}
pub(crate) trait TransportResultExt<T> {
fn transport(self) -> Result<T>;
}
impl<T> TransportResultExt<T> for std::result::Result<T, reqwest::Error> {
fn transport(self) -> Result<T> {
self.map_err(|e| Error::Http(HttpError::new(e)))
}
}
pub type Result<T> = std::result::Result<T, Error>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rate_limited_carries_retry_after() {
let e = Error::RateLimited {
retry_after: Some(Duration::from_secs(30)),
};
assert!(matches!(e, Error::RateLimited { retry_after: Some(d) } if d.as_secs() == 30));
}
#[test]
fn display_is_human_readable() {
assert_eq!(Error::Unauthorized.to_string(), "unauthorized (401)");
}
}