crates_io_api_wasm_patch/
sync_client.rsuse super::*;
use std::iter::Extend;
use reqwest::{blocking::Client as HttpClient, header, StatusCode, Url};
use serde::de::DeserializeOwned;
use crate::{error::JsonDecodeError, types::*};
pub struct SyncClient {
client: HttpClient,
base_url: Url,
rate_limit: std::time::Duration,
last_request_time: std::sync::Mutex<Option<std::time::Instant>>,
}
impl SyncClient {
pub fn new(
user_agent: &str,
rate_limit: std::time::Duration,
) -> Result<Self, reqwest::header::InvalidHeaderValue> {
let mut headers = header::HeaderMap::new();
headers.insert(
header::USER_AGENT,
header::HeaderValue::from_str(user_agent)?,
);
Ok(Self {
client: HttpClient::builder()
.default_headers(headers)
.build()
.unwrap(),
base_url: Url::parse("https://crates.io/api/v1/").unwrap(),
rate_limit,
last_request_time: std::sync::Mutex::new(None),
})
}
fn get<T: DeserializeOwned>(&self, url: Url) -> Result<T, Error> {
let mut lock = self.last_request_time.lock().unwrap();
if let Some(last_request_time) = lock.take() {
let now = std::time::Instant::now();
if last_request_time.elapsed() < self.rate_limit {
std::thread::sleep((last_request_time + self.rate_limit) - now);
}
}
let time = std::time::Instant::now();
let res = self.client.get(url.clone()).send()?;
if !res.status().is_success() {
let err = match res.status() {
StatusCode::NOT_FOUND => Error::NotFound(super::error::NotFoundError {
url: url.to_string(),
}),
StatusCode::FORBIDDEN => {
let reason = res.text().unwrap_or_default();
Error::PermissionDenied(super::error::PermissionDeniedError { reason })
}
_ => Error::from(res.error_for_status().unwrap_err()),
};
return Err(err);
}
*lock = Some(time);
let content = res.text()?;
if let Ok(errors) = serde_json::from_str::<ApiErrors>(&content) {
return Err(Error::Api(errors));
}
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()),
})
})
}
pub fn summary(&self) -> Result<Summary, Error> {
let url = self.base_url.join("summary").unwrap();
self.get(url)
}
pub fn get_crate(&self, crate_name: &str) -> Result<CrateResponse, Error> {
let url = super::util::build_crate_url(&self.base_url, crate_name)?;
self.get(url)
}
pub fn crate_downloads(&self, crate_name: &str) -> Result<CrateDownloads, Error> {
let url = super::util::build_crate_downloads_url(&self.base_url, crate_name)?;
self.get(url)
}
pub fn crate_owners(&self, crate_name: &str) -> Result<Vec<User>, Error> {
let url = super::util::build_crate_owners_url(&self.base_url, crate_name)?;
let resp: Owners = self.get(url)?;
Ok(resp.users)
}
pub fn crate_reverse_dependencies_page(
&self,
crate_name: &str,
page: u64,
) -> Result<ReverseDependencies, Error> {
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 {
dependencies: Vec::new(),
meta: Meta { total: 0 },
};
deps.meta.total = page.meta.total;
deps.extend(page);
Ok(deps)
}
pub fn crate_reverse_dependencies(
&self,
crate_name: &str,
) -> Result<ReverseDependencies, Error> {
let mut deps = ReverseDependencies {
dependencies: Vec::new(),
meta: Meta { total: 0 },
};
for page_number in 1.. {
let page = self.crate_reverse_dependencies_page(crate_name, page_number)?;
if page.dependencies.is_empty() {
break;
}
deps.dependencies.extend(page.dependencies);
deps.meta.total = page.meta.total;
}
Ok(deps)
}
pub fn crate_reverse_dependency_count(&self, crate_name: &str) -> Result<u64, Error> {
let page = self.crate_reverse_dependencies_page(crate_name, 1)?;
Ok(page.meta.total)
}
pub fn crate_authors(&self, crate_name: &str, version: &str) -> Result<Authors, Error> {
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,
})
}
pub fn crate_dependencies(
&self,
crate_name: &str,
version: &str,
) -> Result<Vec<Dependency>, Error> {
let url = super::util::build_crate_dependencies_url(&self.base_url, crate_name, version)?;
let resp: Dependencies = self.get(url)?;
Ok(resp.dependencies)
}
fn full_version(&self, version: Version) -> Result<FullVersion, Error> {
let authors = self.crate_authors(&version.crate_name, &version.num)?;
let deps = self.crate_dependencies(&version.crate_name, &version.num)?;
Ok(FullVersion::from_parts(version, authors, deps))
}
pub fn full_crate(&self, name: &str, all_versions: bool) -> Result<FullCrate, Error> {
let resp = self.get_crate(name)?;
let data = resp.crate_data;
let dls = self.crate_downloads(name)?;
let owners = self.crate_owners(name)?;
let reverse_dependencies = self.crate_reverse_dependencies(name)?;
let versions = if resp.versions.is_empty() {
vec![]
} else if all_versions {
resp.versions
.into_iter()
.map(|v| self.full_version(v))
.collect::<Result<Vec<FullVersion>, Error>>()?
} else {
let v = self.full_version(resp.versions[0].clone())?;
vec![v]
};
let full = FullCrate {
id: data.id,
name: data.name,
description: data.description,
license: versions[0].license.clone(),
documentation: data.documentation,
homepage: data.homepage,
repository: data.repository,
total_downloads: data.downloads,
recent_downloads: data.recent_downloads,
max_version: data.max_version,
max_stable_version: data.max_stable_version,
created_at: data.created_at,
updated_at: data.updated_at,
categories: resp.categories,
keywords: resp.keywords,
downloads: dls,
owners,
reverse_dependencies,
versions,
};
Ok(full)
}
pub fn crates(&self, query: CratesQuery) -> Result<CratesPage, Error> {
let mut url = self.base_url.join("crates")?;
query.build(url.query_pairs_mut());
self.get(url)
}
pub fn user(&self, username: &str) -> Result<User, Error> {
let url = self.base_url.join(&format!("users/{}", username))?;
self.get::<UserResponse>(url).map(|response| response.user)
}
}
#[cfg(test)]
mod test {
use super::*;
fn build_test_client() -> SyncClient {
SyncClient::new(
"crates-io-api-ci (github.com/theduke/crates-io-api)",
std::time::Duration::from_millis(1000),
)
.unwrap()
}
#[test]
fn test_summary() -> Result<(), Error> {
let client = build_test_client();
let summary = client.summary()?;
assert!(!summary.most_downloaded.is_empty());
assert!(!summary.just_updated.is_empty());
assert!(!summary.new_crates.is_empty());
assert!(!summary.most_recently_downloaded.is_empty());
assert!(summary.num_crates > 0);
assert!(summary.num_downloads > 0);
assert!(!summary.popular_categories.is_empty());
assert!(!summary.popular_keywords.is_empty());
Ok(())
}
#[test]
fn test_full_crate() -> Result<(), Error> {
let client = build_test_client();
client.full_crate("crates_io_api", false)?;
Ok(())
}
#[test]
fn sync_client_ensure_send() {
let client = build_test_client();
let _: &dyn Send = &client;
}
#[test]
fn test_user_get_async() -> Result<(), Error> {
let client = build_test_client();
let user = client.user("theduke")?;
assert_eq!(user.login, "theduke");
Ok(())
}
#[test]
fn test_crates_filter_by_user_async() -> Result<(), Error> {
let client = build_test_client();
let user = client.user("theduke")?;
let res = client.crates(CratesQuery {
user_id: Some(user.id),
per_page: 5,
..Default::default()
})?;
assert!(!res.crates.is_empty());
for krate in res.crates {
let owners = client.crate_owners(&krate.name)?;
assert!(owners.iter().any(|o| o.id == user.id));
}
Ok(())
}
#[test]
fn test_crate_reverse_dependency_count() -> Result<(), Error> {
let client = build_test_client();
let count = client.crate_reverse_dependency_count("crates_io_api")?;
assert!(count > 0);
Ok(())
}
}