use serde::Deserialize;
use serde_json::Value;
use std::fmt::{Display, Formatter};
use std::io;
use thiserror::Error as ThisError;
#[derive(Clone, Debug, Deserialize)]
pub struct ApiError {
pub code: String,
pub text: String,
pub data: Option<Value>,
}
impl Display for ApiError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "(code: {}): {}", self.code, self.text)
}
}
#[non_exhaustive]
#[derive(ThisError, Debug)]
pub enum Error {
#[error("HTTP error: {0}")]
HttpError(#[from] reqwest::Error),
#[error("Invalid header value: {0}")]
InvalidHeaderValue(#[from] reqwest::header::InvalidHeaderValue),
#[error("JSON error: {0}")]
InvalidJson(#[from] serde_json::Error),
#[error("Unable to get request lock: {0}")]
LockFailure(#[from] tokio::sync::AcquireError),
#[error("I/O error: {0}")]
IoError(#[from] io::Error),
#[error("Invalid CSRF token")]
BadToken,
#[error("Unable to get token `{0}`")]
TokenFailure(String),
#[error("You're not logged in")]
NotLoggedIn,
#[error("You're not logged in as a bot account")]
NotLoggedInAsBot,
#[error("Upload warnings: {0:?}")]
UploadWarning(Vec<String>),
#[error("maxlag tripped: {info}")]
Maxlag {
info: String,
retry_after: Option<u64>,
},
#[error("MediaWiki is readonly: {info}")]
Readonly {
info: String,
retry_after: Option<u64>,
},
#[error("Internal MediaWiki exception: {0}")]
InternalException(ApiError),
#[error("API error: {0}")]
ApiError(ApiError),
#[error("Unknown error: {0}")]
Unknown(String),
}
impl Error {
pub fn with_retry_after(self, value: u64) -> Self {
match self {
Error::Maxlag { info, .. } => Error::Maxlag {
info,
retry_after: Some(value),
},
Error::Readonly { info, .. } => Error::Readonly {
info,
retry_after: Some(value),
},
err => err,
}
}
pub fn retry_after(&self) -> Option<u64> {
match self {
Error::Maxlag { retry_after, .. } => {
Some((*retry_after).unwrap_or(1))
}
Error::Readonly { retry_after, .. } => {
Some((*retry_after).unwrap_or(1))
}
_ => None,
}
}
}
impl From<ApiError> for Error {
fn from(apierr: ApiError) -> Self {
match apierr.code.as_str() {
"assertuserfailed" => Self::NotLoggedIn,
"assertbotfailed" => Self::NotLoggedInAsBot,
"badtoken" => Self::BadToken,
"maxlag" => Self::Maxlag {
info: apierr.text,
retry_after: None,
},
"readonly" => Self::Readonly {
info: apierr.text,
retry_after: None,
},
code => {
if code.starts_with("internal_api_error_") {
Self::InternalException(apierr)
} else {
Self::ApiError(apierr)
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_from_apierror() {
let apierr = ApiError {
code: "assertbotfailed".to_string(),
text: "Something something".to_string(),
data: None,
};
let err = Error::from(apierr);
if let Error::NotLoggedInAsBot = err {
assert!(true);
} else {
panic!("Expected NotLoggedInAsBot error");
}
}
#[test]
fn test_to_string() {
let apierr = ApiError {
code: "errorcode".to_string(),
text: "Some description".to_string(),
data: None,
};
assert_eq!(&apierr.to_string(), "(code: errorcode): Some description");
}
}