Skip to content
Open
33 changes: 18 additions & 15 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,30 +1,33 @@
[package]
authors = ["theduke <[email protected]>"]
name = "crates_io_api"
description = "API client for crates.io"
authors = ["theduke <[email protected]>", "jonaspleyer <[email protected]"]
name = "crates_io_api-wasm-patch"
description = "WASM-compatible patch of crates_io_api"
license = "MIT/Apache-2.0"
repository = "https://github.com/theduke/crates-io-api"
documentation = "https://docs.rs/crates_io_api"
repository = "https://github.com/jonaspleyer/crates-io-api"
documentation = "https://docs.rs/crates_io_api-wasm-patch"
readme = "README.md"
keywords = [ "crates", "api" ]
categories = [ "web-programming", "web-programming::http-client" ]
edition = "2018"
resolver = "2"

version = "0.11.0"
version = "0.12.1"

[dependencies]
chrono = { version = "0.4.6", default-features = false, features = ["serde"] }
chrono = { version = "0.4", default-features = false, features = ["serde"] }
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json"] }
serde = "1.0.79"
serde_derive = "1.0.79"
serde_json = "1.0.32"
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"
serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0"
url = "2.5"
futures = "0.3"
tokio = { version = "1.43", default-features = false, features = ["sync", "time"] }
serde_path_to_error = "0.1"
web-time = { version = "1.1" }

[dev-dependencies]
tokio = { version = "1.0.1", features = ["macros"]}
tokio = { version = "1.43", features = ["macros"]}
crates_io_api = { package = "crates_io_api-wasm-patch", git = "https://github.com/jonaspleyer/crates-io-api"}

[features]
default = ["reqwest/default-tls"]
Expand Down
101 changes: 22 additions & 79 deletions src/async_client.rs
Original file line number Diff line number Diff line change
@@ -1,24 +1,32 @@
#[cfg(not(target_arch = "wasm32"))]
use futures::future::BoxFuture;
#[cfg(not(target_arch = "wasm32"))]
use futures::prelude::*;
use futures::{future::try_join_all, try_join};
use reqwest::{header, Client as HttpClient, StatusCode, Url};
use serde::de::DeserializeOwned;

#[cfg(not(target_arch = "wasm32"))]
use std::collections::VecDeque;

use web_time::Duration;

use super::Error;
use crate::error::JsonDecodeError;
use crate::types::*;
use crate::util::*;

/// Asynchronous client for the crates.io API.
#[derive(Clone)]
pub struct Client {
client: HttpClient,
rate_limit: std::time::Duration,
last_request_time: std::sync::Arc<tokio::sync::Mutex<Option<tokio::time::Instant>>>,
rate_limit: Duration,
last_request_time: std::sync::Arc<tokio::sync::Mutex<Option<web_time::Instant>>>,
base_url: Url,
}

