use futures::{future, stream, Future, Stream};
use reqwest::{unstable::async, StatusCode, Url};
use serde::de::DeserializeOwned;
use tokio_core::reactor::Handle;
use super::Error;
use types::*;
#[derive(Clone)]
pub struct Client {
client: async::Client,
base_url: Url,
}
impl Client {
pub fn new(handle: &Handle) -> Self {
let c = Self {
client: async::Client::new(handle),
base_url: Url::parse("https://crates.io/api/v1/").unwrap(),
};
c
}
fn get<T: DeserializeOwned>(&self, url: Url) -> impl Future<Item = T, Error = Error> {
trace!("GET {}", url);
self.client
.get(url.clone())
.send()
.map_err(Error::from)
.and_then(|res| {
if res.status() == StatusCode::NotFound {
return Err(Error::NotFound);
}
let res = res.error_for_status()?;
Ok(res)
})
.and_then(|mut res| res.json().map_err(Error::from))
}
pub fn summary(&self) -> impl Future<Item = Summary, Error = Error> {
let url = self.base_url.join("summary").unwrap();
self.get(url)
}
pub fn get_crate(&self, name: &str) -> impl Future<Item = CrateResponse, Error = Error> {
let url = self.base_url.join("crates/").unwrap().join(name).unwrap();
self.get(url)
}
pub fn crate_downloads(&self, name: &str) -> impl Future<Item = Downloads, Error = Error> {
let url = self.base_url
.join(&format!("crates/{}/downloads", name))
.unwrap();
self.get(url)
}
pub fn crate_owners(&self, name: &str) -> impl Future<Item = Vec<User>, Error = Error> {
let url = self.base_url
.join(&format!("crates/{}/owners", name))
.unwrap();
self.get::<Owners>(url).map(|data| data.users)
}
pub fn crate_reverse_dependencies(
&self,
name: &str,
) -> impl Future<Item = Vec<Dependency>, Error = Error> {
fn fetch_page(
c: Client,
name: String,
mut deps: Vec<Dependency>,
page: u64,
) -> impl Future<Item = Vec<Dependency>, Error = Error> {
let url = c.base_url
.join(&format!(
"crates/{}/reverse_dependencies?per_page=100&page={}",
name, page
))
.unwrap();
c.get::<Dependencies>(url).and_then(
move |data| -> Box<Future<Item = Vec<Dependency>, Error = Error>> {
if data.dependencies.len() > 0 {
deps.extend(data.dependencies);
Box::new(fetch_page(c, name, deps, page + 1))
} else {
Box::new(::futures::future::ok(deps))
}
},
)
}
fetch_page(self.clone(), name.to_string(), Vec::new(), 1)
}
pub fn crate_authors(
&self,
name: &str,
version: &str,
) -> impl Future<Item = Authors, Error = Error> {
let url = self.base_url
.join(&format!("crates/{}/{}/authors", name, version))
.unwrap();
self.get::<AuthorsResponse>(url).map(|res| Authors {
names: res.meta.names,
users: res.users,
})
}
pub fn crate_dependencies(
&self,
name: &str,
version: &str,
) -> impl Future<Item = Vec<Dependency>, Error = Error> {
let url = self.base_url
.join(&format!("crates/{}/{}/dependencies", name, version))
.unwrap();
self.get::<Dependencies>(url).map(|res| res.dependencies)
}
fn full_version(&self, version: Version) -> impl Future<Item = FullVersion, Error = Error> {
let authors = self.crate_authors(&version.crate_name, &version.num);
let deps = self.crate_dependencies(&version.crate_name, &version.num);
authors.join(deps).map(|(authors, deps)| 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,
links: version.links,
readme_path: version.readme_path,
author_names: authors.names,
authors: authors.users,
dependencies: deps,
})
}
pub fn full_crate(
&self,
name: &str,
all_versions: bool,
) -> impl Future<Item = FullCrate, Error = Error> {
let c = self.clone();
let crate_and_versions = self.get_crate(name).and_then(
move |info| -> Box<Future<Item = (CrateResponse, Vec<FullVersion>), Error = Error>> {
if all_versions == false {
Box::new(
c.full_version(info.versions[0].clone())
.map(|v| (info, vec![v])),
)
} else {
Box::new(
::futures::future::join_all(
info.versions
.clone()
.into_iter()
.map(|v| c.full_version(v))
.collect::<Vec<_>>(),
).map(|versions| (info, versions)),
)
}
},
);
let dls = self.crate_downloads(name);
let owners = self.crate_owners(name);
let reverse_dependencies = self.crate_reverse_dependencies(name);
crate_and_versions
.join4(dls, owners, reverse_dependencies)
.map(|((resp, versions), dls, owners, reverse_dependencies)| {
let data = resp.crate_data;
FullCrate {
id: data.id,
name: data.name,
description: data.description,
license: data.license,
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: versions,
}
})
}
pub fn crates(&self, spec: ListOptions) -> impl Future<Item = CratesResponse, Error = Error> {
let mut url = self.base_url.join("crates").unwrap();
{
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>) -> impl Stream<Item = Crate, Error = Error> {
let opts = ListOptions {
query: query.clone(),
sort: Sort::Alphabetical,
per_page: 100,
page: 1,
};
let c = self.clone();
self.crates(opts.clone())
.and_then(move |res| {
let pages = (res.meta.total as f64 / 100.0).ceil() as u64;
let streams_futures = (1..pages)
.into_iter()
.map(|page| {
let opts = ListOptions {
page: page,
..opts.clone()
};
c.crates(opts)
.and_then(|res| future::ok(stream::iter_ok(res.crates)))
})
.collect::<Vec<_>>();
let stream = stream::futures_ordered(streams_futures).flatten();
future::ok(stream)
})
.flatten_stream()
}
pub fn all_crates_full(
&self,
query: Option<String>,
all_versions: bool,
) -> impl Stream<Item = FullCrate, Error = Error> {
let c = self.clone();
self.all_crates(query)
.and_then(move |cr| c.full_crate(&cr.name, all_versions))
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_client() {
let mut core = ::tokio_core::reactor::Core::new().unwrap();
let client = Client::new(&core.handle());
let summary = core.run(client.summary()).unwrap();
assert!(summary.most_downloaded.len() > 0);
for item in &summary.most_downloaded[0..3] {
let _ = core.run(client.full_crate(&item.name, false)).unwrap();
}
let crates = core.run(client.all_crates(None).take(3).collect()).unwrap();
println!("{:?}", crates);
}
}