use crate::registry::Crate;
use anyhow::Context as _;
use time::format_description::FormatItem;
pub struct Client {
host: String,
}
impl Client {
pub fn new(host: impl ToString) -> Self {
Self {
host: host.to_string(),
}
}
pub fn cache_latest(&self, crate_name: &str) -> anyhow::Result<Crate> {
let Version { version, .. } = self.get_latest(crate_name)?;
self.cache_crate(crate_name, &version)
}
pub fn cache_crate(&self, crate_name: &str, crate_version: &str) -> anyhow::Result<Crate> {
let (yanked, data) = self.download_crate(crate_name, crate_version)?;
crate::util::extract_crate(&data, crate_name, crate_version).map(|path| Crate {
name: crate_name.to_string(),
version: crate_version.to_string(),
path,
yanked: yanked.into(),
})
}
pub fn get_latest(&self, crate_name: &str) -> anyhow::Result<Version> {
self.list_versions(crate_name)?
.into_iter()
.find(|s| !s.yanked)
.ok_or_else(|| anyhow::anyhow!("no available version for: {}", crate_name))
}
pub fn get_version(&self, crate_name: &str, semver: &str) -> anyhow::Result<Version> {
self.list_versions(crate_name)?
.into_iter()
.find(|s| s.version == semver)
.ok_or_else(|| anyhow::anyhow!("no available version for: {} = {}", crate_name, semver))
}
pub fn list_versions(&self, crate_name: &str) -> anyhow::Result<Vec<Version>> {
#[derive(serde::Deserialize)]
struct Resp {
versions: Vec<Version>,
}
self.fetch_json(&format!("/api/v1/crates/{}", crate_name))
.map(|resp: Resp| resp.versions)
.with_context(|| anyhow::anyhow!("list versions for: {}", crate_name))
}
}
impl Client {
fn download_crate(
&self,
crate_name: &str,
crate_version: &str,
) -> anyhow::Result<(bool, Vec<u8>)> {
#[derive(Debug, serde::Deserialize)]
struct Resp {
version: Version,
}
let version = self
.fetch_json(&format!("/api/v1/crates/{}/{}", crate_name, crate_version))
.map(|resp: Resp| resp.version)
.with_context(|| anyhow::anyhow!("download crate {}/{}", crate_name, crate_version))?;
anyhow::ensure!(version.name == crate_name, "received the wrong crate");
anyhow::ensure!(
version.version == crate_version,
"received the wrong version"
);
anyhow::ensure!(!version.dl_path.is_empty(), "no download path available");
self.fetch_bytes(&version.dl_path)
.map(|data| (version.yanked, data))
}
fn fetch_json<T>(&self, endpoint: &str) -> anyhow::Result<T>
where
for<'de> T: serde::Deserialize<'de>,
{
let resp = attohttpc::get(format!("{}{}", self.host, endpoint))
.header("USER-AGENT", Self::get_user_agent())
.send()?;
anyhow::ensure!(
resp.status().is_success(),
"cannot fetch json for {}",
endpoint
);
resp.json()
.with_context(move || format!("cannot parse json from {}", endpoint))
}
fn fetch_bytes(&self, endpoint: &str) -> anyhow::Result<Vec<u8>> {
let resp = attohttpc::get(format!("{}{}", self.host, endpoint))
.header("USER-AGENT", Self::get_user_agent())
.send()?;
anyhow::ensure!(
resp.status().is_success(),
"cannot fetch bytes for {}",
endpoint
);
let len = resp
.headers()
.get("Content-Length")
.and_then(|s| s.to_str().ok()?.parse::<usize>().ok())
.with_context(|| "cannot get Content-Length")?;
let bytes = resp.bytes()?;
anyhow::ensure!(len == bytes.len(), "fetch size was wrong");
Ok(bytes)
}
const fn get_user_agent() -> &'static str {
concat!(
env!("CARGO_PKG_NAME"),
"/",
env!("CARGO_PKG_VERSION"),
" (",
env!("CARGO_PKG_REPOSITORY"),
")"
)
}
}
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
pub struct Version {
#[serde(rename = "crate")]
pub name: String,
#[serde(rename(deserialize = "num"))]
pub version: String,
pub yanked: bool,
pub license: Option<String>,
#[serde(with = "time::serde::rfc3339")]
pub created_at: time::OffsetDateTime,
dl_path: String,
}
impl Version {
const FMT: &'static [FormatItem<'static>] = time::macros::format_description!(
"[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour sign:mandatory][offset_minute]"
);
pub fn format_verbose_time(&self) -> String {
self.created_at.format(&Self::FMT).expect("valid time")
}
pub fn format_approx_time_span(&self) -> String {
let d = time::OffsetDateTime::now_utc() - self.created_at;
macro_rules! try_time {
($($expr:tt => $class:expr)*) => {{
$(
match d.$expr() {
0 => {}
1 => return format!("1 {} ago", $class),
d => return format!("{} {}s ago", d, $class),
}
)*
String::from("just now")
}};
}
try_time! {
whole_weeks => "week"
whole_days => "day"
whole_hours => "hour"
whole_minutes => "minute"
whole_seconds => "second"
}
}
}
pub mod json {
use crate::{features::Workspace, Version};
fn format_timestamp(time: &time::OffsetDateTime) -> String {
time.format(&Version::FMT).expect("valid time")
}
pub fn create_crates_from_versions(
name: &str,
versions: impl IntoIterator<Item = Version>,
) -> serde_json::Value {
let array = versions.into_iter().map(|version| {
serde_json::json!({
"version": version.version,
"yanked": version.yanked,
"license": version.license,
"created_at": format_timestamp(&version.created_at),
"dl_path": version.dl_path,
})
});
serde_json::json!({
name: array.collect::<Vec<_>>()
})
}
pub fn create_crates_from_workspace<'a>(
workspace: &str,
crates: impl IntoIterator<Item = (&'a String, &'a String, bool)>,
) -> serde_json::Value {
let array = crates.into_iter().map(|(name, version, published)| {
serde_json::json!({
"crate": name,
"version": version,
"published": published,
})
});
serde_json::json!({
workspace: array.collect::<Vec<_>>()
})
}
pub fn workspace(workspace: Workspace) -> serde_json::Value {
let map = workspace
.map
.into_iter()
.map(|(_, features)| {
serde_json::json!({
"crate": features.name,
"version": features.version,
"published": features.published,
"features": features.features,
"dependencies": {
"optional": features.optional_deps,
"required": features.required_deps,
}
})
})
.collect::<Vec<_>>();
serde_json::json!({
"workspace": workspace.hint,
"members": map
})
}
}