use crates_io_api::SyncClient;
use log::debug;
use std::time::Duration;
const USER_AGENT: &str = "cargo-copter/0.3.0 (https://github.com/imazen/cargo-copter)";
const CRATES_IO_PAGE_SIZE: usize = 100;
const MAX_API_PAGES: usize = 100;
lazy_static::lazy_static! {
static ref CRATES_IO_CLIENT: SyncClient = {
SyncClient::new(USER_AGENT, Duration::from_millis(1000))
.expect("Failed to create crates.io API client")
};
}
pub fn get_client() -> &'static SyncClient {
&CRATES_IO_CLIENT
}
#[derive(Debug, Clone)]
pub struct ReverseDependency {
pub name: String,
pub downloads: u64,
}
pub fn get_reverse_dependencies(crate_name: &str, limit: Option<usize>) -> Result<Vec<ReverseDependency>, String> {
debug!("fetching reverse dependencies for {}", crate_name);
let mut all_deps = Vec::new();
let max_pages = match limit {
Some(lim) => lim.div_ceil(CRATES_IO_PAGE_SIZE),
None => MAX_API_PAGES,
};
for page in 1..=max_pages {
debug!("fetching page {} of reverse dependencies", page);
let deps = CRATES_IO_CLIENT
.crate_reverse_dependencies_page(crate_name, page as u64)
.map_err(|e| format!("Failed to fetch reverse dependencies: {}", e))?;
let page_size = deps.dependencies.len();
debug!("got {} dependencies on page {}", page_size, page);
for dep in deps.dependencies {
all_deps.push(ReverseDependency {
name: dep.crate_version.crate_name.clone(),
downloads: dep.crate_version.downloads,
});
}
if page_size < CRATES_IO_PAGE_SIZE {
break;
}
if let Some(lim) = limit
&& all_deps.len() >= lim
{
break;
}
}
all_deps.sort_by_key(|d| std::cmp::Reverse(d.downloads));
if let Some(lim) = limit {
all_deps.truncate(lim);
}
debug!("found {} reverse dependencies for {}", all_deps.len(), crate_name);
Ok(all_deps)
}
pub fn get_top_dependents(crate_name: &str, limit: usize) -> Result<Vec<ReverseDependency>, String> {
get_reverse_dependencies(crate_name, Some(limit))
}
#[derive(Debug, Clone)]
pub struct VersionDownloads {
pub version: String,
pub downloads: u64,
pub yanked: bool,
}
pub fn get_version_downloads(crate_name: &str) -> Result<Vec<VersionDownloads>, String> {
debug!("fetching version downloads for {}", crate_name);
let krate = CRATES_IO_CLIENT
.get_crate(crate_name)
.map_err(|e| format!("Failed to fetch crate info for {}: {}", crate_name, e))?;
let mut versions: Vec<VersionDownloads> = krate
.versions
.iter()
.filter(|v| !v.yanked)
.filter(|v| {
semver::Version::parse(&v.num).map(|sv| sv.pre.is_empty()).unwrap_or(false)
})
.map(|v| VersionDownloads { version: v.num.clone(), downloads: v.downloads, yanked: v.yanked })
.collect();
versions.sort_by_key(|v| std::cmp::Reverse(v.downloads));
debug!("found {} versions for {}", versions.len(), crate_name);
Ok(versions)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[ignore] fn test_get_top_dependents() {
let deps = get_top_dependents("serde", 5).unwrap();
assert_eq!(deps.len(), 5);
for i in 1..deps.len() {
assert!(deps[i - 1].downloads >= deps[i].downloads);
}
}
#[test]
#[ignore] fn test_get_reverse_dependencies_with_limit() {
let deps = get_reverse_dependencies("log", Some(10)).unwrap();
assert_eq!(deps.len(), 10);
}
#[test]
fn test_reverse_dependency_structure() {
let dep = ReverseDependency { name: "test-crate".to_string(), downloads: 1000 };
assert_eq!(dep.name, "test-crate");
assert_eq!(dep.downloads, 1000);
}
}