crates_io_api_wasm_patch/
sync_client.rs

1use 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
9/// A synchronous client for the crates.io API.
10pub 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    /// Instantiate a new client.
19    ///
20    /// Returns an [`Error`] if the given user agent is invalid.
21    ///
22    /// To respect the offical [Crawler Policy](https://crates.io/policies#crawlers),
23    /// you must specify both a descriptive user agent and a rate limit interval.
24    ///
25    /// At most one request will be executed in the specified duration.
26    /// The guidelines suggest 1 per second or less.
27    ///
28    /// Example user agent: `"my_bot (my_bot.com/info)"` or `"my_bot (help@my_bot.com)"`.
29    ///
30    /// ```rust
31    /// # fn f() -> Result<(), Box<dyn std::error::Error>> {
32    /// let client = crates_io_api::AsyncClient::new(
33    ///   "my_bot (help@my_bot.com)",
34    ///   std::time::Duration::from_millis(1000),
35    /// ).unwrap();
36    /// # Ok(())
37    /// # }
38    /// ```
39    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        // First, check for api errors.
93
94        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    /// Retrieve a summary containing crates.io wide information.
107    pub fn summary(&self) -> Result<Summary, Error> {
108        let url = self.base_url.join("summary").unwrap();
109        self.get(url)
110    }
111
112    /// Retrieve information of a crate.
113    ///
114    /// If you require detailed information, consider using [full_crate]().
115    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    /// Retrieve download stats for a crate.
121    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    /// Retrieve the owners of a crate.
127    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    /// Get a single page of reverse dependencies.
134    ///
135    /// Note: if the page is 0, it is coerced to 1.
136    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    /// Load all reverse dependencies of a crate.
154    ///
155    /// Note: Since the reverse dependency endpoint requires pagination, this
156    /// will result in multiple requests if the crate has more than 100 reverse
157    /// dependencies.
158    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    /// Get the total count of reverse dependencies for a given crate.
180    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    /// Retrieve the authors for a crate version.
186    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    /// Retrieve the dependencies of a crate version.
195    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    /// Retrieve all available information for a crate, including download
212    /// stats,  owners and reverse dependencies.
213    ///
214    /// The `all_versions` argument controls the retrieval of detailed version
215    /// information.
216    /// If false, only the data for the latest version will be fetched, if true,
217    /// detailed information for all versions will be available.
218    ///
219    /// Note: Each version requires two extra requests.
220    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            //let versions_res: Result<Vec<FullVersion>> = resp.versions
232            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    /// Retrieve a page of crates, optionally constrained by a query.
267    ///
268    /// If you want to get all results without worrying about paging,
269    /// use [`all_crates`].
270    ///
271    /// # Examples
272    ///
273    /// Retrieve the first page of results for the query "api", with 100 items
274    /// per page and sorted alphabetically.
275    ///
276    /// ```rust
277    /// # use crates_io_api::{SyncClient, CratesQuery, Sort, Error};
278    ///
279    /// # fn f() -> Result<(), Box<dyn std::error::Error>> {
280    /// # let client = SyncClient::new(
281    /// #     "my-bot-name (my-contact@domain.com)",
282    /// #     std::time::Duration::from_millis(1000),
283    /// # ).unwrap();
284    /// let q = CratesQuery::builder()
285    ///   .sort(Sort::Alphabetical)
286    ///   .search("awesome")
287    ///   .build();
288    /// let crates = client.crates(q)?;
289    /// # std::mem::drop(crates);
290    /// # Ok(())
291    /// # }
292    /// ```
293    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    /// Retrieves a user by username.
301    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    /// Ensure that the sync Client remains send.
342    #[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        // Ensure all found have the searched user as owner.
370        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}