├── .github ├── dependabot.yml └── workflows │ └── rust.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── shell.nix └── src ├── async_client.rs ├── error.rs ├── lib.rs ├── sync_client.rs └── types.rs /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | versioning-strategy: increase-if-necessary 8 | ignore: 9 | # Ignore patch releases. 10 | - dependency-name: "*" 11 | update-types: ["version-update:semver-patch"] 12 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | 8 | # Automatically cancel previous workflow runs when a new commit is pushed. 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | rust-channel: [stable, nightly] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | 24 | - name: Install Rust 25 | uses: dtolnay/rust-toolchain@stable 26 | 27 | - name: Check formatting 28 | run: cargo fmt --check 29 | 30 | - name: Clippy lints 31 | run: cargo clippy -- --deny warnings 32 | 33 | - name: Test 34 | run: cargo test --verbose 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | **/*.rs.bk 3 | Cargo.lock 4 | .idea 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.11.0 - 2024-04-12 4 | 5 | * Upgrade to reqwest 0.12, which uses hyper 1.0 6 | 7 | ## 0.9.0 - 2023-12-04 8 | 9 | * Add `Version::audit_actions` 10 | [PR #69(https://github.com/theduke/crates-io-api/pull/69) by @kornelski 11 | 12 | 13 | ## 0.8.2 - 2023-10-28 14 | 15 | * Fix typo in `Display` implementation (#59) 16 | * Add .page() method to crate query builder (#61) 17 | * Add `recent_downloads` field to FullCrate (#63) 18 | * Derive more traits for `Author` (#64) 19 | * String representaton for Sort::Relevance (#65) 20 | * Add rust_version field to Version (#67) 21 | 22 | ## 0.8.1 - 2022-09-05 23 | 24 | * Add `AsyncClient::with_http_client` constructor 25 | * Add `Crate::max_stable_version` field 26 | * Improve error reporting for JSON decoding errors 27 | * Deprecate the `Crate::license field` 28 | (field is unused and always empty) 29 | 30 | ## 0.8.0 - 2022-01-29 31 | 32 | This version has quite a few breaking changes, 33 | mainly to clean up and future-proof the API. 34 | 35 | ### Features 36 | 37 | * Get user data with `Client::user()` 38 | * Filter crates by category 39 | * Filter crates by user_id 40 | * Add `reverse_dependency_count()` to easily get the number of reverse deps 41 | * Allow retrieving single reverse dependency pages (`crate_reverse_dependencies_page`) 42 | * (async): Add a paginated Stream for listing crates (`AsyncClient::crates_stream()`) 43 | 44 | ### (Breaking) Changes 45 | 46 | * Error 47 | - make Error #[non_exhaustive] 48 | - add Error::Api variant 49 | - Rename NotFound => NotFoundError 50 | - Rename PermissionDenied => PermissionDeniedError 51 | - Remove InvalidHeaders variant (only relevant for client construction) 52 | 53 | * Types 54 | - Rename `CratesResponse` => `CratesPage` 55 | - Rename `DownloadsMeta` => `CrateDownloadsMeta` 56 | - Rename `Downloads` => `CrateDownloads` 57 | - Don't expose internal types (`AuthorsResponse`) 58 | - Remove unused `Authors`/`FullVersion`::users field 59 | 60 | * General 61 | - Properly handle API errors (Error::Api variant) 62 | 63 | * Querying 64 | - rename `ListOptions` to `CratesQuery` 65 | - make `CratesQuery` fields private (future proofing) 66 | - add `CratesQueryBuilder` for query construction 67 | 68 | ### Sync Client 69 | 70 | * Remove `all_crates` method, which should never have been there... 71 | 72 | ### Async Client 73 | 74 | * Clean up the old pre-async futures code 75 | * Don't auto-clone: futures are now tied to the client lifetime. 76 | Manually clone if you need the futures to be owned. 77 | 78 | 79 | ## 0.7.3 - 2021-10-26 80 | 81 | * Fix sort by relevance (https://github.com/theduke/crates_io_api/pull/35) 82 | * Provide rustls option via feature flag (https://github.com/theduke/crates_io_api/pull/34) 83 | 84 | ## 0.7.2 - 2021-07-05 85 | 86 | * Disable default features of chrono to have fewer dependencies. 87 | 88 | ## 0.7.1 - 2021-05-18 89 | 90 | * Deprecate the `VersionLinks.authors` field that was removed from the API 91 | Now will always be empty. 92 | Field will be removed in 0.8. 93 | 94 | ## 0.6.1 - 2020-07-19 95 | 96 | * Make `SyncClient` `Send + Sync` [#22](https://github.com/theduke/crates_io_api/pull/22) 97 | 98 | ## 0.6.0 - 2020-05-25 99 | 100 | * Upgrade the async client to Futures 0.3 + reqwest 0.10 101 | (reqwest 0.10 also respects standard http_proxy env variables) 102 | * Removed `failure` dependency 103 | * Adhere to the crawler policy by requiring a custom user agent 104 | * Add a *simple* rate limiter that restricts a client to one request in a given 105 | duration, and only a single concurrent request. 106 | 107 | ## 0.5.1 - 2019-08-23 108 | 109 | * Fix faulty condition check in SyncClient::all_crates 110 | 111 | ## 0.5.0 - 2019/06/22 112 | 113 | * Add 7 missing type fields for: 114 | * Crate {recent_downloads, exact_match} 115 | * CrateResponse {versions, keywords, categories} 116 | * Version {crate_size, published_by} 117 | * Make field optional: User {kind} 118 | * Fix getting the reverse dependencies. 119 | * Rearrange the received data for simpler manipulation. 120 | * Add 3 new types: 121 | * ReverseDependenciesAsReceived {dependencies, versions, meta} 122 | * ReverseDependencies {dependencies, meta} 123 | * ReverseDependency {crate_version, dependency} 124 | 125 | ## 0.4.1 - 2019/03/09 126 | 127 | * Fixed errors for version information due to the `id` field being removed from the API. [PR #11](https://github.com/theduke/crates_io_api/pull/11) 128 | 129 | ## 0.4.0 - 2019/03/01 130 | 131 | * Added `with_user_agent` method to client 132 | * Switch to 2018 edition, requiring rustc 1.31+ 133 | 134 | ## 0.3.0 - 2018/10/09 135 | 136 | * Upgrade reqwest to 0.9 137 | * Upgrade to tokio instead of tokio_core 138 | 139 | ## 0.2.0 - 2018/04/29 140 | 141 | * Add AsyncClient 142 | * Switch from error_chain to failure 143 | * Remove unused time dependency and loosen dependency constraints 144 | 145 | ## 0.1.0 - 2018/02/10 146 | 147 | * Add some newly introduced fields in the API 148 | * Fix URL for the /summary endpoint 149 | * Upgrade dependencies 150 | * Add a simple test 151 | 152 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["theduke "] 3 | name = "crates_io_api" 4 | description = "API client for crates.io" 5 | license = "MIT/Apache-2.0" 6 | repository = "https://github.com/theduke/crates-io-api" 7 | documentation = "https://docs.rs/crates_io_api" 8 | readme = "README.md" 9 | keywords = [ "crates", "api" ] 10 | categories = [ "web-programming", "web-programming::http-client" ] 11 | edition = "2018" 12 | 13 | version = "0.11.0" 14 | 15 | [dependencies] 16 | chrono = { version = "0.4.6", default-features = false, features = ["serde"] } 17 | reqwest = { version = "0.12", default-features = false, features = ["blocking", "json"] } 18 | serde = "1.0.79" 19 | serde_derive = "1.0.79" 20 | serde_json = "1.0.32" 21 | url = "2.1.0" 22 | futures = "0.3.4" 23 | tokio = { version = "1.0.1", default-features = false, features = ["sync", "time"] } 24 | serde_path_to_error = "0.1.8" 25 | 26 | [dev-dependencies] 27 | tokio = { version = "1.0.1", features = ["macros"]} 28 | 29 | [features] 30 | default = ["reqwest/default-tls"] 31 | rustls = ["reqwest/rustls-tls"] 32 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2017 Christoph Herzog 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Christoph Herzog 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # crates-io-api 2 | 3 | [![Crate][cratesioimg]][cratesio] 4 | [![API Docs][docsrsimg]][docsrs] 5 | 6 | [cratesio]: https://crates.io/crates/crates_io_api 7 | [cratesioimg]: https://img.shields.io/crates/v/crates_io_api.svg 8 | [docsrs]: https://docs.rs/crates_io_api 9 | [docsrsimg]: https://img.shields.io/badge/current-docs-brightgreen.svg 10 | [crawlerpolicy]: https://crates.io/policies#crawlers 11 | [reqwest]: https://github.com/seanmonstar/reqwest 12 | 13 | A Rust API client for the [crates.io](https://crates.io) API. 14 | 15 | This crate aims to provide an easy to use and complete client for retrieving 16 | detailed information about Rusts crate ecosystem. 17 | 18 | The library uses the [reqwest][reqwest] HTTP client and provides both an async 19 | and synchronous interface. 20 | 21 | Please consult the official [Crawler Policy][crawlerpolicy] before using this 22 | library. 23 | A rate limiter is included and enabled by default. 24 | 25 | ## Usage 26 | 27 | For usage information and examples, check out the [Documentation][docsrs]. 28 | 29 | ### rustls 30 | 31 | By default the system TLS implementation is used. 32 | 33 | You can also use [rustls](https://github.com/rustls/rustls). 34 | 35 | `Cargo.toml:` 36 | ``` 37 | [dependencies] 38 | crates_io_api = { version = "?", default-features = false, features = ["rustls"] } 39 | ``` 40 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import {} }: 2 | 3 | pkgs.mkShell { 4 | buildInputs = with pkgs; [ 5 | pkg-config 6 | openssl 7 | ]; 8 | LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [ 9 | pkgs.openssl 10 | ]; 11 | } 12 | -------------------------------------------------------------------------------- /src/async_client.rs: -------------------------------------------------------------------------------- 1 | use futures::future::BoxFuture; 2 | use futures::prelude::*; 3 | use futures::{future::try_join_all, try_join}; 4 | use reqwest::{header, Client as HttpClient, StatusCode, Url}; 5 | use serde::de::DeserializeOwned; 6 | 7 | use std::collections::VecDeque; 8 | 9 | use super::Error; 10 | use crate::error::JsonDecodeError; 11 | use crate::types::*; 12 | 13 | /// Asynchronous client for the crates.io API. 14 | #[derive(Clone)] 15 | pub struct Client { 16 | client: HttpClient, 17 | rate_limit: std::time::Duration, 18 | last_request_time: std::sync::Arc>>, 19 | base_url: Url, 20 | } 21 | 22 | pub struct CrateStream { 23 | client: Client, 24 | filter: CratesQuery, 25 | 26 | closed: bool, 27 | items: VecDeque, 28 | next_page_fetch: Option>>, 29 | } 30 | 31 | impl CrateStream { 32 | fn new(client: Client, filter: CratesQuery) -> Self { 33 | Self { 34 | client, 35 | filter, 36 | closed: false, 37 | items: VecDeque::new(), 38 | next_page_fetch: None, 39 | } 40 | } 41 | } 42 | 43 | impl futures::stream::Stream for CrateStream { 44 | type Item = Result; 45 | 46 | fn poll_next( 47 | self: std::pin::Pin<&mut Self>, 48 | cx: &mut std::task::Context<'_>, 49 | ) -> std::task::Poll> { 50 | let inner = self.get_mut(); 51 | 52 | if inner.closed { 53 | return std::task::Poll::Ready(None); 54 | } 55 | 56 | if let Some(krate) = inner.items.pop_front() { 57 | return std::task::Poll::Ready(Some(Ok(krate))); 58 | } 59 | 60 | if let Some(mut fut) = inner.next_page_fetch.take() { 61 | return match fut.poll_unpin(cx) { 62 | std::task::Poll::Ready(res) => match res { 63 | Ok(page) if page.crates.is_empty() => { 64 | inner.closed = true; 65 | std::task::Poll::Ready(None) 66 | } 67 | Ok(page) => { 68 | let mut iter = page.crates.into_iter(); 69 | let next = iter.next(); 70 | inner.items.extend(iter); 71 | 72 | std::task::Poll::Ready(next.map(Ok)) 73 | } 74 | Err(err) => { 75 | inner.closed = true; 76 | std::task::Poll::Ready(Some(Err(err))) 77 | } 78 | }, 79 | std::task::Poll::Pending => { 80 | inner.next_page_fetch = Some(fut); 81 | std::task::Poll::Pending 82 | } 83 | }; 84 | } 85 | 86 | let filter = inner.filter.clone(); 87 | inner.filter.page += 1; 88 | 89 | let c = inner.client.clone(); 90 | let mut f = Box::pin(async move { c.crates(filter).await }); 91 | assert!(matches!(f.poll_unpin(cx), std::task::Poll::Pending)); 92 | inner.next_page_fetch = Some(f); 93 | 94 | cx.waker().wake_by_ref(); 95 | 96 | std::task::Poll::Pending 97 | } 98 | } 99 | 100 | impl Client { 101 | /// Instantiate a new client. 102 | /// 103 | /// Returns an [`Error`] if the given user agent is invalid. 104 | /// 105 | /// To respect the offical [Crawler Policy](https://crates.io/policies#crawlers), 106 | /// you must specify both a descriptive user agent and a rate limit interval. 107 | /// 108 | /// At most one request will be executed in the specified duration. 109 | /// The guidelines suggest 1 per second or less. 110 | /// (Only one request is executed concurrenly, even if the given Duration is 0). 111 | /// 112 | /// Example user agent: `"my_bot (my_bot.com/info)"` or `"my_bot (help@my_bot.com)"`. 113 | /// 114 | /// ```rust 115 | /// # fn f() -> Result<(), Box> { 116 | /// let client = crates_io_api::AsyncClient::new( 117 | /// "my_bot (help@my_bot.com)", 118 | /// std::time::Duration::from_millis(1000), 119 | /// ).unwrap(); 120 | /// # Ok(()) 121 | /// # } 122 | /// ``` 123 | pub fn new( 124 | user_agent: &str, 125 | rate_limit: std::time::Duration, 126 | ) -> Result { 127 | let mut headers = header::HeaderMap::new(); 128 | headers.insert( 129 | header::USER_AGENT, 130 | header::HeaderValue::from_str(user_agent)?, 131 | ); 132 | 133 | let client = HttpClient::builder() 134 | .default_headers(headers) 135 | .build() 136 | .unwrap(); 137 | 138 | Ok(Self::with_http_client(client, rate_limit)) 139 | } 140 | 141 | /// Instantiate a new client. 142 | /// 143 | /// To respect the offical [Crawler Policy](https://crates.io/policies#crawlers), 144 | /// you must specify both a descriptive user agent and a rate limit interval. 145 | /// 146 | /// At most one request will be executed in the specified duration. 147 | /// The guidelines suggest 1 per second or less. 148 | /// (Only one request is executed concurrenly, even if the given Duration is 0). 149 | pub fn with_http_client(client: HttpClient, rate_limit: std::time::Duration) -> Self { 150 | let limiter = std::sync::Arc::new(tokio::sync::Mutex::new(None)); 151 | 152 | Self { 153 | rate_limit, 154 | last_request_time: limiter, 155 | client, 156 | base_url: Url::parse("https://crates.io/api/v1/").unwrap(), 157 | } 158 | } 159 | 160 | async fn get(&self, url: &Url) -> Result { 161 | let mut lock = self.last_request_time.clone().lock_owned().await; 162 | 163 | if let Some(last_request_time) = lock.take() { 164 | if last_request_time.elapsed() < self.rate_limit { 165 | tokio::time::sleep(self.rate_limit - last_request_time.elapsed()).await; 166 | } 167 | } 168 | 169 | let time = tokio::time::Instant::now(); 170 | let res = self.client.get(url.clone()).send().await?; 171 | 172 | if !res.status().is_success() { 173 | let err = match res.status() { 174 | StatusCode::NOT_FOUND => Error::NotFound(super::error::NotFoundError { 175 | url: url.to_string(), 176 | }), 177 | StatusCode::FORBIDDEN => { 178 | let reason = res.text().await.unwrap_or_default(); 179 | Error::PermissionDenied(super::error::PermissionDeniedError { reason }) 180 | } 181 | _ => Error::from(res.error_for_status().unwrap_err()), 182 | }; 183 | 184 | return Err(err); 185 | } 186 | 187 | let content = res.text().await?; 188 | 189 | // Free up the lock 190 | (*lock) = Some(time); 191 | 192 | // First, check for api errors. 193 | 194 | if let Ok(errors) = serde_json::from_str::(&content) { 195 | return Err(Error::Api(errors)); 196 | } 197 | 198 | let jd = &mut serde_json::Deserializer::from_str(&content); 199 | serde_path_to_error::deserialize::<_, T>(jd).map_err(|err| { 200 | Error::JsonDecode(JsonDecodeError { 201 | message: format!("Could not decode JSON: {err} (path: {})", err.path()), 202 | }) 203 | }) 204 | } 205 | 206 | /// Retrieve a summary containing crates.io wide information. 207 | pub async fn summary(&self) -> Result { 208 | let url = self.base_url.join("summary").unwrap(); 209 | self.get(&url).await 210 | } 211 | 212 | /// Retrieve information of a crate. 213 | /// 214 | /// If you require detailed information, consider using [full_crate](). 215 | pub async fn get_crate(&self, crate_name: &str) -> Result { 216 | let url = build_crate_url(&self.base_url, crate_name)?; 217 | 218 | self.get(&url).await 219 | } 220 | 221 | /// Retrieve download stats for a crate. 222 | pub async fn crate_downloads(&self, crate_name: &str) -> Result { 223 | let url = build_crate_downloads_url(&self.base_url, crate_name)?; 224 | self.get(&url).await 225 | } 226 | 227 | /// Retrieve the owners of a crate. 228 | pub async fn crate_owners(&self, name: &str) -> Result, Error> { 229 | let url = build_crate_owners_url(&self.base_url, name)?; 230 | self.get::(&url).await.map(|data| data.users) 231 | } 232 | 233 | /// Get a single page of reverse dependencies. 234 | /// 235 | /// Note: if the page is 0, it is coerced to 1. 236 | pub async fn crate_reverse_dependencies_page( 237 | &self, 238 | crate_name: &str, 239 | page: u64, 240 | ) -> Result { 241 | // If page is zero, bump it to 1. 242 | let page = page.max(1); 243 | 244 | let url = build_crate_reverse_deps_url(&self.base_url, crate_name, page)?; 245 | let page = self.get::(&url).await?; 246 | 247 | let mut deps = ReverseDependencies { 248 | dependencies: Vec::new(), 249 | meta: Meta { total: 0 }, 250 | }; 251 | deps.meta.total = page.meta.total; 252 | deps.extend(page); 253 | Ok(deps) 254 | } 255 | 256 | /// Load all reverse dependencies of a crate. 257 | /// 258 | /// Note: Since the reverse dependency endpoint requires pagination, this 259 | /// will result in multiple requests if the crate has more than 100 reverse 260 | /// dependencies. 261 | pub async fn crate_reverse_dependencies( 262 | &self, 263 | crate_name: &str, 264 | ) -> Result { 265 | let mut deps = ReverseDependencies { 266 | dependencies: Vec::new(), 267 | meta: Meta { total: 0 }, 268 | }; 269 | 270 | for page_number in 1.. { 271 | let page = self 272 | .crate_reverse_dependencies_page(crate_name, page_number) 273 | .await?; 274 | if page.dependencies.is_empty() { 275 | break; 276 | } 277 | deps.dependencies.extend(page.dependencies); 278 | deps.meta.total = page.meta.total; 279 | } 280 | 281 | Ok(deps) 282 | } 283 | 284 | /// Get the total count of reverse dependencies for a given crate. 285 | pub async fn crate_reverse_dependency_count(&self, crate_name: &str) -> Result { 286 | let page = self.crate_reverse_dependencies_page(crate_name, 1).await?; 287 | Ok(page.meta.total) 288 | } 289 | 290 | /// Retrieve the authors for a crate version. 291 | pub async fn crate_authors(&self, crate_name: &str, version: &str) -> Result { 292 | let url = build_crate_authors_url(&self.base_url, crate_name, version)?; 293 | self.get::(&url).await.map(|res| Authors { 294 | names: res.meta.names, 295 | }) 296 | } 297 | 298 | /// Retrieve the dependencies of a crate version. 299 | pub async fn crate_dependencies( 300 | &self, 301 | crate_name: &str, 302 | version: &str, 303 | ) -> Result, Error> { 304 | let url = build_crate_dependencies_url(&self.base_url, crate_name, version)?; 305 | self.get::(&url) 306 | .await 307 | .map(|res| res.dependencies) 308 | } 309 | 310 | async fn full_version(&self, version: Version) -> Result { 311 | let authors_fut = self.crate_authors(&version.crate_name, &version.num); 312 | let deps_fut = self.crate_dependencies(&version.crate_name, &version.num); 313 | try_join!(authors_fut, deps_fut) 314 | .map(|(authors, deps)| FullVersion::from_parts(version, authors, deps)) 315 | } 316 | 317 | /// Retrieve all available information for a crate, including download 318 | /// stats, owners and reverse dependencies. 319 | /// 320 | /// The `all_versions` argument controls the retrieval of detailed version 321 | /// information. 322 | /// If false, only the data for the latest version will be fetched, if true, 323 | /// detailed information for all versions will be available. 324 | /// Note: Each version requires two extra requests. 325 | pub async fn full_crate(&self, name: &str, all_versions: bool) -> Result { 326 | let krate = self.get_crate(name).await?; 327 | let versions = if !all_versions { 328 | self.full_version(krate.versions[0].clone()) 329 | .await 330 | .map(|v| vec![v]) 331 | } else { 332 | try_join_all( 333 | krate 334 | .versions 335 | .clone() 336 | .into_iter() 337 | .map(|v| self.full_version(v)), 338 | ) 339 | .await 340 | }?; 341 | let dls_fut = self.crate_downloads(name); 342 | let owners_fut = self.crate_owners(name); 343 | let reverse_dependencies_fut = self.crate_reverse_dependencies(name); 344 | try_join!(dls_fut, owners_fut, reverse_dependencies_fut).map( 345 | |(dls, owners, reverse_dependencies)| { 346 | let data = krate.crate_data; 347 | FullCrate { 348 | id: data.id, 349 | name: data.name, 350 | description: data.description, 351 | license: krate.versions[0].license.clone(), 352 | documentation: data.documentation, 353 | homepage: data.homepage, 354 | repository: data.repository, 355 | total_downloads: data.downloads, 356 | recent_downloads: data.recent_downloads, 357 | max_version: data.max_version, 358 | max_stable_version: data.max_stable_version, 359 | created_at: data.created_at, 360 | updated_at: data.updated_at, 361 | categories: krate.categories, 362 | keywords: krate.keywords, 363 | downloads: dls, 364 | owners, 365 | reverse_dependencies, 366 | versions, 367 | } 368 | }, 369 | ) 370 | } 371 | 372 | /// Retrieve a page of crates, optionally constrained by a query. 373 | /// 374 | /// If you want to get all results without worrying about paging, 375 | /// use [`all_crates`]. 376 | pub async fn crates(&self, query: CratesQuery) -> Result { 377 | let mut url = self.base_url.join("crates").unwrap(); 378 | query.build(url.query_pairs_mut()); 379 | self.get(&url).await 380 | } 381 | 382 | /// Get a stream over all crates matching the given [`CratesQuery`]. 383 | pub fn crates_stream(&self, filter: CratesQuery) -> CrateStream { 384 | CrateStream::new(self.clone(), filter) 385 | } 386 | 387 | /// Retrieves a user by username. 388 | pub async fn user(&self, username: &str) -> Result { 389 | let url = self.base_url.join(&format!("users/{}", username)).unwrap(); 390 | self.get::(&url).await.map(|res| res.user) 391 | } 392 | } 393 | 394 | pub(crate) fn build_crate_url(base: &Url, crate_name: &str) -> Result { 395 | let mut url = base.join("crates")?; 396 | url.path_segments_mut().unwrap().push(crate_name); 397 | 398 | // Guard against slashes in the crate name. 399 | // The API returns a nonsensical error in this case. 400 | if crate_name.contains('/') { 401 | Err(Error::NotFound(crate::error::NotFoundError { 402 | url: url.to_string(), 403 | })) 404 | } else { 405 | Ok(url) 406 | } 407 | } 408 | 409 | fn build_crate_url_nested(base: &Url, crate_name: &str) -> Result { 410 | let mut url = base.join("crates")?; 411 | url.path_segments_mut().unwrap().push(crate_name).push("/"); 412 | 413 | // Guard against slashes in the crate name. 414 | // The API returns a nonsensical error in this case. 415 | if crate_name.contains('/') { 416 | Err(Error::NotFound(crate::error::NotFoundError { 417 | url: url.to_string(), 418 | })) 419 | } else { 420 | Ok(url) 421 | } 422 | } 423 | 424 | pub(crate) fn build_crate_downloads_url(base: &Url, crate_name: &str) -> Result { 425 | build_crate_url_nested(base, crate_name)? 426 | .join("downloads") 427 | .map_err(Error::from) 428 | } 429 | 430 | pub(crate) fn build_crate_owners_url(base: &Url, crate_name: &str) -> Result { 431 | build_crate_url_nested(base, crate_name)? 432 | .join("owners") 433 | .map_err(Error::from) 434 | } 435 | 436 | pub(crate) fn build_crate_reverse_deps_url( 437 | base: &Url, 438 | crate_name: &str, 439 | page: u64, 440 | ) -> Result { 441 | build_crate_url_nested(base, crate_name)? 442 | .join(&format!("reverse_dependencies?per_page=100&page={page}")) 443 | .map_err(Error::from) 444 | } 445 | 446 | pub(crate) fn build_crate_authors_url( 447 | base: &Url, 448 | crate_name: &str, 449 | version: &str, 450 | ) -> Result { 451 | build_crate_url_nested(base, crate_name)? 452 | .join(&format!("{version}/authors")) 453 | .map_err(Error::from) 454 | } 455 | 456 | pub(crate) fn build_crate_dependencies_url( 457 | base: &Url, 458 | crate_name: &str, 459 | version: &str, 460 | ) -> Result { 461 | build_crate_url_nested(base, crate_name)? 462 | .join(&format!("{version}/dependencies")) 463 | .map_err(Error::from) 464 | } 465 | 466 | #[cfg(test)] 467 | mod test { 468 | use super::*; 469 | 470 | fn build_test_client() -> Client { 471 | Client::new( 472 | "crates-io-api-continuous-integration (github.com/theduke/crates-io-api)", 473 | std::time::Duration::from_millis(1000), 474 | ) 475 | .unwrap() 476 | } 477 | 478 | #[tokio::test] 479 | async fn test_summary_async() -> Result<(), Error> { 480 | let client = build_test_client(); 481 | let summary = client.summary().await?; 482 | assert!(!summary.most_downloaded.is_empty()); 483 | assert!(!summary.just_updated.is_empty()); 484 | assert!(!summary.new_crates.is_empty()); 485 | assert!(!summary.most_recently_downloaded.is_empty()); 486 | assert!(summary.num_crates > 0); 487 | assert!(summary.num_downloads > 0); 488 | assert!(!summary.popular_categories.is_empty()); 489 | assert!(!summary.popular_keywords.is_empty()); 490 | Ok(()) 491 | } 492 | 493 | #[tokio::test] 494 | async fn test_crates_stream_async() { 495 | let client = build_test_client(); 496 | 497 | let mut stream = client.crates_stream(CratesQuery { 498 | per_page: 10, 499 | ..Default::default() 500 | }); 501 | 502 | for _ in 0..40 { 503 | let _krate = stream.next().await.unwrap().unwrap(); 504 | eprintln!("CRATE {}", _krate.name); 505 | } 506 | } 507 | 508 | #[tokio::test] 509 | async fn test_full_crate_async() -> Result<(), Error> { 510 | let client = build_test_client(); 511 | client.full_crate("crates_io_api", false).await?; 512 | 513 | Ok(()) 514 | } 515 | 516 | #[tokio::test] 517 | async fn test_user_get_async() -> Result<(), Error> { 518 | let client = build_test_client(); 519 | let user = client.user("theduke").await?; 520 | assert_eq!(user.login, "theduke"); 521 | Ok(()) 522 | } 523 | 524 | #[tokio::test] 525 | async fn test_crates_filter_by_user_async() -> Result<(), Error> { 526 | let client = build_test_client(); 527 | 528 | let user = client.user("theduke").await?; 529 | 530 | let res = client 531 | .crates(CratesQuery { 532 | user_id: Some(user.id), 533 | per_page: 20, 534 | ..Default::default() 535 | }) 536 | .await?; 537 | 538 | assert!(!res.crates.is_empty()); 539 | // Ensure all found have the searched user as owner. 540 | for krate in res.crates { 541 | let owners = client.crate_owners(&krate.name).await?; 542 | assert!(owners.iter().any(|o| o.id == user.id)); 543 | } 544 | 545 | Ok(()) 546 | } 547 | 548 | #[tokio::test] 549 | async fn test_crates_filter_by_category_async() -> Result<(), Error> { 550 | let client = build_test_client(); 551 | 552 | let category = "wasm".to_string(); 553 | 554 | let res = client 555 | .crates(CratesQuery { 556 | category: Some(category.clone()), 557 | per_page: 3, 558 | ..Default::default() 559 | }) 560 | .await?; 561 | 562 | assert!(!res.crates.is_empty()); 563 | // Ensure all found crates have the given category. 564 | for list_crate in res.crates { 565 | let krate = client.get_crate(&list_crate.name).await?; 566 | assert!(krate.categories.iter().any(|c| c.id == category)); 567 | } 568 | 569 | Ok(()) 570 | } 571 | 572 | #[tokio::test] 573 | async fn test_crates_filter_by_ids_async() -> Result<(), Error> { 574 | let client = build_test_client(); 575 | 576 | let ids = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"] 577 | .map(Into::into) 578 | .to_vec(); 579 | let res = client 580 | .crates(CratesQuery { 581 | ids: Some(ids), 582 | per_page: 10, 583 | ..Default::default() 584 | }) 585 | .await?; 586 | 587 | assert_eq!( 588 | res.crates.len(), 589 | 10, 590 | "Expected 10 crates, actually got {}. Crates: {:#?}", 591 | res.crates.len(), 592 | res.crates 593 | ); 594 | Ok(()) 595 | } 596 | 597 | #[tokio::test] 598 | async fn test_crate_reverse_dependency_count_async() -> Result<(), Error> { 599 | let client = build_test_client(); 600 | let count = client 601 | .crate_reverse_dependency_count("crates_io_api") 602 | .await?; 603 | assert!(count > 0); 604 | 605 | Ok(()) 606 | } 607 | 608 | /// Regression test for https://github.com/theduke/crates-io-api/issues/44 609 | #[tokio::test] 610 | async fn test_get_crate_with_slash() { 611 | let client = build_test_client(); 612 | match client.get_crate("a/b").await { 613 | Err(Error::NotFound(_)) => {} 614 | other => { 615 | panic!("Invalid response: expected NotFound error, got {:?}", other); 616 | } 617 | } 618 | } 619 | } 620 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | //! Error types. 2 | 3 | /// Errors returned by the api client. 4 | #[derive(Debug)] 5 | #[non_exhaustive] 6 | pub enum Error { 7 | /// Low-level http error. 8 | Http(reqwest::Error), 9 | /// Invalid URL. 10 | Url(url::ParseError), 11 | /// Crate could not be found. 12 | NotFound(NotFoundError), 13 | /// No permission to access the resource. 14 | PermissionDenied(PermissionDeniedError), 15 | /// JSON decoding of API response failed. 16 | JsonDecode(JsonDecodeError), 17 | /// Error returned by the crates.io API directly. 18 | Api(crate::types::ApiErrors), 19 | } 20 | 21 | impl std::fmt::Display for Error { 22 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 23 | match self { 24 | Error::Http(e) => e.fmt(f), 25 | Error::Url(e) => e.fmt(f), 26 | Error::NotFound(e) => e.fmt(f), 27 | Error::PermissionDenied(e) => e.fmt(f), 28 | Error::Api(err) => { 29 | let inner = if err.errors.is_empty() { 30 | "Unknown API error".to_string() 31 | } else { 32 | err.errors 33 | .iter() 34 | .map(|err| err.to_string()) 35 | .collect::>() 36 | .join(", ") 37 | }; 38 | 39 | write!(f, "API Error ({})", inner) 40 | } 41 | Error::JsonDecode(err) => write!(f, "Could not decode API JSON response: {err}"), 42 | } 43 | } 44 | } 45 | 46 | impl std::error::Error for Error { 47 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 48 | match self { 49 | Error::Http(e) => Some(e), 50 | Error::Url(e) => Some(e), 51 | Error::NotFound(_) => None, 52 | Error::PermissionDenied(_) => None, 53 | Error::Api(_) => None, 54 | Error::JsonDecode(err) => Some(err), 55 | } 56 | } 57 | 58 | // TODO: uncomment once backtrace feature is stabilized (https://github.com/rust-lang/rust/issues/53487). 59 | /* 60 | fn backtrace(&self) -> Option<&std::backtrace::Backtrace> { 61 | match self { 62 | Self::Http(e) => e.backtrace(), 63 | Self::Url(e) => e.backtrace(), 64 | Self::InvalidHeader(e) => e.backtrace(), 65 | Self::NotFound(_) => None, 66 | } 67 | } 68 | */ 69 | } 70 | 71 | impl From for Error { 72 | fn from(e: reqwest::Error) -> Self { 73 | Error::Http(e) 74 | } 75 | } 76 | 77 | impl From for Error { 78 | fn from(e: url::ParseError) -> Self { 79 | Error::Url(e) 80 | } 81 | } 82 | 83 | /// Error returned when the JSON returned by the API could not be decoded. 84 | #[derive(Debug)] 85 | pub struct JsonDecodeError { 86 | pub(crate) message: String, 87 | } 88 | 89 | impl std::fmt::Display for JsonDecodeError { 90 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 91 | write!(f, "Could not decode JSON: {}", self.message) 92 | } 93 | } 94 | 95 | impl std::error::Error for JsonDecodeError {} 96 | 97 | /// Error returned when a resource could not be found. 98 | #[derive(Debug)] 99 | pub struct NotFoundError { 100 | pub(crate) url: String, 101 | } 102 | 103 | impl std::fmt::Display for NotFoundError { 104 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 105 | write!(f, "Resource at url '{}' could not be found", self.url) 106 | } 107 | } 108 | 109 | /// Error returned when a resource is not accessible. 110 | #[derive(Debug)] 111 | pub struct PermissionDeniedError { 112 | pub(crate) reason: String, 113 | } 114 | 115 | impl std::fmt::Display for PermissionDeniedError { 116 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 117 | write!(f, "Permission denied: {}", self.reason) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! API client for [crates.io](https://crates.io). 2 | //! 3 | //! It aims to provide an easy to use and complete client for retrieving 4 | //! information about Rust's crate ecosystem. 5 | //! 6 | //! Both a [AsyncClient](struct.AsyncClient.html) and a [SyncClient](struct.SyncClient.html) are available, providing either a 7 | //! Futures based or a blocking interface. 8 | //! 9 | //! Please read the official crates.io [Crawler Policy](https://crates.io/policies#crawlers) 10 | //! before using this crate. 11 | //! 12 | //! Due to this policy, you must specify both a user agent and a desired 13 | //! rate limit delay when constructing a client. 14 | //! See [SyncClient::new](struct.SyncClient.html#method.new) and [AsyncClient::new](struct.AsyncClient.html#method.new) for more information. 15 | //! 16 | //! # Examples 17 | //! 18 | //! Print the most downloaded crates and their non-optional dependencies: 19 | //! 20 | //! ```rust 21 | //! use crates_io_api::{SyncClient, Error}; 22 | //! 23 | //! fn list_top_dependencies() -> Result<(), Error> { 24 | //! // Instantiate the client. 25 | //! let client = SyncClient::new( 26 | //! "my-user-agent (my-contact@domain.com)", 27 | //! std::time::Duration::from_millis(1000), 28 | //! ).unwrap(); 29 | //! // Retrieve summary data. 30 | //! let summary = client.summary()?; 31 | //! for c in summary.most_downloaded { 32 | //! println!("{}:", c.id); 33 | //! for dep in client.crate_dependencies(&c.id, &c.max_version)? { 34 | //! // Ignore optional dependencies. 35 | //! if !dep.optional { 36 | //! println!(" * {} - {}", dep.id, dep.version_id); 37 | //! } 38 | //! } 39 | //! } 40 | //! Ok(()) 41 | //! } 42 | //! ``` 43 | 44 | #![recursion_limit = "128"] 45 | #![deny(missing_docs)] 46 | 47 | mod async_client; 48 | mod error; 49 | mod sync_client; 50 | mod types; 51 | 52 | pub use crate::{ 53 | async_client::Client as AsyncClient, 54 | error::{Error, NotFoundError, PermissionDeniedError}, 55 | sync_client::SyncClient, 56 | types::*, 57 | }; 58 | -------------------------------------------------------------------------------- /src/sync_client.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use std::iter::Extend; 3 | 4 | use reqwest::{blocking::Client as HttpClient, header, StatusCode, Url}; 5 | use serde::de::DeserializeOwned; 6 | 7 | use crate::{error::JsonDecodeError, types::*}; 8 | 9 | /// A synchronous client for the crates.io API. 10 | pub struct SyncClient { 11 | client: HttpClient, 12 | base_url: Url, 13 | rate_limit: std::time::Duration, 14 | last_request_time: std::sync::Mutex>, 15 | } 16 | 17 | impl 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> { 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 { 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(&self, url: Url) -> Result { 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::(&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 { 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 { 116 | let url = super::async_client::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 { 122 | let url = super::async_client::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, Error> { 128 | let url = super::async_client::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 { 141 | let url = 142 | super::async_client::build_crate_reverse_deps_url(&self.base_url, crate_name, page)?; 143 | let page = self.get::(url)?; 144 | 145 | let mut deps = ReverseDependencies { 146 | dependencies: Vec::new(), 147 | meta: Meta { total: 0 }, 148 | }; 149 | deps.meta.total = page.meta.total; 150 | deps.extend(page); 151 | Ok(deps) 152 | } 153 | 154 | /// Load all reverse dependencies of a crate. 155 | /// 156 | /// Note: Since the reverse dependency endpoint requires pagination, this 157 | /// will result in multiple requests if the crate has more than 100 reverse 158 | /// dependencies. 159 | pub fn crate_reverse_dependencies( 160 | &self, 161 | crate_name: &str, 162 | ) -> Result { 163 | let mut deps = ReverseDependencies { 164 | dependencies: Vec::new(), 165 | meta: Meta { total: 0 }, 166 | }; 167 | 168 | for page_number in 1.. { 169 | let page = self.crate_reverse_dependencies_page(crate_name, page_number)?; 170 | if page.dependencies.is_empty() { 171 | break; 172 | } 173 | 174 | deps.dependencies.extend(page.dependencies); 175 | deps.meta.total = page.meta.total; 176 | } 177 | Ok(deps) 178 | } 179 | 180 | /// Get the total count of reverse dependencies for a given crate. 181 | pub fn crate_reverse_dependency_count(&self, crate_name: &str) -> Result { 182 | let page = self.crate_reverse_dependencies_page(crate_name, 1)?; 183 | Ok(page.meta.total) 184 | } 185 | 186 | /// Retrieve the authors for a crate version. 187 | pub fn crate_authors(&self, crate_name: &str, version: &str) -> Result { 188 | let url = 189 | super::async_client::build_crate_authors_url(&self.base_url, crate_name, version)?; 190 | let res: AuthorsResponse = self.get(url)?; 191 | Ok(Authors { 192 | names: res.meta.names, 193 | }) 194 | } 195 | 196 | /// Retrieve the dependencies of a crate version. 197 | pub fn crate_dependencies( 198 | &self, 199 | crate_name: &str, 200 | version: &str, 201 | ) -> Result, Error> { 202 | let url = 203 | super::async_client::build_crate_dependencies_url(&self.base_url, crate_name, version)?; 204 | let resp: Dependencies = self.get(url)?; 205 | Ok(resp.dependencies) 206 | } 207 | 208 | fn full_version(&self, version: Version) -> Result { 209 | let authors = self.crate_authors(&version.crate_name, &version.num)?; 210 | let deps = self.crate_dependencies(&version.crate_name, &version.num)?; 211 | Ok(FullVersion::from_parts(version, authors, deps)) 212 | } 213 | 214 | /// Retrieve all available information for a crate, including download 215 | /// stats, owners and reverse dependencies. 216 | /// 217 | /// The `all_versions` argument controls the retrieval of detailed version 218 | /// information. 219 | /// If false, only the data for the latest version will be fetched, if true, 220 | /// detailed information for all versions will be available. 221 | /// 222 | /// Note: Each version requires two extra requests. 223 | pub fn full_crate(&self, name: &str, all_versions: bool) -> Result { 224 | let resp = self.get_crate(name)?; 225 | let data = resp.crate_data; 226 | 227 | let dls = self.crate_downloads(name)?; 228 | let owners = self.crate_owners(name)?; 229 | let reverse_dependencies = self.crate_reverse_dependencies(name)?; 230 | 231 | let versions = if resp.versions.is_empty() { 232 | vec![] 233 | } else if all_versions { 234 | //let versions_res: Result> = resp.versions 235 | resp.versions 236 | .into_iter() 237 | .map(|v| self.full_version(v)) 238 | .collect::, Error>>()? 239 | } else { 240 | let v = self.full_version(resp.versions[0].clone())?; 241 | vec![v] 242 | }; 243 | 244 | let full = FullCrate { 245 | id: data.id, 246 | name: data.name, 247 | description: data.description, 248 | license: versions[0].license.clone(), 249 | documentation: data.documentation, 250 | homepage: data.homepage, 251 | repository: data.repository, 252 | total_downloads: data.downloads, 253 | recent_downloads: data.recent_downloads, 254 | max_version: data.max_version, 255 | max_stable_version: data.max_stable_version, 256 | created_at: data.created_at, 257 | updated_at: data.updated_at, 258 | 259 | categories: resp.categories, 260 | keywords: resp.keywords, 261 | downloads: dls, 262 | owners, 263 | reverse_dependencies, 264 | versions, 265 | }; 266 | Ok(full) 267 | } 268 | 269 | /// Retrieve a page of crates, optionally constrained by a query. 270 | /// 271 | /// If you want to get all results without worrying about paging, 272 | /// use [`all_crates`]. 273 | /// 274 | /// # Examples 275 | /// 276 | /// Retrieve the first page of results for the query "api", with 100 items 277 | /// per page and sorted alphabetically. 278 | /// 279 | /// ```rust 280 | /// # use crates_io_api::{SyncClient, CratesQuery, Sort, Error}; 281 | /// 282 | /// # fn f() -> Result<(), Box> { 283 | /// # let client = SyncClient::new( 284 | /// # "my-bot-name (my-contact@domain.com)", 285 | /// # std::time::Duration::from_millis(1000), 286 | /// # ).unwrap(); 287 | /// let q = CratesQuery::builder() 288 | /// .sort(Sort::Alphabetical) 289 | /// .search("awesome") 290 | /// .build(); 291 | /// let crates = client.crates(q)?; 292 | /// # std::mem::drop(crates); 293 | /// # Ok(()) 294 | /// # } 295 | /// ``` 296 | pub fn crates(&self, query: CratesQuery) -> Result { 297 | let mut url = self.base_url.join("crates")?; 298 | query.build(url.query_pairs_mut()); 299 | 300 | self.get(url) 301 | } 302 | 303 | /// Retrieves a user by username. 304 | pub fn user(&self, username: &str) -> Result { 305 | let url = self.base_url.join(&format!("users/{}", username))?; 306 | self.get::(url).map(|response| response.user) 307 | } 308 | } 309 | 310 | #[cfg(test)] 311 | mod test { 312 | use super::*; 313 | 314 | fn build_test_client() -> SyncClient { 315 | SyncClient::new( 316 | "crates-io-api-ci (github.com/theduke/crates-io-api)", 317 | std::time::Duration::from_millis(1000), 318 | ) 319 | .unwrap() 320 | } 321 | 322 | #[test] 323 | fn test_summary() -> Result<(), Error> { 324 | let client = build_test_client(); 325 | let summary = client.summary()?; 326 | assert!(!summary.most_downloaded.is_empty()); 327 | assert!(!summary.just_updated.is_empty()); 328 | assert!(!summary.new_crates.is_empty()); 329 | assert!(!summary.most_recently_downloaded.is_empty()); 330 | assert!(summary.num_crates > 0); 331 | assert!(summary.num_downloads > 0); 332 | assert!(!summary.popular_categories.is_empty()); 333 | assert!(!summary.popular_keywords.is_empty()); 334 | Ok(()) 335 | } 336 | 337 | #[test] 338 | fn test_full_crate() -> Result<(), Error> { 339 | let client = build_test_client(); 340 | client.full_crate("crates_io_api", false)?; 341 | Ok(()) 342 | } 343 | 344 | /// Ensure that the sync Client remains send. 345 | #[test] 346 | fn sync_client_ensure_send() { 347 | let client = build_test_client(); 348 | let _: &dyn Send = &client; 349 | } 350 | 351 | #[test] 352 | fn test_user_get_async() -> Result<(), Error> { 353 | let client = build_test_client(); 354 | let user = client.user("theduke")?; 355 | assert_eq!(user.login, "theduke"); 356 | Ok(()) 357 | } 358 | 359 | #[test] 360 | fn test_crates_filter_by_user_async() -> Result<(), Error> { 361 | let client = build_test_client(); 362 | 363 | let user = client.user("theduke")?; 364 | 365 | let res = client.crates(CratesQuery { 366 | user_id: Some(user.id), 367 | per_page: 5, 368 | ..Default::default() 369 | })?; 370 | 371 | assert!(!res.crates.is_empty()); 372 | // Ensure all found have the searched user as owner. 373 | for krate in res.crates { 374 | let owners = client.crate_owners(&krate.name)?; 375 | assert!(owners.iter().any(|o| o.id == user.id)); 376 | } 377 | 378 | Ok(()) 379 | } 380 | 381 | #[test] 382 | fn test_crate_reverse_dependency_count() -> Result<(), Error> { 383 | let client = build_test_client(); 384 | let count = client.crate_reverse_dependency_count("crates_io_api")?; 385 | assert!(count > 0); 386 | 387 | Ok(()) 388 | } 389 | } 390 | -------------------------------------------------------------------------------- /src/types.rs: -------------------------------------------------------------------------------- 1 | //! Types for the data that is available via the API. 2 | 3 | use chrono::{DateTime, NaiveDate, Utc}; 4 | use serde_derive::*; 5 | use std::{collections::HashMap, fmt}; 6 | 7 | /// A list of errors returned by the API. 8 | #[derive(Deserialize, Debug, Clone, PartialEq, Eq)] 9 | pub struct ApiErrors { 10 | /// Individual errors. 11 | pub errors: Vec, 12 | } 13 | 14 | /// An error returned by the API. 15 | #[derive(Deserialize, Debug, Clone, PartialEq, Eq)] 16 | pub struct ApiError { 17 | /// Error message. 18 | pub detail: Option, 19 | } 20 | 21 | impl fmt::Display for ApiError { 22 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 23 | write!( 24 | f, 25 | "{}", 26 | self.detail.as_deref().unwrap_or("Unknown API Error") 27 | ) 28 | } 29 | } 30 | 31 | /// Used to specify the sort behaviour of the `Client::crates()` method. 32 | #[derive(Debug, Clone, PartialEq, Eq)] 33 | pub enum Sort { 34 | /// Sort alphabetically. 35 | Alphabetical, 36 | /// Sort by relevance (meaningless if used without a query). 37 | Relevance, 38 | /// Sort by downloads. 39 | Downloads, 40 | /// Sort by recent downloads 41 | RecentDownloads, 42 | /// Sort by recent updates 43 | RecentUpdates, 44 | /// Sort by new 45 | NewlyAdded, 46 | } 47 | 48 | impl Sort { 49 | pub(crate) fn to_str(&self) -> &str { 50 | match self { 51 | Self::Alphabetical => "alpha", 52 | Self::Relevance => "relevance", 53 | Self::Downloads => "downloads", 54 | Self::RecentDownloads => "recent-downloads", 55 | Self::RecentUpdates => "recent-updates", 56 | Self::NewlyAdded => "new", 57 | } 58 | } 59 | } 60 | 61 | /// Options for the [crates]() method of the client. 62 | /// 63 | /// Used to specify pagination, sorting and a query. 64 | #[derive(Clone, Debug)] 65 | pub struct CratesQuery { 66 | /// Sort. 67 | pub(crate) sort: Sort, 68 | /// Number of items per page. 69 | pub(crate) per_page: u64, 70 | /// The page to fetch. 71 | pub(crate) page: u64, 72 | pub(crate) user_id: Option, 73 | pub(crate) team_id: Option, 74 | /// Crates.io category name. 75 | /// See 76 | /// NOTE: requires lower-case dash-separated categories, not the pretty 77 | /// titles visible in the listing linked above. 78 | pub(crate) category: Option, 79 | /// Search query string. 80 | pub(crate) search: Option, 81 | /// List of crate ids. 82 | pub(crate) ids: Option>, 83 | } 84 | 85 | impl CratesQuery { 86 | pub(crate) fn build(&self, mut q: url::form_urlencoded::Serializer<'_, url::UrlQuery<'_>>) { 87 | q.append_pair("page", &self.page.to_string()); 88 | q.append_pair("per_page", &self.per_page.to_string()); 89 | q.append_pair("sort", self.sort.to_str()); 90 | if let Some(id) = self.user_id { 91 | q.append_pair("user_id", &id.to_string()); 92 | } 93 | if let Some(id) = self.team_id { 94 | q.append_pair("team_id", &id.to_string()); 95 | } 96 | if let Some(search) = &self.search { 97 | q.append_pair("q", search); 98 | } 99 | if let Some(cat) = &self.category { 100 | q.append_pair("category", cat); 101 | } 102 | if let Some(ids) = &self.ids { 103 | for id in ids { 104 | q.append_pair("ids[]", id); 105 | } 106 | } 107 | } 108 | } 109 | 110 | impl CratesQuery { 111 | /// Construct a new [`CratesQueryBuilder`]. 112 | pub fn builder() -> CratesQueryBuilder { 113 | CratesQueryBuilder::new() 114 | } 115 | 116 | /// Get a reference to the crate query's sort. 117 | pub fn sort(&self) -> &Sort { 118 | &self.sort 119 | } 120 | 121 | /// Set the crate query's sort. 122 | pub fn set_sort(&mut self, sort: Sort) { 123 | self.sort = sort; 124 | } 125 | 126 | /// Get the crate query's per page. 127 | pub fn page_size(&self) -> u64 { 128 | self.per_page 129 | } 130 | 131 | /// Set the crate query's per page. 132 | pub fn set_page_size(&mut self, per_page: u64) { 133 | self.per_page = per_page; 134 | } 135 | 136 | /// Get the crate query's page. 137 | pub fn page(&self) -> u64 { 138 | self.page 139 | } 140 | 141 | /// Set the crate query's page. 142 | pub fn set_page(&mut self, page: u64) { 143 | self.page = page; 144 | } 145 | 146 | /// Get the crate query's user id. 147 | pub fn user_id(&self) -> Option { 148 | self.user_id 149 | } 150 | 151 | /// Set the crate query's user id. 152 | pub fn set_user_id(&mut self, user_id: Option) { 153 | self.user_id = user_id; 154 | } 155 | 156 | /// Get the crate query's team id. 157 | pub fn team_id(&self) -> Option { 158 | self.team_id 159 | } 160 | 161 | /// Set the crate query's team id. 162 | pub fn set_team_id(&mut self, team_id: Option) { 163 | self.team_id = team_id; 164 | } 165 | 166 | /// Get a reference to the crate query's category. 167 | pub fn category(&self) -> Option<&String> { 168 | self.category.as_ref() 169 | } 170 | 171 | /// Set the crate query's category. 172 | pub fn set_category(&mut self, category: Option) { 173 | self.category = category; 174 | } 175 | 176 | /// Get a reference to the crate query's search. 177 | pub fn search(&self) -> Option<&String> { 178 | self.search.as_ref() 179 | } 180 | 181 | /// Set the crate query's search. 182 | pub fn set_search(&mut self, search: Option) { 183 | self.search = search; 184 | } 185 | 186 | /// Get a reference to the crate query's ids. 187 | pub fn ids(&self) -> Option<&Vec> { 188 | self.ids.as_ref() 189 | } 190 | 191 | /// Set the crate query's ids. 192 | pub fn set_ids(&mut self, ids: Option>) { 193 | self.ids = ids; 194 | } 195 | } 196 | 197 | impl Default for CratesQuery { 198 | fn default() -> Self { 199 | Self { 200 | sort: Sort::RecentUpdates, 201 | per_page: 30, 202 | page: 1, 203 | user_id: None, 204 | team_id: None, 205 | category: None, 206 | search: None, 207 | ids: None, 208 | } 209 | } 210 | } 211 | 212 | /// Builder that enables easy construction of a [`CratesQuery`]. 213 | pub struct CratesQueryBuilder { 214 | query: CratesQuery, 215 | } 216 | 217 | impl CratesQueryBuilder { 218 | /// Construct a new builder. 219 | #[must_use] 220 | pub fn new() -> Self { 221 | Self { 222 | query: CratesQuery::default(), 223 | } 224 | } 225 | 226 | /// Set the sorting method. 227 | #[must_use] 228 | pub fn sort(mut self, sort: Sort) -> Self { 229 | self.query.sort = sort; 230 | self 231 | } 232 | 233 | /// Set the page. 234 | #[must_use] 235 | pub fn page(mut self, page: u64) -> Self { 236 | self.query.page = page; 237 | self 238 | } 239 | 240 | /// Set the page size. 241 | #[must_use] 242 | pub fn page_size(mut self, size: u64) -> Self { 243 | self.query.per_page = size; 244 | self 245 | } 246 | 247 | /// Filter by a user id. 248 | #[must_use] 249 | pub fn user_id(mut self, user_id: u64) -> Self { 250 | self.query.user_id = Some(user_id); 251 | self 252 | } 253 | 254 | /// Filter by a team id. 255 | #[must_use] 256 | pub fn team_id(mut self, team_id: u64) -> Self { 257 | self.query.team_id = Some(team_id); 258 | self 259 | } 260 | 261 | /// Crates.io category name. 262 | /// See 263 | /// NOTE: requires lower-case dash-separated categories, not the pretty 264 | /// titles visible in the listing linked above. 265 | #[must_use] 266 | pub fn category(mut self, category: impl Into) -> Self { 267 | self.query.category = Some(category.into()); 268 | self 269 | } 270 | 271 | /// Search term. 272 | #[must_use] 273 | pub fn search(mut self, search: impl Into) -> Self { 274 | self.query.search = Some(search.into()); 275 | self 276 | } 277 | 278 | /// List of crate ids. 279 | #[must_use] 280 | pub fn ids(mut self, ids: Vec) -> Self { 281 | self.query.ids = Some(ids); 282 | self 283 | } 284 | 285 | /// Finalize the builder into a usable [`CratesQuery`]. 286 | #[must_use] 287 | pub fn build(self) -> CratesQuery { 288 | self.query 289 | } 290 | } 291 | 292 | impl Default for CratesQueryBuilder { 293 | fn default() -> Self { 294 | Self::new() 295 | } 296 | } 297 | 298 | /// Pagination information. 299 | #[derive(Serialize, Deserialize, Debug, Clone)] 300 | pub struct Meta { 301 | /// The total amount of results. 302 | pub total: u64, 303 | } 304 | 305 | /// Links to individual API endpoints that provide crate details. 306 | #[derive(Serialize, Deserialize, Debug, Clone)] 307 | #[allow(missing_docs)] 308 | pub struct CrateLinks { 309 | pub owner_team: String, 310 | pub owner_user: String, 311 | pub owners: String, 312 | pub reverse_dependencies: String, 313 | pub version_downloads: String, 314 | pub versions: Option, 315 | } 316 | 317 | /// A Rust crate published to crates.io. 318 | #[derive(Serialize, Deserialize, Debug, Clone)] 319 | #[allow(missing_docs)] 320 | pub struct Crate { 321 | pub id: String, 322 | pub name: String, 323 | pub description: Option, 324 | // FIXME: Remove on next breaking version bump. 325 | #[deprecated( 326 | since = "0.8.1", 327 | note = "This field is always empty. The license is only available on a specific `Version` of a crate or on `FullCrate`. This field will be removed in the next minor version bump." 328 | )] 329 | pub license: Option, 330 | pub documentation: Option, 331 | pub homepage: Option, 332 | pub repository: Option, 333 | // TODO: determine badge format. 334 | // pub badges: Vec, 335 | pub downloads: u64, 336 | pub recent_downloads: Option, 337 | /// NOTE: not set if the crate was loaded via a list query. 338 | pub categories: Option>, 339 | /// NOTE: not set if the crate was loaded via a list query. 340 | pub keywords: Option>, 341 | pub versions: Option>, 342 | pub max_version: String, 343 | pub max_stable_version: Option, 344 | pub links: CrateLinks, 345 | pub created_at: DateTime, 346 | pub updated_at: DateTime, 347 | pub exact_match: Option, 348 | } 349 | 350 | /// Full data for a crate listing. 351 | #[derive(Serialize, Deserialize, Debug, Clone)] 352 | #[allow(missing_docs)] 353 | pub struct CratesPage { 354 | pub crates: Vec, 355 | #[serde(default)] 356 | pub versions: Vec, 357 | #[serde(default)] 358 | pub keywords: Vec, 359 | #[serde(default)] 360 | pub categories: Vec, 361 | pub meta: Meta, 362 | } 363 | 364 | /// Links to API endpoints providing extra data for a crate version. 365 | #[derive(Serialize, Deserialize, Debug, Clone)] 366 | #[allow(missing_docs)] 367 | pub struct VersionLinks { 368 | #[deprecated( 369 | since = "0.7.1", 370 | note = "This field was removed from the API and will always be empty. Will be removed in 0.8.0." 371 | )] 372 | #[serde(default)] 373 | pub authors: String, 374 | pub dependencies: String, 375 | pub version_downloads: String, 376 | } 377 | 378 | /// Changes made to a create [`Version`] 379 | #[derive(Serialize, Deserialize, Debug, Clone)] 380 | #[allow(missing_docs)] 381 | pub struct AuditAction { 382 | /// publish, yank, unyank 383 | action: String, 384 | time: DateTime, 385 | user: User, 386 | } 387 | 388 | /// A [`Crate`] version. 389 | #[derive(Serialize, Deserialize, Debug, Clone)] 390 | #[allow(missing_docs)] 391 | pub struct Version { 392 | #[serde(rename = "crate")] 393 | pub crate_name: String, 394 | pub created_at: DateTime, 395 | pub updated_at: DateTime, 396 | pub dl_path: String, 397 | pub downloads: u64, 398 | pub features: HashMap>, 399 | pub id: u64, 400 | pub num: String, 401 | pub yanked: bool, 402 | pub license: Option, 403 | pub readme_path: Option, 404 | pub links: VersionLinks, 405 | pub crate_size: Option, 406 | pub published_by: Option, 407 | pub rust_version: Option, 408 | #[serde(default)] 409 | pub audit_actions: Vec, 410 | } 411 | 412 | /// A crate category. 413 | #[derive(Serialize, Deserialize, Debug, Clone)] 414 | #[allow(missing_docs)] 415 | pub struct Category { 416 | pub category: String, 417 | pub crates_cnt: u64, 418 | pub created_at: DateTime, 419 | pub description: String, 420 | pub id: String, 421 | pub slug: String, 422 | } 423 | 424 | /// A keyword available on crates.io. 425 | #[derive(Serialize, Deserialize, Debug, Clone)] 426 | #[allow(missing_docs)] 427 | pub struct Keyword { 428 | pub id: String, 429 | pub keyword: String, 430 | pub crates_cnt: u64, 431 | pub created_at: DateTime, 432 | } 433 | 434 | /// Full data for a crate. 435 | #[derive(Serialize, Deserialize, Debug, Clone)] 436 | #[allow(missing_docs)] 437 | pub struct CrateResponse { 438 | pub categories: Vec, 439 | #[serde(rename = "crate")] 440 | pub crate_data: Crate, 441 | pub keywords: Vec, 442 | pub versions: Vec, 443 | } 444 | 445 | /// Summary for crates.io. 446 | #[derive(Serialize, Deserialize, Debug, Clone)] 447 | #[allow(missing_docs)] 448 | pub struct Summary { 449 | pub just_updated: Vec, 450 | pub most_downloaded: Vec, 451 | pub new_crates: Vec, 452 | pub most_recently_downloaded: Vec, 453 | pub num_crates: u64, 454 | pub num_downloads: u64, 455 | pub popular_categories: Vec, 456 | pub popular_keywords: Vec, 457 | } 458 | 459 | /// Download data for a single crate version. 460 | #[derive(Serialize, Deserialize, Debug, Clone)] 461 | #[allow(missing_docs)] 462 | pub struct VersionDownloads { 463 | pub date: NaiveDate, 464 | pub downloads: u64, 465 | pub version: u64, 466 | } 467 | 468 | /// Crate downloads that don't fit a particular date. 469 | /// Only required for old download data. 470 | #[derive(Serialize, Deserialize, Debug, Clone)] 471 | #[allow(missing_docs)] 472 | pub struct ExtraDownloads { 473 | pub date: NaiveDate, 474 | pub downloads: u64, 475 | } 476 | 477 | /// Additional data for crate downloads. 478 | #[derive(Serialize, Deserialize, Debug, Clone)] 479 | #[allow(missing_docs)] 480 | pub struct CrateDownloadsMeta { 481 | pub extra_downloads: Vec, 482 | } 483 | 484 | /// Download data for all versions of a [`Crate`]. 485 | #[derive(Serialize, Deserialize, Debug, Clone)] 486 | #[allow(missing_docs)] 487 | pub struct CrateDownloads { 488 | pub version_downloads: Vec, 489 | pub meta: CrateDownloadsMeta, 490 | } 491 | 492 | /// A crates.io user. 493 | #[derive(Serialize, Deserialize, Debug, Clone)] 494 | #[allow(missing_docs)] 495 | pub struct User { 496 | pub avatar: Option, 497 | pub email: Option, 498 | pub id: u64, 499 | pub kind: Option, 500 | pub login: String, 501 | pub name: Option, 502 | pub url: String, 503 | } 504 | 505 | /// Additional crate author metadata. 506 | #[derive(Serialize, Deserialize, Debug, Clone)] 507 | #[allow(missing_docs)] 508 | pub struct AuthorsMeta { 509 | pub names: Vec, 510 | } 511 | 512 | /// API Response for authors data. 513 | #[derive(Serialize, Deserialize, Debug, Clone)] 514 | #[allow(missing_docs)] 515 | pub(crate) struct AuthorsResponse { 516 | pub meta: AuthorsMeta, 517 | } 518 | 519 | /// Crate author names. 520 | #[derive(Serialize, Deserialize, Debug, Clone)] 521 | #[allow(missing_docs)] 522 | pub struct Authors { 523 | pub names: Vec, 524 | } 525 | 526 | /// Crate owners. 527 | #[derive(Serialize, Deserialize, Debug, Clone)] 528 | #[allow(missing_docs)] 529 | pub struct Owners { 530 | pub users: Vec, 531 | } 532 | 533 | /// A crate dependency. 534 | /// Specifies the crate and features. 535 | #[derive(Serialize, Deserialize, Debug, Clone)] 536 | #[allow(missing_docs)] 537 | pub struct Dependency { 538 | pub crate_id: String, 539 | pub default_features: bool, 540 | pub downloads: u64, 541 | pub features: Vec, 542 | pub id: u64, 543 | pub kind: String, 544 | pub optional: bool, 545 | pub req: String, 546 | pub target: Option, 547 | pub version_id: u64, 548 | } 549 | 550 | /// List of dependencies of a crate. 551 | #[derive(Serialize, Deserialize, Debug, Clone)] 552 | #[allow(missing_docs)] 553 | pub struct Dependencies { 554 | pub dependencies: Vec, 555 | } 556 | 557 | /// Single reverse dependency (aka a dependent) of a crate. 558 | #[derive(Serialize, Deserialize, Debug, Clone)] 559 | #[allow(missing_docs)] 560 | pub struct ReverseDependency { 561 | pub crate_version: Version, 562 | pub dependency: Dependency, 563 | } 564 | 565 | // This is how reverse dependencies are received 566 | #[derive(Serialize, Deserialize, Debug, Clone)] 567 | pub(super) struct ReverseDependenciesAsReceived { 568 | pub dependencies: Vec, 569 | pub versions: Vec, 570 | pub meta: Meta, 571 | } 572 | 573 | /// Full list of reverse dependencies for a crate (version). 574 | #[derive(Serialize, Deserialize, Debug, Clone)] 575 | #[allow(missing_docs)] 576 | pub struct ReverseDependencies { 577 | pub dependencies: Vec, 578 | pub meta: Meta, 579 | } 580 | 581 | impl ReverseDependencies { 582 | /// Fills the dependencies field from a ReverseDependenciesAsReceived struct. 583 | pub(crate) fn extend(&mut self, rdeps: ReverseDependenciesAsReceived) { 584 | for d in rdeps.dependencies { 585 | for v in &rdeps.versions { 586 | if v.id == d.version_id { 587 | // Right now it iterates over the full vector for each vector element. 588 | // For large vectors, it may be faster to remove each matched element 589 | // using the drain_filter() method once it's stabilized: 590 | // https://doc.rust-lang.org/nightly/std/vec/struct.Vec.html#method.drain_filter 591 | self.dependencies.push(ReverseDependency { 592 | crate_version: v.clone(), 593 | dependency: d.clone(), 594 | }); 595 | } 596 | } 597 | } 598 | } 599 | } 600 | 601 | /// Complete information for a crate version. 602 | #[derive(Serialize, Deserialize, Debug, Clone)] 603 | #[allow(missing_docs)] 604 | pub struct FullVersion { 605 | #[serde(rename = "crate")] 606 | pub crate_name: String, 607 | pub created_at: DateTime, 608 | pub updated_at: DateTime, 609 | pub dl_path: String, 610 | pub downloads: u64, 611 | pub features: HashMap>, 612 | pub id: u64, 613 | pub num: String, 614 | pub yanked: bool, 615 | pub license: Option, 616 | pub readme_path: Option, 617 | pub links: VersionLinks, 618 | pub crate_size: Option, 619 | pub published_by: Option, 620 | pub rust_version: Option, 621 | #[serde(default)] 622 | pub audit_actions: Vec, 623 | 624 | pub author_names: Vec, 625 | pub dependencies: Vec, 626 | } 627 | 628 | impl FullVersion { 629 | /// Creates a [`FullVersion`] from a [`Version`], author names, and dependencies. 630 | pub fn from_parts(version: Version, authors: Authors, dependencies: Vec) -> Self { 631 | FullVersion { 632 | crate_name: version.crate_name, 633 | created_at: version.created_at, 634 | updated_at: version.updated_at, 635 | dl_path: version.dl_path, 636 | downloads: version.downloads, 637 | features: version.features, 638 | id: version.id, 639 | num: version.num, 640 | yanked: version.yanked, 641 | license: version.license, 642 | links: version.links, 643 | readme_path: version.readme_path, 644 | crate_size: version.crate_size, 645 | published_by: version.published_by, 646 | rust_version: version.rust_version, 647 | audit_actions: version.audit_actions, 648 | 649 | author_names: authors.names, 650 | dependencies, 651 | } 652 | } 653 | } 654 | 655 | /// Complete information for a crate. 656 | #[derive(Serialize, Deserialize, Debug, Clone)] 657 | #[allow(missing_docs)] 658 | pub struct FullCrate { 659 | pub id: String, 660 | pub name: String, 661 | pub description: Option, 662 | pub license: Option, 663 | pub documentation: Option, 664 | pub homepage: Option, 665 | pub repository: Option, 666 | pub total_downloads: u64, 667 | pub recent_downloads: Option, 668 | pub max_version: String, 669 | pub max_stable_version: Option, 670 | pub created_at: DateTime, 671 | pub updated_at: DateTime, 672 | 673 | pub categories: Vec, 674 | pub keywords: Vec, 675 | pub downloads: CrateDownloads, 676 | pub owners: Vec, 677 | pub reverse_dependencies: ReverseDependencies, 678 | 679 | pub versions: Vec, 680 | } 681 | 682 | #[derive(Serialize, Deserialize, Debug, Clone)] 683 | pub(crate) struct UserResponse { 684 | pub user: User, 685 | } 686 | --------------------------------------------------------------------------------