Skip to content

Add WASM Support #77

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
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"]
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,
@@ -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 {
@@ -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>;

@@ -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(
@@ -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 {
@@ -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() {
@@ -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)
}
@@ -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()
}
9 changes: 8 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
@@ -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
@@ -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)
}
@@ -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 {
@@ -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,
@@ -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)
}
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)
}