cargo_lookup/lib.rs
1//! [![github]](https://github.com/collinoc/cargo-lookup)
2//!
3//! [github]: https://img.shields.io/badge/github-blue?style=for-the-badge&logo=github&link=https%3A%2F%2Fgithub.com%2Fcollinoc%2Fcargo-lookup
4//!
5//! A library for querying Rust crate registries
6//!
7//! ## Examples
8//!
9//! Get all info for a package:
10//! ```no_run
11//! use cargo_lookup::{Query, Result};
12//!
13//! fn main() -> Result<()> {
14//! let query: Query = "cargo".parse()?;
15//! let all_package_info = query.package()?;
16//!
17//! println!("{all_package_info:?}");
18//!
19//! Ok(())
20//! }
21//! ```
22//!
23//! Get a specific release of a package:
24//! ```no_run
25//! use cargo_lookup::{Query, Result};
26//!
27//! fn main() -> Result<()> {
28//! let query: Query = "cargo@=0.2.153".parse()?;
29//! let specific_release = query.submit()?;
30//!
31//! println!("{specific_release:?}");
32//!
33//! Ok(())
34//! }
35//! ```
36
37#![deny(clippy::all)]
38
39pub mod error;
40#[cfg(test)]
41mod tests;
42
43use semver::{Version, VersionReq};
44use serde::{Deserialize, Serialize};
45use std::{collections::BTreeMap, str::FromStr};
46
47use error::Error;
48
49/// The default crates.io index URL
50pub const CRATES_IO_INDEX_URL: &str = "https://index.crates.io";
51
52pub type Result<T> = std::result::Result<T, Error>;
53
54/// A query for a specific rust package based on the packages name, an option version requirement,
55/// in an optional custom index. By default, [`CRATES_IO_INDEX_URL`] will be used as the index
56#[derive(Debug, Clone)]
57pub struct Query {
58 name: String,
59 version_req: Option<VersionReq>,
60 custom_index: Option<String>,
61}
62
63impl FromStr for Query {
64 type Err = Error;
65
66 fn from_str(name: &str) -> std::result::Result<Self, Self::Err> {
67 let (name, version_req) = match name.split_once('@') {
68 Some((name, version)) if !version.is_empty() => (name, Some(version)),
69 _ => (name, None),
70 };
71
72 let version_req = version_req
73 .map(|req| VersionReq::parse(req).map_err(Error::InvalidVersion))
74 .transpose()?;
75
76 Ok(Self {
77 name: name.to_owned(),
78 version_req,
79 custom_index: None,
80 })
81 }
82}
83
84impl Query {
85 /// USe a custom crate index for this query
86 pub fn with_index<T>(mut self, custom_index: T) -> Self
87 where
88 String: From<T>,
89 {
90 self.custom_index = Some(String::from(custom_index));
91 self
92 }
93
94 /// Return the raw contents of the index file found by this query
95 pub fn raw_index(&self) -> Result<String> {
96 let index_url = self.custom_index.as_deref().unwrap_or(CRATES_IO_INDEX_URL);
97 let index_path = get_index_path(&self.name);
98 let response = ureq::get(&format!("{index_url}/{index_path}"))
99 .call()
100 .map_err(|err| Error::Request(Box::new(err)))?
101 .into_string()
102 .map_err(Error::Io)?;
103
104 Ok(response)
105 }
106
107 /// Return all of the info for the package found by this query
108 pub fn package(&self) -> Result<Package> {
109 Package::from_index(self.raw_index()?)
110 }
111
112 /// Return a specific release of a package found by this query
113 ///
114 /// If no version requirement ws specified, the latest version of the found package
115 /// will be returned
116 pub fn submit(&self) -> Result<Option<Release>> {
117 let package = self.package()?;
118
119 match self.version_req {
120 Some(ref version_req) => Ok(package.into_version(version_req)),
121 None => Ok(package.into_latest()),
122 }
123 }
124}
125
126/// All info on a package from it's index file, including all of it's releases
127#[derive(Debug, Clone)]
128pub struct Package {
129 name: String,
130 index_path: String,
131 releases: Vec<Release>,
132}
133
134impl Package {
135 /// Return this package's name
136 pub fn name(&self) -> &str {
137 self.name.as_str()
138 }
139
140 /// Return the index path for this package
141 pub fn index_path(&self) -> &str {
142 self.index_path.as_str()
143 }
144
145 /// Return a list of releases for this package
146 pub fn releases(&self) -> &Vec<Release> {
147 &self.releases
148 }
149
150 /// Convert into a packages latest release
151 pub fn into_latest(mut self) -> Option<Release> {
152 self.releases.pop()
153 }
154
155 /// Get a packages latest release
156 pub fn latest(&self) -> Option<&Release> {
157 self.releases.last()
158 }
159
160 /// Convert to a package release from a given version requirement
161 ///
162 /// This will find the latest possible release that matches the version requirement
163 ///
164 /// For example, with a version requirement of `^0.1.0`, this will return `0.1.9` before it
165 /// will return `0.1.8`
166 pub fn into_version(self, version_req: &semver::VersionReq) -> Option<Release> {
167 self.releases
168 .into_iter()
169 .rev()
170 .find(|release| version_req.matches(&release.vers))
171 }
172
173 /// Find a package release from a given version requirement
174 ///
175 /// This will find the latest possible release that matches the version requirement
176 ///
177 /// For example, with a version requirement of `^0.1.0`, this will return `0.1.9` before it
178 /// will return `0.1.8`
179 pub fn version(&self, version_req: &semver::VersionReq) -> Option<&Release> {
180 self.releases
181 .iter()
182 .rev()
183 .find(|release| version_req.matches(&release.vers))
184 }
185
186 /// Parse a package from it's index file
187 pub fn from_index<T>(content: T) -> Result<Self>
188 where
189 T: AsRef<str>,
190 {
191 let content = content.as_ref();
192
193 let releases: Result<Vec<Release>> = content
194 .lines()
195 .map(|release| serde_json::from_str(release).map_err(Error::Deserialize))
196 .collect();
197 let releases = releases?;
198
199 let name = releases
200 .last()
201 .ok_or(Error::FromIndexFile("empty"))?
202 .name
203 .clone();
204
205 let index_path = get_index_path(&name);
206
207 Ok(Package {
208 name,
209 index_path,
210 releases,
211 })
212 }
213}
214
215// See: https://github.com/serde-rs/serde/issues/368
216const fn one() -> u32 {
217 1
218}
219
220/// An entry for a given release version of a package
221///
222/// A package index file contains one line for each release of a package in json format, from oldest to latest.
223///
224/// More info on the schema can be found in [The Cargo Book](https://doc.rust-lang.org/cargo/reference/registry-index.html#json-schema)
225#[derive(Debug, Clone, Serialize, Deserialize)]
226pub struct Release {
227 /// The name of the package
228 pub name: String,
229 /// The specific version of the release
230 pub vers: Version,
231 /// List of direct dependencies of this package
232 pub deps: Vec<Dependency>,
233 /// The SHA256 checksum of the releases .crate
234 pub cksum: String,
235 /// A mapping of features' names to the features/dependencies they enable
236 pub features: Features,
237 /// Whether or not this release has been yanked
238 pub yanked: bool,
239 /// The `links` value from this packages manifest
240 pub links: Option<String>,
241 /// Value indicating the schema version for this entry
242 ///
243 /// Defaults to `1`
244 #[serde(default = "one")]
245 pub v: u32,
246 /// A mapping of features with new extended syntax including
247 /// namespaced features and weak dependencies
248 pub features2: Option<Features>,
249 /// The minimum supported rust version requirement without operator
250 pub rust_version: Option<VersionReq>,
251}
252
253impl Release {
254 /// Convert the release to it's json representation
255 pub fn as_json_string(&self) -> Result<String> {
256 serde_json::to_string(self).map_err(Error::Serialize)
257 }
258}
259
260pub type Features = BTreeMap<String, Vec<String>>;
261
262/// A dependency of a package
263///
264/// The structure can be found in [The Cargo Book](https://doc.rust-lang.org/cargo/reference/registry-index.html#json-schema)
265#[derive(Debug, Clone, Serialize, Deserialize)]
266pub struct Dependency {
267 /// The name of the dependency
268 pub name: String,
269 /// The SemVer requirement for the dependency
270 pub req: VersionReq,
271 /// List of features enabled for this dependency
272 pub features: Vec<String>,
273 /// Whether this dependency is optional or not
274 pub optional: bool,
275 /// Whether default features are enabled for this dependency or not
276 pub default_features: bool,
277 /// Target platform for the dependency
278 pub target: Option<String>,
279 /// The dependency kind (dev, build, or normal)
280 pub kind: Option<String>,
281 /// The URL of the index where this dependency is from. Defaults to current registry
282 pub registry: Option<String>,
283 /// If dependency is renamed, this is the name of the actual dependend upon package
284 pub package: Option<String>,
285}
286
287/// Get the index path for a package
288///
289/// ## Examples
290///
291/// ```
292/// use cargo_lookup::get_index_path;
293///
294/// assert_eq!(get_index_path("cargo"), "ca/rg/cargo");
295/// assert_eq!(get_index_path("ice"), "3/i/ice");
296/// ```
297pub fn get_index_path<T>(package: T) -> String
298where
299 T: AsRef<str>,
300{
301 let package = package.as_ref();
302
303 let path = match package.len() {
304 1 => format!("1/{package}"),
305 2 => format!("2/{package}"),
306 3 => {
307 let first_char = &package[..1];
308 format!("3/{first_char}/{package}")
309 }
310 _ => {
311 let first_two_chars = &package[..2];
312 let next_two_chars = &package[2..4];
313 format!("{first_two_chars}/{next_two_chars}/{package}")
314 }
315 };
316
317 path.to_ascii_lowercase()
318}