1use crate::registry::Crate;
2use anyhow::Context as _;
3use time::format_description::FormatItem;
4
5pub struct Client {
7 host: String,
8}
9
10impl Client {
11 pub fn new(host: impl ToString) -> Self {
13 Self {
14 host: host.to_string(),
15 }
16 }
17
18 pub fn cache_latest(&self, crate_name: &str) -> anyhow::Result<Crate> {
20 let Version { version, .. } = self.get_latest(crate_name)?;
21 self.cache_crate(crate_name, &version)
22 }
23
24 pub fn cache_crate(&self, crate_name: &str, crate_version: &str) -> anyhow::Result<Crate> {
26 let (yanked, data) = self.download_crate(crate_name, crate_version)?;
27 crate::util::extract_crate(&data, crate_name, crate_version).map(|path| Crate {
28 name: crate_name.to_string(),
29 version: crate_version.to_string(),
30 path,
31 yanked: yanked.into(),
32 })
33 }
34
35 pub fn get_latest(&self, crate_name: &str) -> anyhow::Result<Version> {
37 self.list_versions(crate_name)?
38 .into_iter()
39 .find(|s| !s.yanked)
40 .ok_or_else(|| anyhow::anyhow!("no available version for: {}", crate_name))
41 }
42
43 pub fn get_version(&self, crate_name: &str, semver: &str) -> anyhow::Result<Version> {
45 self.list_versions(crate_name)?
46 .into_iter()
47 .find(|s| s.version == semver)
48 .ok_or_else(|| anyhow::anyhow!("no available version for: {} = {}", crate_name, semver))
49 }
50
51 pub fn list_versions(&self, crate_name: &str) -> anyhow::Result<Vec<Version>> {
53 #[derive(serde::Deserialize)]
54 struct Resp {
55 versions: Vec<Version>,
56 }
57
58 self.fetch_json(&format!("/api/v1/crates/{}", crate_name))
59 .map(|resp: Resp| resp.versions)
60 .with_context(|| anyhow::anyhow!("list versions for: {}", crate_name))
61 }
62}
63
64impl Client {
65 fn download_crate(
66 &self,
67 crate_name: &str,
68 crate_version: &str,
69 ) -> anyhow::Result<(bool, Vec<u8>)> {
70 #[derive(Debug, serde::Deserialize)]
71 struct Resp {
72 version: Version,
73 }
74
75 let version = self
76 .fetch_json(&format!("/api/v1/crates/{}/{}", crate_name, crate_version))
77 .map(|resp: Resp| resp.version)
78 .with_context(|| anyhow::anyhow!("download crate {}/{}", crate_name, crate_version))?;
79
80 anyhow::ensure!(version.name == crate_name, "received the wrong crate");
81 anyhow::ensure!(
82 version.version == crate_version,
83 "received the wrong version"
84 );
85 anyhow::ensure!(!version.dl_path.is_empty(), "no download path available");
86
87 self.fetch_bytes(&version.dl_path)
88 .map(|data| (version.yanked, data))
89 }
90
91 fn fetch_json<T>(&self, endpoint: &str) -> anyhow::Result<T>
92 where
93 for<'de> T: serde::Deserialize<'de>,
94 {
95 let resp = attohttpc::get(format!("{}{}", self.host, endpoint))
96 .header("USER-AGENT", Self::get_user_agent())
97 .send()?;
98
99 anyhow::ensure!(
100 resp.status().is_success(),
101 "cannot fetch json for {}",
102 endpoint
103 );
104
105 resp.json()
106 .with_context(move || format!("cannot parse json from {}", endpoint))
107 }
108
109 fn fetch_bytes(&self, endpoint: &str) -> anyhow::Result<Vec<u8>> {
110 let resp = attohttpc::get(format!("{}{}", self.host, endpoint))
111 .header("USER-AGENT", Self::get_user_agent())
112 .send()?;
113
114 anyhow::ensure!(
115 resp.status().is_success(),
116 "cannot fetch bytes for {}",
117 endpoint
118 );
119
120 let len = resp
121 .headers()
122 .get("Content-Length")
123 .and_then(|s| s.to_str().ok()?.parse::<usize>().ok())
124 .with_context(|| "cannot get Content-Length")?;
125
126 let bytes = resp.bytes()?;
127 anyhow::ensure!(len == bytes.len(), "fetch size was wrong");
128
129 Ok(bytes)
130 }
131
132 const fn get_user_agent() -> &'static str {
133 concat!(
134 env!("CARGO_PKG_NAME"),
135 "/",
136 env!("CARGO_PKG_VERSION"),
137 " (",
138 env!("CARGO_PKG_REPOSITORY"),
139 ")"
140 )
141 }
142}
143
144#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
146pub struct Version {
147 #[serde(rename = "crate")]
148 pub name: String,
150 #[serde(rename(deserialize = "num"))]
151 pub version: String,
153 pub yanked: bool,
155 pub license: Option<String>,
157 #[serde(with = "time::serde::rfc3339")]
159 pub created_at: time::OffsetDateTime,
160
161 dl_path: String,
162}
163
164impl Version {
165 const FMT: &'static [FormatItem<'static>] = time::macros::format_description!(
166 "[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour sign:mandatory][offset_minute]"
167 );
168
169 pub fn format_verbose_time(&self) -> String {
170 self.created_at.format(&Self::FMT).expect("valid time")
171 }
172
173 pub fn format_approx_time_span(&self) -> String {
174 let d = time::OffsetDateTime::now_utc() - self.created_at;
175 macro_rules! try_time {
176 ($($expr:tt => $class:expr)*) => {{
177 $(
178 match d.$expr() {
179 0 => {}
180 1 => return format!("1 {} ago", $class),
181 d => return format!("{} {}s ago", d, $class),
182 }
183 )*
184 String::from("just now")
185 }};
186 }
187
188 try_time! {
189 whole_weeks => "week"
190 whole_days => "day"
191 whole_hours => "hour"
192 whole_minutes => "minute"
193 whole_seconds => "second"
194 }
195 }
196}
197
198pub mod json {
199 use crate::{features::Workspace, Version};
200
201 fn format_timestamp(time: &time::OffsetDateTime) -> String {
202 time.format(&Version::FMT).expect("valid time")
203 }
204
205 pub fn create_crates_from_versions(
206 name: &str,
207 versions: impl IntoIterator<Item = Version>,
208 ) -> serde_json::Value {
209 let array = versions.into_iter().map(|version| {
210 serde_json::json!({
211 "version": version.version,
212 "yanked": version.yanked,
213 "license": version.license,
214 "created_at": format_timestamp(&version.created_at),
215 "dl_path": version.dl_path,
216 })
217 });
218
219 serde_json::json!({
220 name: array.collect::<Vec<_>>()
221 })
222 }
223
224 pub fn create_crates_from_workspace<'a>(
225 workspace: &str,
226 crates: impl IntoIterator<Item = (&'a String, &'a String, bool)>,
227 ) -> serde_json::Value {
228 let array = crates.into_iter().map(|(name, version, published)| {
229 serde_json::json!({
230 "crate": name,
231 "version": version,
232 "published": published,
233 })
234 });
235
236 serde_json::json!({
237 workspace: array.collect::<Vec<_>>()
238 })
239 }
240
241 pub fn workspace(workspace: Workspace) -> serde_json::Value {
242 let map = workspace
243 .map
244 .into_iter()
245 .map(|(_, features)| {
246 serde_json::json!({
247 "crate": features.name,
248 "version": features.version,
249 "published": features.published,
250 "features": features.features,
251 "dependencies": {
252 "optional": features.optional_deps,
253 "required": features.required_deps,
254 }
255 })
256 })
257 .collect::<Vec<_>>();
258
259 serde_json::json!({
260 "workspace": workspace.hint,
261 "members": map
262 })
263 }
264}