diff --git a/Cargo.toml b/Cargo.toml index d52f5e8..ee1464d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ url = "2.1.0" futures = "0.3.4" tokio = { version = "1.0.1", default-features = false, features = ["sync", "time"] } serde_path_to_error = "0.1.8" +thiserror = "2.0.12" [dev-dependencies] tokio = { version = "1.0.1", features = ["macros"]} diff --git a/src/async_client.rs b/src/async_client.rs index c0b628b..dc18252 100644 --- a/src/async_client.rs +++ b/src/async_client.rs @@ -7,7 +7,6 @@ use serde::de::DeserializeOwned; use std::collections::VecDeque; use super::Error; -use crate::error::JsonDecodeError; use crate::types::*; /// Asynchronous client for the crates.io API. @@ -171,12 +170,10 @@ impl Client { if !res.status().is_success() { let err = match res.status() { - StatusCode::NOT_FOUND => Error::NotFound(super::error::NotFoundError { - url: url.to_string(), - }), + StatusCode::NOT_FOUND => Error::NotFound(url.to_string()), StatusCode::FORBIDDEN => { let reason = res.text().await.unwrap_or_default(); - Error::PermissionDenied(super::error::PermissionDeniedError { reason }) + Error::PermissionDenied(reason) } _ => Error::from(res.error_for_status().unwrap_err()), }; @@ -197,9 +194,10 @@ impl Client { let jd = &mut serde_json::Deserializer::from_str(&content); serde_path_to_error::deserialize::<_, T>(jd).map_err(|err| { - Error::JsonDecode(JsonDecodeError { - message: format!("Could not decode JSON: {err} (path: {})", err.path()), - }) + Error::JsonDecode(format!( + "Could not decode JSON: {err} (path: {})", + err.path() + )) }) } @@ -398,9 +396,7 @@ pub(crate) fn build_crate_url(base: &Url, crate_name: &str) -> Result Result { // Guard against slashes in the crate name. // The API returns a nonsensical error in this case. if crate_name.contains('/') { - Err(Error::NotFound(crate::error::NotFoundError { - url: url.to_string(), - })) + Err(Error::NotFound(url.to_string())) } else { Ok(url) } diff --git a/src/error.rs b/src/error.rs index 5199175..8221add 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,119 +1,25 @@ //! Error types. /// Errors returned by the api client. -#[derive(Debug)] +#[derive(Debug, thiserror::Error)] #[non_exhaustive] pub enum Error { - /// Low-level http error. - Http(reqwest::Error), - /// Invalid URL. - Url(url::ParseError), - /// Crate could not be found. - NotFound(NotFoundError), + /// Low level http error + #[error("Low level http error: {0}")] + Http(#[from] reqwest::Error), + /// Invalid url + #[error("Invalid url: {0}")] + Url(#[from] url::ParseError), + /// Crate couldn't be found + #[error("Resource at {0} couldn't be found.")] + NotFound(String), /// No permission to access the resource. - PermissionDenied(PermissionDeniedError), + #[error("No permission to access the resource: {0}")] + PermissionDenied(String), /// JSON decoding of API response failed. - JsonDecode(JsonDecodeError), + #[error("JSON decoding of API response failed: {0}")] + JsonDecode(String), /// Error returned by the crates.io API directly. - Api(crate::types::ApiErrors), -} - -impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Error::Http(e) => e.fmt(f), - Error::Url(e) => e.fmt(f), - Error::NotFound(e) => e.fmt(f), - Error::PermissionDenied(e) => e.fmt(f), - Error::Api(err) => { - let inner = if err.errors.is_empty() { - "Unknown API error".to_string() - } else { - err.errors - .iter() - .map(|err| err.to_string()) - .collect::>() - .join(", ") - }; - - write!(f, "API Error ({})", inner) - } - Error::JsonDecode(err) => write!(f, "Could not decode API JSON response: {err}"), - } - } -} - -impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Error::Http(e) => Some(e), - Error::Url(e) => Some(e), - Error::NotFound(_) => None, - Error::PermissionDenied(_) => None, - Error::Api(_) => None, - Error::JsonDecode(err) => Some(err), - } - } - - // TODO: uncomment once backtrace feature is stabilized (https://github.com/rust-lang/rust/issues/53487). - /* - fn backtrace(&self) -> Option<&std::backtrace::Backtrace> { - match self { - Self::Http(e) => e.backtrace(), - Self::Url(e) => e.backtrace(), - Self::InvalidHeader(e) => e.backtrace(), - Self::NotFound(_) => None, - } - } - */ -} - -impl From for Error { - fn from(e: reqwest::Error) -> Self { - Error::Http(e) - } -} - -impl From for Error { - fn from(e: url::ParseError) -> Self { - Error::Url(e) - } -} - -/// Error returned when the JSON returned by the API could not be decoded. -#[derive(Debug)] -pub struct JsonDecodeError { - pub(crate) message: String, -} - -impl std::fmt::Display for JsonDecodeError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "Could not decode JSON: {}", self.message) - } -} - -impl std::error::Error for JsonDecodeError {} - -/// Error returned when a resource could not be found. -#[derive(Debug)] -pub struct NotFoundError { - pub(crate) url: String, -} - -impl std::fmt::Display for NotFoundError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "Resource at url '{}' could not be found", self.url) - } -} - -/// Error returned when a resource is not accessible. -#[derive(Debug)] -pub struct PermissionDeniedError { - pub(crate) reason: String, -} - -impl std::fmt::Display for PermissionDeniedError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "Permission denied: {}", self.reason) - } + #[error("Error returned by the crates.io API directly: {0:?}")] + Api(#[from] crate::types::ApiErrors), } diff --git a/src/lib.rs b/src/lib.rs index b304da1..54530a5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -50,8 +50,5 @@ mod sync_client; mod types; pub use crate::{ - async_client::Client as AsyncClient, - error::{Error, NotFoundError, PermissionDeniedError}, - sync_client::SyncClient, - types::*, + async_client::Client as AsyncClient, error::Error, sync_client::SyncClient, types::*, }; diff --git a/src/sync_client.rs b/src/sync_client.rs index fd2ca44..2ce1822 100644 --- a/src/sync_client.rs +++ b/src/sync_client.rs @@ -4,7 +4,7 @@ use std::iter::Extend; use reqwest::{blocking::Client as HttpClient, header, StatusCode, Url}; use serde::de::DeserializeOwned; -use crate::{error::JsonDecodeError, types::*}; +use crate::types::*; /// A synchronous client for the crates.io API. pub struct SyncClient { @@ -72,12 +72,10 @@ impl SyncClient { if !res.status().is_success() { let err = match res.status() { - StatusCode::NOT_FOUND => Error::NotFound(super::error::NotFoundError { - url: url.to_string(), - }), + StatusCode::NOT_FOUND => Error::NotFound(url.to_string()), StatusCode::FORBIDDEN => { let reason = res.text().unwrap_or_default(); - Error::PermissionDenied(super::error::PermissionDeniedError { reason }) + Error::PermissionDenied(reason) } _ => Error::from(res.error_for_status().unwrap_err()), }; @@ -97,9 +95,10 @@ impl SyncClient { let jd = &mut serde_json::Deserializer::from_str(&content); serde_path_to_error::deserialize::<_, T>(jd).map_err(|err| { - Error::JsonDecode(JsonDecodeError { - message: format!("Could not decode JSON: {err} (path: {})", err.path()), - }) + Error::JsonDecode(format!( + "Could not decode JSON: {err} (path: {})", + err.path() + )) }) } diff --git a/src/types.rs b/src/types.rs index a082d2e..4aa2ddb 100644 --- a/src/types.rs +++ b/src/types.rs @@ -2,32 +2,24 @@ use chrono::{DateTime, NaiveDate, Utc}; use serde_derive::*; -use std::{collections::HashMap, fmt}; +use std::collections::HashMap; /// A list of errors returned by the API. -#[derive(Deserialize, Debug, Clone, PartialEq, Eq)] +#[derive(Deserialize, Debug, Clone, PartialEq, Eq, thiserror::Error)] +#[error("A list of errors returned by the API: {errors:?}")] pub struct ApiErrors { /// Individual errors. pub errors: Vec, } /// An error returned by the API. -#[derive(Deserialize, Debug, Clone, PartialEq, Eq)] +#[derive(Deserialize, Debug, Clone, PartialEq, Eq, thiserror::Error)] +#[error("An error returned by the API: {detail:?}")] pub struct ApiError { /// Error message. pub detail: Option, } -impl fmt::Display for ApiError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{}", - self.detail.as_deref().unwrap_or("Unknown API Error") - ) - } -} - /// Used to specify the sort behaviour of the `Client::crates()` method. #[derive(Debug, Clone, PartialEq, Eq)] pub enum Sort {