#[cfg(not(target_arch = "wasm32"))]
#[cfg_attr(docsrs, doc(cfg(not(target_arch = "wasm32"))))]
pub struct CrateStream {
client: Client,
filter: CratesQuery,
Expand All @@ -28,6 +36,8 @@ pub struct CrateStream {
next_page_fetch: Option<BoxFuture<'static, Result<CratesPage, Error>>>,
}

#[cfg(not(target_arch = "wasm32"))]
#[cfg_attr(docsrs, doc(cfg(not(target_arch = "wasm32"))))]
impl CrateStream {
fn new(client: Client, filter: CratesQuery) -> Self {
Self {
Expand All @@ -40,6 +50,8 @@ impl CrateStream {
}
}

#[cfg(not(target_arch = "wasm32"))]
#[cfg_attr(docsrs, doc(cfg(not(target_arch = "wasm32"))))]
impl futures::stream::Stream for CrateStream {
type Item = Result<Crate, Error>;

Expand Down Expand Up @@ -112,17 +124,18 @@ impl Client {
/// Example user agent: `"my_bot (my_bot.com/info)"` or `"my_bot (help@my_bot.com)"`.
///
/// ```rust
/// # use web_time::Duration;
/// # fn f() -> Result<(), Box<dyn std::error::Error>> {
/// let client = crates_io_api::AsyncClient::new(
/// "my_bot (help@my_bot.com)",
/// std::time::Duration::from_millis(1000),
/// Duration::from_millis(1000),
/// ).unwrap();
/// # Ok(())
/// # }
/// ```
pub fn new(
user_agent: &str,
rate_limit: std::time::Duration,
rate_limit: Duration,
) -> Result<Self, reqwest::header::InvalidHeaderValue> {
let mut headers = header::HeaderMap::new();
headers.insert(
Expand All @@ -146,7 +159,7 @@ impl Client {
/// At most one request will be executed in the specified duration.
/// The guidelines suggest 1 per second or less.
/// (Only one request is executed concurrenly, even if the given Duration is 0).
pub fn with_http_client(client: HttpClient, rate_limit: std::time::Duration) -> Self {
pub fn with_http_client(client: HttpClient, rate_limit: Duration) -> Self {
let limiter = std::sync::Arc::new(tokio::sync::Mutex::new(None));

Self {
Expand All @@ -166,7 +179,7 @@ impl Client {
}
}

let time = tokio::time::Instant::now();
let time = web_time::Instant::now();
let res = self.client.get(url.clone()).send().await?;

if !res.status().is_success() {
Expand Down Expand Up @@ -380,6 +393,8 @@ impl Client {
}

/// Get a stream over all crates matching the given [`CratesQuery`].
#[cfg(not(target_arch = "wasm32"))]
#[cfg_attr(docsrs, doc(cfg(not(target_arch = "wasm32"))))]
pub fn crates_stream(&self, filter: CratesQuery) -> CrateStream {
CrateStream::new(self.clone(), filter)
}
Expand All @@ -391,86 +406,14 @@ impl Client {
}
}

pub(crate) fn build_crate_url(base: &Url, crate_name: &str) -> Result<Url, Error> {
let mut url = base.join("crates")?;
url.path_segments_mut().unwrap().push(crate_name);

// 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(),
}))
} else {
Ok(url)
}
}

fn build_crate_url_nested(base: &Url, crate_name: &str) -> Result<Url, Error> {
let mut url = base.join("crates")?;
url.path_segments_mut().unwrap().push(crate_name).push("/");

// 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(),
}))
} else {
Ok(url)
}
}

pub(crate) fn build_crate_downloads_url(base: &Url, crate_name: &str) -> Result<Url, Error> {
build_crate_url_nested(base, crate_name)?
.join("downloads")
.map_err(Error::from)
}

pub(crate) fn build_crate_owners_url(base: &Url, crate_name: &str) -> Result<Url, Error> {
build_crate_url_nested(base, crate_name)?
.join("owners")
.map_err(Error::from)
}

pub(crate) fn build_crate_reverse_deps_url(
base: &Url,
crate_name: &str,
page: u64,
) -> Result<Url, Error> {
build_crate_url_nested(base, crate_name)?
.join(&format!("reverse_dependencies?per_page=100&page={page}"))
.map_err(Error::from)
}

pub(crate) fn build_crate_authors_url(
base: &Url,
crate_name: &str,
version: &str,
) -> Result<Url, Error> {
build_crate_url_nested(base, crate_name)?
.join(&format!("{version}/authors"))
.map_err(Error::from)
}

pub(crate) fn build_crate_dependencies_url(
base: &Url,
crate_name: &str,
version: &str,
) -> Result<Url, Error> {
build_crate_url_nested(base, crate_name)?
.join(&format!("{version}/dependencies"))
.map_err(Error::from)
}

#[cfg(test)]
mod test {
use super::*;

fn build_test_client() -> Client {
Client::new(
"crates-io-api-continuous-integration (github.com/theduke/crates-io-api)",
std::time::Duration::from_millis(1000),
web_time::Duration::from_millis(1000),
)
.unwrap()
}
Expand Down
9 changes: 8 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,22 @@

#![recursion_limit = "128"]
#![deny(missing_docs)]
#![cfg_attr(docsrs, feature(doc_cfg))]

mod async_client;
mod error;
#[cfg(not(target_arch = "wasm32"))]
#[cfg_attr(docsrs, doc(cfg(not(target_arch = "wasm32"))))]
mod sync_client;
mod types;
mod util;

pub use crate::{
async_client::Client as AsyncClient,
error::{Error, NotFoundError, PermissionDeniedError},
sync_client::SyncClient,
types::*,
};

#[cfg(not(target_arch = "wasm32"))]
#[cfg_attr(docsrs, doc(cfg(not(target_arch = "wasm32"))))]
pub use crate::sync_client::SyncClient;
15 changes: 6 additions & 9 deletions src/sync_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,19 +113,19 @@ impl SyncClient {
///
/// If you require detailed information, consider using [full_crate]().
pub fn get_crate(&self, crate_name: &str) -> Result<CrateResponse, Error> {
let url = super::async_client::build_crate_url(&self.base_url, crate_name)?;
let url = super::util::build_crate_url(&self.base_url, crate_name)?;
self.get(url)
}

/// Retrieve download stats for a crate.
pub fn crate_downloads(&self, crate_name: &str) -> Result<CrateDownloads, Error> {
let url = super::async_client::build_crate_downloads_url(&self.base_url, crate_name)?;
let url = super::util::build_crate_downloads_url(&self.base_url, crate_name)?;
self.get(url)
}

/// Retrieve the owners of a crate.
pub fn crate_owners(&self, crate_name: &str) -> Result<Vec<User>, Error> {
let url = super::async_client::build_crate_owners_url(&self.base_url, crate_name)?;
let url = super::util::build_crate_owners_url(&self.base_url, crate_name)?;
let resp: Owners = self.get(url)?;
Ok(resp.users)
}
Expand All @@ -138,8 +138,7 @@ impl SyncClient {
crate_name: &str,
page: u64,
) -> Result<ReverseDependencies, Error> {
let url =
super::async_client::build_crate_reverse_deps_url(&self.base_url, crate_name, page)?;
let url = super::util::build_crate_reverse_deps_url(&self.base_url, crate_name, page)?;
let page = self.get::<ReverseDependenciesAsReceived>(url)?;

let mut deps = ReverseDependencies {
Expand Down Expand Up @@ -185,8 +184,7 @@ impl SyncClient {

/// Retrieve the authors for a crate version.
pub fn crate_authors(&self, crate_name: &str, version: &str) -> Result<Authors, Error> {
let url =
super::async_client::build_crate_authors_url(&self.base_url, crate_name, version)?;
let url = super::util::build_crate_authors_url(&self.base_url, crate_name, version)?;
let res: AuthorsResponse = self.get(url)?;
Ok(Authors {
names: res.meta.names,
Expand All @@ -199,8 +197,7 @@ impl SyncClient {
crate_name: &str,
version: &str,
) -> Result<Vec<Dependency>, Error> {
let url =
super::async_client::build_crate_dependencies_url(&self.base_url, crate_name, version)?;
let url = super::util::build_crate_dependencies_url(&self.base_url, crate_name, version)?;
let resp: Dependencies = self.get(url)?;
Ok(resp.dependencies)
}
Expand Down
75 changes: 75 additions & 0 deletions src/util.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
use reqwest::Url;

use super::Error;

pub(crate) fn build_crate_url(base: &Url, crate_name: &str) -> Result<Url, Error> {
let mut url = base.join("crates")?;
url.path_segments_mut().unwrap().push(crate_name);

// 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(),
}))
} else {
Ok(url)
}
}

fn build_crate_url_nested(base: &Url, crate_name: &str) -> Result<Url, Error> {
let mut url = base.join("crates")?;
url.path_segments_mut().unwrap().push(crate_name).push("/");

// 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(),
}))
} else {
Ok(url)
}
}

pub(crate) fn build_crate_downloads_url(base: &Url, crate_name: &str) -> Result<Url, Error> {
build_crate_url_nested(base, crate_name)?
.join("downloads")
.map_err(Error::from)
}

pub(crate) fn build_crate_owners_url(base: &Url, crate_name: &str) -> Result<Url, Error> {
build_crate_url_nested(base, crate_name)?
.join("owners")
.map_err(Error::from)
}

pub(crate) fn build_crate_reverse_deps_url(
base: &Url,
crate_name: &str,
page: u64,
) -> Result<Url, Error> {
build_crate_url_nested(base, crate_name)?
.join(&format!("reverse_dependencies?per_page=100&page={page}"))
.map_err(Error::from)
}

pub(crate) fn build_crate_authors_url(
base: &Url,
crate_name: &str,
version: &str,
) -> Result<Url, Error> {
build_crate_url_nested(base, crate_name)?
.join(&format!("{version}/authors"))
.map_err(Error::from)
}

pub(crate) fn build_crate_dependencies_url(
base: &Url,
crate_name: &str,
version: &str,
) -> Result<Url, Error> {
build_crate_url_nested(base, crate_name)?
.join(&format!("{version}/dependencies"))
.map_err(Error::from)
}
Loading