use super::*;
use std::iter::Extend;
use log::trace;
use reqwest::{header, StatusCode, Url};
use serde::de::DeserializeOwned;
use crate::types::*;
pub struct SyncClient {
client: reqwest::Client,
base_url: Url,
}
impl SyncClient {
pub fn new() -> Self {
Self {
client: reqwest::Client::new(),
base_url: Url::parse("https://crates.io/api/v1/").unwrap(),
}
}
pub fn with_user_agent(user_agent: &str) -> Self {
let mut headers = header::HeaderMap::new();
headers.insert(
header::USER_AGENT,
header::HeaderValue::from_str(user_agent).unwrap(),
);
Self {
client: reqwest::Client::builder()
.default_headers(headers)
.build()
.unwrap(),
base_url: Url::parse("https://crates.io/api/v1/").unwrap(),
}
}
fn get<T: DeserializeOwned>(&self, url: Url) -> Result<T, Error> {
trace!("GET {}", url);
let mut res = {
let res = self.client.get(url).send()?;
if res.status() == StatusCode::NOT_FOUND {
return Err(Error::NotFound);
}
res.error_for_status()?
};
let data: T = res.json()?;
Ok(data)
}
pub fn summary(&self) -> Result<Summary, Error> {
let url = self.base_url.join("summary").unwrap();
self.get(url)
}
pub fn get_crate(&self, name: &str) -> Result<CrateResponse, Error> {
let url = self.base_url.join("crates/")?.join(name)?;
self.get(url)
}
pub fn crate_downloads(&self, name: &str) -> Result<Downloads, Error> {
let url = self.base_url.join(&format!("crates/{}/downloads", name))?;
self.get(url)
}
pub fn crate_owners(&self, name: &str) -> Result<Vec<User>, Error> {
let url = self.base_url.join(&format!("crates/{}/owners", name))?;
let resp: Owners = self.get(url)?;
Ok(resp.users)
}
pub fn crate_reverse_dependencies(&self, name: &str) -> Result<Vec<Dependency>, Error> {
let mut page = 1;
let mut deps = Vec::new();
loop {
let url = self.base_url.join(&format!(
"crates/{}/reverse_dependencies?per_page=100&page={}",
name, page
))?;
let res: Dependencies = self.get(url)?;
if !res.dependencies.is_empty() {
deps.extend(res.dependencies);
page += 1;
} else {
break;
}
}
Ok(deps)
}
pub fn crate_authors(&self, name: &str, version: &str) -> Result<Authors, Error> {
let url = self
.base_url
.join(&format!("crates/{}/{}/authors", name, version))?;
let res: AuthorsResponse = self.get(url)?;
Ok(Authors {
names: res.meta.names,
users: res.users,
})
}
pub fn crate_dependencies(&self, name: &str, version: &str) -> Result<Vec<Dependency>, Error> {
let url = self
.base_url
.join(&format!("crates/{}/{}/dependencies", 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)?;
let v = FullVersion {
created_at: version.created_at,
updated_at: version.updated_at,
dl_path: version.dl_path,
downloads: version.downloads,
features: version.features,
id: version.id,
num: version.num,
yanked: version.yanked,
license: version.license,
links: version.links,
readme_path: version.readme_path,
author_names: authors.names,
authors: authors.users,
dependencies: deps,
};
Ok(v)
}
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,
max_version: data.max_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, spec: ListOptions) -> Result<CratesResponse, Error> {
let mut url = self.base_url.join("crates")?;
{
let mut q = url.query_pairs_mut();
q.append_pair("page", &spec.page.to_string());
q.append_pair("per_page", &spec.per_page.to_string());
q.append_pair("sort", spec.sort.to_str());
if let Some(query) = spec.query {
q.append_pair("q", &query);
}
}
self.get(url)
}
pub fn all_crates(&self, query: Option<String>) -> Result<Vec<Crate>, Error> {
let mut page = 1;
let mut crates = Vec::new();
loop {
let res = self.crates(ListOptions {
query: query.clone(),
sort: Sort::Alphabetical,
per_page: 100,
page,
})?;
if res.crates.is_empty() {
crates.extend(res.crates);
page += 1;
} else {
break;
}
}
Ok(crates)
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_client() {
let client = SyncClient::new();
let summary = client.summary().unwrap();
assert!(summary.most_downloaded.len() > 0);
for item in &summary.most_downloaded[0..3] {
let _ = client.full_crate(&item.name, false).unwrap();
}
}
}