Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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