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}