use serde_json::Value;
use super::client::HttpClient;
use super::client::HttpOutcome;
use super::client::ServiceKind;
use super::client::ServiceSignal;
use super::constants::CRATES_IO_CRATE_KEY;
use super::constants::CRATES_IO_DOWNLOADS_KEY;
use super::constants::CRATES_IO_MAX_STABLE_VERSION_KEY;
use super::constants::CRATES_IO_MAX_VERSION_KEY;
use super::constants::USER_AGENT_HEADER;
use super::rate_limit;
use crate::constants::CRATES_IO_API_BASE;
use crate::constants::CRATES_IO_USER_AGENT;
use crate::scan::CratesIoInfo;
impl HttpClient {
pub(crate) async fn fetch_crates_io_info_async(
&self,
crate_name: &str,
) -> HttpOutcome<CratesIoInfo> {
let url = format!("{CRATES_IO_API_BASE}/crates/{crate_name}");
let response = match self
.client
.get(&url)
.header(USER_AGENT_HEADER, CRATES_IO_USER_AGENT)
.send()
.await
{
Ok(response) => response,
Err(error) => {
return (
None,
rate_limit::classify_network_error(ServiceKind::CratesIo, &error),
);
},
};
if response.status() == reqwest::StatusCode::TOO_MANY_REQUESTS {
return (
None,
Some(ServiceSignal::RateLimited(ServiceKind::CratesIo)),
);
}
let body = match response.bytes().await {
Ok(body) => body,
Err(error) => {
return (
None,
rate_limit::classify_network_error(ServiceKind::CratesIo, &error)
.or(Some(ServiceSignal::Reachable(ServiceKind::CratesIo))),
);
},
};
let Ok(json) = serde_json::from_slice::<serde_json::Value>(&body) else {
return (None, Some(ServiceSignal::Reachable(ServiceKind::CratesIo)));
};
let Some(krate) = json.get(CRATES_IO_CRATE_KEY) else {
return (None, Some(ServiceSignal::Reachable(ServiceKind::CratesIo)));
};
(
crates_io_info_from_crate(krate),
Some(ServiceSignal::Reachable(ServiceKind::CratesIo)),
)
}
pub(crate) fn fetch_crates_io_info(&self, crate_name: &str) -> HttpOutcome<CratesIoInfo> {
self.handle
.block_on(self.fetch_crates_io_info_async(crate_name))
}
}
fn crates_io_info_from_crate(krate: &Value) -> Option<CratesIoInfo> {
let stable = krate
.get(CRATES_IO_MAX_STABLE_VERSION_KEY)
.and_then(serde_json::Value::as_str);
let newest = krate
.get(CRATES_IO_MAX_VERSION_KEY)
.and_then(serde_json::Value::as_str);
let (version, prerelease) = match (stable, newest) {
(Some(stable), Some(newest)) if newest != stable && newest.contains('-') => {
(stable.to_string(), Some(newest.to_string()))
},
(Some(stable), _) => (stable.to_string(), None),
(None, Some(newest)) => (newest.to_string(), None),
(None, None) => return None,
};
let downloads = krate
.get(CRATES_IO_DOWNLOADS_KEY)
.and_then(serde_json::Value::as_u64)
.unwrap_or(0);
Some(CratesIoInfo {
version,
prerelease,
downloads,
})
}
#[cfg(test)]
#[allow(
clippy::expect_used,
reason = "tests should panic on unexpected values"
)]
mod crates_io_tests {
use super::crates_io_info_from_crate;
#[test]
fn stable_with_newer_prerelease_returns_both() {
let krate = serde_json::json!({
"max_stable_version": "0.20.2",
"max_version": "0.21.0-rc.2",
"downloads": 663,
});
let info = crates_io_info_from_crate(&krate).expect("info");
assert_eq!(info.version, "0.20.2");
assert_eq!(info.prerelease.as_deref(), Some("0.21.0-rc.2"));
assert_eq!(info.downloads, 663);
}
#[test]
fn stable_without_newer_prerelease_omits_prerelease() {
let krate = serde_json::json!({
"max_stable_version": "1.2.3",
"max_version": "1.2.3",
"downloads": 10,
});
let info = crates_io_info_from_crate(&krate).expect("info");
assert_eq!(info.version, "1.2.3");
assert_eq!(info.prerelease, None);
}
#[test]
fn only_prereleases_shows_newest_as_version() {
let krate = serde_json::json!({
"max_stable_version": serde_json::Value::Null,
"max_version": "0.1.0-alpha.1",
"downloads": 5,
});
let info = crates_io_info_from_crate(&krate).expect("info");
assert_eq!(info.version, "0.1.0-alpha.1");
assert_eq!(info.prerelease, None);
}
#[test]
fn no_versions_returns_none() {
let krate = serde_json::json!({ "downloads": 0 });
assert!(crates_io_info_from_crate(&krate).is_none());
}
}