Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]}
Expand Down
22 changes: 8 additions & 14 deletions src/async_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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()),
};
Expand All @@ -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()
))
})
}

Expand Down Expand Up @@ -398,9 +396,7 @@ pub(crate) fn build_crate_url(base: &Url, crate_name: &str) -> Result<Url, Error
// 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)
}
Expand All @@ -413,9 +409,7 @@ fn build_crate_url_nested(base: &Url, crate_name: &str) -> Result<Url, Error> {
// 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)
}
Expand Down
126 changes: 16 additions & 110 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -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),
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed everything below because thiserror automatically implements them

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::<Vec<_>>()
.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<reqwest::Error> for Error {
fn from(e: reqwest::Error) -> Self {
Error::Http(e)
}
}

impl From<url::ParseError> 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),
}
5 changes: 1 addition & 4 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*,
};
15 changes: 7 additions & 8 deletions src/sync_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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()),
};
Expand All @@ -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()
))
})
}

Expand Down
18 changes: 5 additions & 13 deletions src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ApiError>,
}

/// 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<String>,
}

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 {
Expand Down