use std::string::FromUtf8Error;
use std::{error, fmt, io::Error as IoError, num::TryFromIntError};
use derive_is_enum_variant::is_enum_variant;
#[cfg(feature = "env")]
use envy::Error as EnvyError;
use reqwest::{header::ToStrError as HeaderStrError, Error as HttpError, StatusCode};
use serde::Deserialize;
use serde_json::Error as SerdeError;
use serde_urlencoded::ser::Error as UrlEncodedError;
#[cfg(feature = "toml")]
use tomlcrate::de::Error as TomlDeError;
#[cfg(feature = "toml")]
use tomlcrate::ser::Error as TomlSerError;
use url::ParseError as UrlError;
pub type Result<T> = ::std::result::Result<T, Error>;
#[derive(Debug, thiserror::Error, is_enum_variant)]
pub enum Error {
#[error("API error: status: {status:?}, response:\n{response:#?}")]
Api {
status: StatusCode,
response: ApiError,
},
#[error("error from serde")]
Serde(#[from] SerdeError),
#[error("error serializing to url-encoded string")]
UrlEncoded(#[from] UrlEncodedError),
#[error("Error encountered in the HTTP backend while requesting a route.")]
Http(#[from] HttpError),
#[error("io error")]
Io(#[from] IoError),
#[error("error parsing URL")]
Url(#[from] UrlError),
#[error("Missing Client Id.")]
ClientIdRequired,
#[error("Missing Client Secret.")]
ClientSecretRequired,
#[error("Missing Access Token.")]
AccessTokenRequired,
#[error("builder required field {0:?} to be constructed")]
MissingField(&'static str),
#[cfg(feature = "toml")]
#[error("Error serializing to toml")]
TomlSer(#[from] TomlSerError),
#[cfg(feature = "toml")]
#[error("Error deserializing from toml")]
TomlDe(#[from] TomlDeError),
#[cfg(any(feature = "toml", feature = "json"))]
#[error("Not all bytes were written")]
NotAllBytesWritten,
#[error("Error converting an http header to a string")]
HeaderStrError(#[from] HeaderStrError),
#[error("error parsing http link header")]
LinkHeaderParse(#[from] parse_link_header::Error),
#[error("unrecognized rel {rel:?} in link header {link:?}")]
UnrecognizedRel {
rel: String,
link: String,
},
#[cfg(feature = "env")]
#[error("Error deserializing config from the environment")]
Envy(#[from] EnvyError),
#[error("integer didn't fit in the target size")]
IntConversion(#[from] TryFromIntError),
#[error(transparent)]
Entities(#[from] mastodon_async_entities::error::Error),
#[error(transparent)]
FromUtf8(#[from] FromUtf8Error),
#[error("other error: {0:?}")]
Other(String),
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ApiError {
pub error: String,
pub error_description: Option<String>,
}
impl fmt::Display for ApiError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{self:?}")
}
}
impl error::Error for ApiError {}
#[macro_export]
macro_rules! format_err {
( $( $arg:tt )* ) => {
{
$crate::Error::Other(format!($($arg)*))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io;
macro_rules! assert_is {
($err:ident, $variant:pat) => {
assert!(match $err {
$variant => true,
_ => false,
});
};
}
#[tokio::test]
async fn from_http_error() {
let err: HttpError = reqwest::get("not an actual URL").await.unwrap_err();
let err: Error = Error::from(err);
assert_is!(err, Error::Http(..));
}
#[test]
fn from_io_error() {
let err: IoError = io::Error::new(io::ErrorKind::Other, "other error");
let err: Error = Error::from(err);
assert_is!(err, Error::Io(..));
}
#[test]
fn from_serde_error() {
let err: SerdeError = serde_json::from_str::<()>("not valid json").unwrap_err();
let err: Error = Error::from(err);
assert_is!(err, Error::Serde(..));
}
#[test]
fn from_url_encoded_error() {
let err: UrlEncodedError = serde_urlencoded::ser::Error::Custom("error".into());
let err: Error = Error::from(err);
assert_is!(err, Error::UrlEncoded(..));
}
#[test]
fn from_url_error() {
let err: UrlError = UrlError::EmptyHost;
let err: Error = Error::from(err);
assert_is!(err, Error::Url(..));
}
#[cfg(feature = "toml")]
#[test]
fn from_toml_de_error() {
use tomlcrate;
let err: TomlDeError = tomlcrate::from_str::<()>("not valid toml").unwrap_err();
let err: Error = Error::from(err);
assert_is!(err, Error::TomlDe(..));
}
}