crates_io_api_wasm_patch/
sync_client.rs1use super::*;
2use std::iter::Extend;
3
4use reqwest::{blocking::Client as HttpClient, header, StatusCode, Url};
5use serde::de::DeserializeOwned;
6
7use crate::{error::JsonDecodeError, types::*};
8
9pub struct SyncClient {
11 client: HttpClient,
12 base_url: Url,
13 rate_limit: std::time::Duration,
14 last_request_time: std::sync::Mutex<Option<std::time::Instant>>,
15}
16
17impl SyncClient {
18 pub fn new(
40 user_agent: &str,
41 rate_limit: std::time::Duration,
42 ) -> Result<Self, reqwest::header::InvalidHeaderValue> {
43 let mut headers = header::HeaderMap::new();
44 headers.insert(
45 header::USER_AGENT,
46 header::HeaderValue::from_str(user_agent)?,
47 );
48
49 Ok(Self {
50 client: HttpClient::builder()
51 .default_headers(headers)
52 .build()
53 .unwrap(),
54 base_url: Url::parse("https://crates.io/api/v1/").unwrap(),
55 rate_limit,
56 last_request_time: std::sync::Mutex::new(None),
57 })
58 }
59
60 fn get<T: DeserializeOwned>(&self, url: Url) -> Result<T, Error> {
61 let mut lock = self.last_request_time.lock().unwrap();
62 if let Some(last_request_time) = lock.take() {
63 let now = std::time::Instant::now();
64 if last_request_time.elapsed() < self.rate_limit {
65 std::thread::sleep((last_request_time + self.rate_limit) - now);
66 }
67 }
68
69 let time = std::time::Instant::now();
70
71 let res = self.client.get(url.clone()).send()?;
72
73 if !res.status().is_success() {
74 let err = match res.status() {
75 StatusCode::NOT_FOUND => Error::NotFound(super::error::NotFoundError {
76 url: url.to_string(),
77 }),
78 StatusCode::FORBIDDEN => {
79 let reason = res.text().unwrap_or_default();
80 Error::PermissionDenied(super::error::PermissionDeniedError { reason })
81 }
82 _ => Error::from(res.error_for_status().unwrap_err()),
83 };
84
85 return Err(err);
86 }
87
88 *lock = Some(time);
89
90 let content = res.text()?;
91
92 if let Ok(errors) = serde_json::from_str::<ApiErrors>(&content) {
95 return Err(Error::Api(errors));
96 }
97
98 let jd = &mut serde_json::Deserializer::from_str(&content);
99 serde_path_to_error::deserialize::<_, T>(jd).map_err(|err| {
100 Error::JsonDecode(JsonDecodeError {
101 message: format!("Could not decode JSON: {err} (path: {})", err.path()),
102 })
103 })
104 }
105
106 pub fn summary(&self) -> Result<Summary, Error> {
108 let url = self.base_url.join("summary").unwrap();
109 self.get(url)
110 }
111
112 pub fn get_crate(&self, crate_name: &str) -> Result<CrateResponse, Error> {
116 let url = super::util::build_crate_url(&self.base_url, crate_name)?;
117 self.get(url)
118 }
119
120 pub fn crate_downloads(&self, crate_name: &str) -> Result<CrateDownloads, Error> {
122 let url = super::util::build_crate_downloads_url(&self.base_url, crate_name)?;
123 self.get(url)
124 }
125
126 pub fn crate_owners(&self, crate_name: &str) -> Result<Vec<User>, Error> {
128 let url = super::util::build_crate_owners_url(&self.base_url, crate_name)?;
129 let resp: Owners = self.get(url)?;
130 Ok(resp.users)
131 }
132
133 pub fn crate_reverse_dependencies_page(
137 &self,
138 crate_name: &str,
139 page: u64,
140 ) -> Result<ReverseDependencies, Error> {
141 let url = super::util::build_crate_reverse_deps_url(&self.base_url, crate_name, page)?;
142 let page = self.get::<ReverseDependenciesAsReceived>(url)?;
143
144 let mut deps = ReverseDependencies {
145 dependencies: Vec::new(),
146 meta: Meta { total: 0 },
147 };
148 deps.meta.total = page.meta.total;
149 deps.extend(page);
150 Ok(deps)
151 }
152
153 pub fn crate_reverse_dependencies(
159 &self,
160 crate_name: &str,
161 ) -> Result<ReverseDependencies, Error> {
162 let mut deps = ReverseDependencies {
163 dependencies: Vec::new(),
164 meta: Meta { total: 0 },
165 };
166
167 for page_number in 1.. {
168 let page = self.crate_reverse_dependencies_page(crate_name, page_number)?;
169 if page.dependencies.is_empty() {
170 break;
171 }
172
173 deps.dependencies.extend(page.dependencies);
174 deps.meta.total = page.meta.total;
175 }
176 Ok(deps)
177 }
178
179 pub fn crate_reverse_dependency_count(&self, crate_name: &str) -> Result<u64, Error> {
181 let page = self.crate_reverse_dependencies_page(crate_name, 1)?;
182 Ok(page.meta.total)
183 }
184
185 pub fn crate_authors(&self, crate_name: &str, version: &str) -> Result<Authors, Error> {
187 let url = super::util::build_crate_authors_url(&self.base_url, crate_name, version)?;
188 let res: AuthorsResponse = self.get(url)?;
189 Ok(Authors {
190 names: res.meta.names,
191 })
192 }
193
194 pub fn crate_dependencies(
196 &self,
197 crate_name: &str,
198 version: &str,
199 ) -> Result<Vec<Dependency>, Error> {
200 let url = super::util::build_crate_dependencies_url(&self.base_url, crate_name, version)?;
201 let resp: Dependencies = self.get(url)?;
202 Ok(resp.dependencies)
203 }
204
205 fn full_version(&self, version: Version) -> Result<FullVersion, Error> {
206 let authors = self.crate_authors(&version.crate_name, &version.num)?;
207 let deps = self.crate_dependencies(&version.crate_name, &version.num)?;
208 Ok(FullVersion::from_parts(version, authors, deps))
209 }
210
211 pub fn full_crate(&self, name: &str, all_versions: bool) -> Result<FullCrate, Error> {
221 let resp = self.get_crate(name)?;
222 let data = resp.crate_data;
223
224 let dls = self.crate_downloads(name)?;
225 let owners = self.crate_owners(name)?;
226 let reverse_dependencies = self.crate_reverse_dependencies(name)?;
227
228 let versions = if resp.versions.is_empty() {
229 vec![]
230 } else if all_versions {
231 resp.versions
233 .into_iter()
234 .map(|v| self.full_version(v))
235 .collect::<Result<Vec<FullVersion>, Error>>()?
236 } else {
237 let v = self.full_version(resp.versions[0].clone())?;
238 vec![v]
239 };
240
241 let full = FullCrate {
242 id: data.id,
243 name: data.name,
244 description: data.description,
245 license: versions[0].license.clone(),
246 documentation: data.documentation,
247 homepage: data.homepage,
248 repository: data.repository,
249 total_downloads: data.downloads,
250 recent_downloads: data.recent_downloads,
251 max_version: data.max_version,
252 max_stable_version: data.max_stable_version,
253 created_at: data.created_at,
254 updated_at: data.updated_at,
255
256 categories: resp.categories,
257 keywords: resp.keywords,
258 downloads: dls,
259 owners,
260 reverse_dependencies,
261 versions,
262 };
263 Ok(full)
264 }
265
266 pub fn crates(&self, query: CratesQuery) -> Result<CratesPage, Error> {
294 let mut url = self.base_url.join("crates")?;
295 query.build(url.query_pairs_mut());
296
297 self.get(url)
298 }
299
300 pub fn user(&self, username: &str) -> Result<User, Error> {
302 let url = self.base_url.join(&format!("users/{}", username))?;
303 self.get::<UserResponse>(url).map(|response| response.user)
304 }
305}
306
307#[cfg(test)]
308mod test {
309 use super::*;
310
311 fn build_test_client() -> SyncClient {
312 SyncClient::new(
313 "crates-io-api-ci (github.com/theduke/crates-io-api)",
314 std::time::Duration::from_millis(1000),
315 )
316 .unwrap()
317 }
318
319 #[test]
320 fn test_summary() -> Result<(), Error> {
321 let client = build_test_client();
322 let summary = client.summary()?;
323 assert!(!summary.most_downloaded.is_empty());
324 assert!(!summary.just_updated.is_empty());
325 assert!(!summary.new_crates.is_empty());
326 assert!(!summary.most_recently_downloaded.is_empty());
327 assert!(summary.num_crates > 0);
328 assert!(summary.num_downloads > 0);
329 assert!(!summary.popular_categories.is_empty());
330 assert!(!summary.popular_keywords.is_empty());
331 Ok(())
332 }
333
334 #[test]
335 fn test_full_crate() -> Result<(), Error> {
336 let client = build_test_client();
337 client.full_crate("crates_io_api", false)?;
338 Ok(())
339 }
340
341 #[test]
343 fn sync_client_ensure_send() {
344 let client = build_test_client();
345 let _: &dyn Send = &client;
346 }
347
348 #[test]
349 fn test_user_get_async() -> Result<(), Error> {
350 let client = build_test_client();
351 let user = client.user("theduke")?;
352 assert_eq!(user.login, "theduke");
353 Ok(())
354 }
355
356 #[test]
357 fn test_crates_filter_by_user_async() -> Result<(), Error> {
358 let client = build_test_client();
359
360 let user = client.user("theduke")?;
361
362 let res = client.crates(CratesQuery {
363 user_id: Some(user.id),
364 per_page: 5,
365 ..Default::default()
366 })?;
367
368 assert!(!res.crates.is_empty());
369 for krate in res.crates {
371 let owners = client.crate_owners(&krate.name)?;
372 assert!(owners.iter().any(|o| o.id == user.id));
373 }
374
375 Ok(())
376 }
377
378 #[test]
379 fn test_crate_reverse_dependency_count() -> Result<(), Error> {
380 let client = build_test_client();
381 let count = client.crate_reverse_dependency_count("crates_io_api")?;
382 assert!(count > 0);
383
384 Ok(())
385 }
386}