check_latest/
lib.rs

1//! # Examples
2//! ## The Basics
3//!
4//! ```rust,no_run
5//! use check_latest::check_max;
6//!
7//! if let Ok(Some(version)) = check_max!() {
8//!     println!("Version {} is now available!", version);
9//! }
10//! ```
11//!
12//! ## Something Slightly More Complicated
13//!
14//! ```rust,no_run
15//! use check_latest::*;
16//!
17//! if let Ok(available_versions) = Versions::new(crate_name!(), user_agent!()) {
18//!     let current_version = crate_version!();
19//!     let is_yanked = available_versions
20//!         .versions()
21//!         .iter()
22//!         .filter(|version| version.yanked)
23//!         .any(|version| version == current_version);
24//!
25//!     if is_yanked {
26//!         println!("This version has been yanked.");
27//!     }
28//! } else {
29//!     eprintln!("We couldn't check for available versions.");
30//! }
31//! ```
32//!
33//! # Features
34//! ## `blocking`
35//!
36//! This feature is enabled by default.
37//!
38//! Provides the basic usage.
39//!
40//! ## `async`
41//!
42//! Allows you to asynchronously check for available versions.
43//! If enabled, it will provide async versions of the macros, which can be used
44//! with `<macro_name>_async!` For example, `check_max_async!`.
45//!
46//! ```toml
47//! [dependencies.check-latest]
48//! default-features = false # If you want async, you probably don't want blocking
49//! features = ["async"]
50//! ```
51
52#![deny(missing_docs)]
53
54use chrono::{DateTime, Utc};
55use semver::Version as SemVer;
56use serde::Deserialize;
57use std::cmp::Ordering;
58use std::fmt::{self, Display};
59
60/// A collection of `Version`s.
61#[derive(Debug, Deserialize)]
62pub struct Versions {
63    versions: Vec<Version>,
64}
65
66/// A release to [Crates.io].
67///
68/// [Crates.io]: https://crates.io/
69#[derive(Clone, Debug, Deserialize)]
70#[non_exhaustive]
71pub struct Version {
72    #[serde(rename = "num")]
73    version: SemVer,
74    /// If this version was yanked
75    pub yanked: bool,
76    /// When this version was published
77    pub created_at: DateTime<Utc>,
78}
79
80impl Versions {
81    /// Gets *any* max version.
82    ///
83    /// # Example
84    ///
85    /// ```rust,no_run
86    /// use check_latest::Versions;
87    ///
88    /// let newest = Versions::new("my-cool-crate", "my-cool-crate/1.0.0")
89    ///     .unwrap()
90    ///     .max_version();
91    /// ```
92    pub fn max_version(&self) -> Option<&Version> {
93        self.versions
94            .iter()
95            .max_by(|v1, v2| v1.version.cmp(&v2.version))
96    }
97    /// Gets the max version that hasn't been yanked.
98    ///
99    /// # Example
100    ///
101    /// ```rust,no_run
102    /// use check_latest::Versions;
103    ///
104    /// let newest = Versions::new("my-cool-crate", "my-cool-crate/1.0.0")
105    ///     .unwrap()
106    ///     .max_unyanked_version();
107    /// ```
108    pub fn max_unyanked_version(&self) -> Option<&Version> {
109        self.versions
110            .iter()
111            .filter(|v| !v.yanked)
112            .max_by(|v1, v2| v1.version.cmp(&v2.version))
113    }
114    /// Gets max version that has been yanked.
115    ///
116    /// # Example
117    ///
118    /// ```rust,no_run
119    /// use check_latest::Versions;
120    ///
121    /// let newest = Versions::new("my-cool-crate", "my-cool-crate/1.0.0")
122    ///     .unwrap()
123    ///     .newest_yanked_version();
124    /// ```
125    pub fn max_yanked_version(&self) -> Option<&Version> {
126        self.versions
127            .iter()
128            .filter(|v| v.yanked)
129            .max_by(|v1, v2| v1.version.cmp(&v2.version))
130    }
131    /// Gets *any* max version with the same major version.
132    ///
133    /// For example, if `major` = 1, then `1.0.0 <= max_minor_version < 2.0.0`.
134    ///
135    /// # Example
136    ///
137    /// ```rust,no_run
138    /// use check_latest::Versions;
139    ///
140    /// let newest = Versions::new("my-cool-crate", "my-cool-crate/1.0.0")
141    ///     .unwrap()
142    ///     .max_minor_version(1);
143    /// ```
144    pub fn max_minor_version(&self, major: u64) -> Option<&Version> {
145        self.versions
146            .iter()
147            .filter(|v| v.major() == major)
148            .max_by(|v1, v2| v1.version.cmp(&v2.version))
149    }
150    /// Gets the max version that hasn't been yanked with the same major
151    /// version.
152    ///
153    /// For example, if `major` = 1, then `1.0.0 <= max_minor_version < 2.0.0`.
154    ///
155    /// # Example
156    ///
157    /// ```rust,no_run
158    /// use check_latest::Versions;
159    ///
160    /// let newest = Versions::new("my-cool-crate", "my-cool-crate/1.0.0")
161    ///     .unwrap()
162    ///     .max_unyanked_minor_version(1);
163    /// ```
164    pub fn max_unyanked_minor_version(&self, major: u64) -> Option<&Version> {
165        self.versions
166            .iter()
167            .filter(|v| !v.yanked)
168            .filter(|v| v.major() == major)
169            .max_by(|v1, v2| v1.version.cmp(&v2.version))
170    }
171    /// Gets max version that has been yanked with the same major version.
172    ///
173    /// For example, if `major` = 1, then `1.0.0 <= max_minor_version < 2.0.0`.
174    ///
175    /// # Example
176    ///
177    /// ```rust,no_run
178    /// use check_latest::Versions;
179    ///
180    /// let newest = Versions::new("my-cool-crate", "my-cool-crate/1.0.0")
181    ///     .unwrap()
182    ///     .max_yanked_minor_version(1);
183    /// ```
184    pub fn max_yanked_minor_version(&self, major: u64) -> Option<&Version> {
185        self.versions
186            .iter()
187            .filter(|v| v.yanked)
188            .filter(|v| v.major() == major)
189            .max_by(|v1, v2| v1.version.cmp(&v2.version))
190    }
191    /// Gets *any* max version with the same major and minor version.
192    ///
193    /// For example, if `major` = 1 and `minor` = 2,
194    /// then `1.2.0 <= max_patch < 1.3.0`.
195    ///
196    /// # Example
197    ///
198    /// ```rust,no_run
199    /// use check_latest::Versions;
200    ///
201    /// let newest = Versions::new("my-cool-crate", "my-cool-crate/1.0.0")
202    ///     .unwrap()
203    ///     .max_patch(1, 2);
204    /// ```
205    pub fn max_patch(&self, major: u64, minor: u64) -> Option<&Version> {
206        self.versions
207            .iter()
208            .filter(|v| v.major() == major)
209            .filter(|v| v.minor() == minor)
210            .max_by(|v1, v2| v1.version.cmp(&v2.version))
211    }
212    /// Gets the max version that hasn't been yanked with the same major
213    /// and minor version.
214    ///
215    /// For example, if `major` = 1 and `minor` = 2,
216    /// then `1.2.0 <= max_patch < 1.3.0`.
217    ///
218    /// # Example
219    ///
220    /// ```rust,no_run
221    /// use check_latest::Versions;
222    ///
223    /// let newest = Versions::new("my-cool-crate", "my-cool-crate/1.0.0")
224    ///     .unwrap()
225    ///     .max_unyanked_patch(1, 2);
226    /// ```
227    pub fn max_unyanked_patch(&self, major: u64, minor: u64) -> Option<&Version> {
228        self.versions
229            .iter()
230            .filter(|v| !v.yanked)
231            .filter(|v| v.major() == major)
232            .filter(|v| v.minor() == minor)
233            .max_by(|v1, v2| v1.version.cmp(&v2.version))
234    }
235    /// Gets max version that has been yanked with the same major and minor
236    /// version.
237    ///
238    /// For example, if `major` = 1 and `minor` = 2,
239    /// then `1.2.0 <= max_patch < 1.3.0`.
240    ///
241    /// # Example
242    ///
243    /// ```rust,no_run
244    /// use check_latest::Versions;
245    ///
246    /// let newest = Versions::new("my-cool-crate", "my-cool-crate/1.0.0")
247    ///     .unwrap()
248    ///     .max_yanked_patch(1, 2);
249    /// ```
250    pub fn max_yanked_patch(&self, major: u64, minor: u64) -> Option<&Version> {
251        self.versions
252            .iter()
253            .filter(|v| v.yanked)
254            .filter(|v| v.major() == major)
255            .filter(|v| v.minor() == minor)
256            .max_by(|v1, v2| v1.version.cmp(&v2.version))
257    }
258    /// Gets *any* newest version.
259    ///
260    /// # Example
261    ///
262    /// ```rust,no_run
263    /// use check_latest::Versions;
264    ///
265    /// let newest = Versions::new("my-cool-crate", "my-cool-crate/1.0.0")
266    ///     .unwrap()
267    ///     .newest_version();
268    /// ```
269    pub fn newest_version(&self) -> Option<&Version> {
270        self.versions
271            .iter()
272            .max_by(|v1, v2| v1.created_at.cmp(&v2.created_at))
273    }
274    /// Gets the newest version that hasn't been yanked.
275    ///
276    /// # Example
277    ///
278    /// ```rust,no_run
279    /// use check_latest::Versions;
280    ///
281    /// let newest = Versions::new("my-cool-crate", "my-cool-crate/1.0.0")
282    ///     .unwrap()
283    ///     .newest_unyanked_version();
284    /// ```
285    pub fn newest_unyanked_version(&self) -> Option<&Version> {
286        self.versions
287            .iter()
288            .filter(|v| !v.yanked)
289            .max_by(|v1, v2| v1.created_at.cmp(&v2.created_at))
290    }
291    /// Gets newest version that has been yanked.
292    ///
293    /// # Example
294    ///
295    /// ```rust,no_run
296    /// use check_latest::Versions;
297    ///
298    /// let newest = Versions::new("my-cool-crate", "my-cool-crate/1.0.0")
299    ///     .unwrap()
300    ///     .newest_yanked_version();
301    /// ```
302    pub fn newest_yanked_version(&self) -> Option<&Version> {
303        self.versions
304            .iter()
305            .filter(|v| v.yanked)
306            .max_by(|v1, v2| v1.created_at.cmp(&v2.created_at))
307    }
308    /// Gets the full list of versions that were found.
309    pub fn versions(&self) -> &Vec<Version> {
310        &self.versions
311    }
312    /// Gets a mutable list of versions that were found.
313    pub fn versions_mut(&mut self) -> &mut Vec<Version> {
314        &mut self.versions
315    }
316    /// Takes ownership of `self` and returns owned versions list.
317    pub fn versions_owned(self) -> Vec<Version> {
318        self.versions
319    }
320}
321
322impl Version {
323    /// Gets the SemVer MAJOR version
324    pub fn major(&self) -> u64 {
325        self.version.major
326    }
327    /// Gets the SemVer MINOR version
328    pub fn minor(&self) -> u64 {
329        self.version.minor
330    }
331    /// Gets the SemVer PATCH version
332    pub fn patch(&self) -> u64 {
333        self.version.patch
334    }
335}
336
337impl PartialEq<SemVer> for Version {
338    fn eq(&self, rhs: &SemVer) -> bool {
339        self.version.eq(&rhs)
340    }
341}
342
343impl PartialEq<str> for Version {
344    fn eq(&self, rhs: &str) -> bool {
345        match SemVer::parse(rhs) {
346            Ok(version) => self.eq(&version),
347            Err(_) => false,
348        }
349    }
350}
351
352impl PartialEq<&str> for Version {
353    fn eq(&self, rhs: &&str) -> bool {
354        self.eq(rhs.to_owned())
355    }
356}
357
358impl PartialOrd<SemVer> for Version {
359    fn partial_cmp(&self, rhs: &SemVer) -> Option<Ordering> {
360        self.version.partial_cmp(rhs)
361    }
362}
363
364impl PartialOrd<str> for Version {
365    fn partial_cmp(&self, rhs: &str) -> Option<Ordering> {
366        match SemVer::parse(rhs) {
367            Ok(version) => self.partial_cmp(&version),
368            Err(_) => None,
369        }
370    }
371}
372
373impl PartialOrd<&str> for Version {
374    fn partial_cmp(&self, rhs: &&str) -> Option<Ordering> {
375        self.partial_cmp(rhs.to_owned())
376    }
377}
378
379impl Display for Version {
380    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
381        write!(f, "{}", self.version)?;
382        if self.yanked {
383            write!(f, " (yanked)")
384        } else {
385            Ok(())
386        }
387    }
388}
389
390impl From<Version> for SemVer {
391    fn from(v: Version) -> SemVer {
392        v.version
393    }
394}
395
396fn build_url(crate_name: &str) -> String {
397    format!(
398        "https://crates.io/api/v1/crates/{crate_name}",
399        crate_name = crate_name,
400    )
401}
402
403/// Check for version updates with asynchronous requests.
404#[cfg(feature = "async")]
405pub mod r#async;
406
407/// Check for version updates with blocking requests.
408#[cfg(feature = "blocking")]
409pub mod blocking;
410
411/// Gets the name of the crate as defined in *your* `Cargo.toml`.
412#[macro_export]
413macro_rules! crate_name {
414    () => {
415        env!("CARGO_PKG_NAME")
416    };
417}
418
419/// Gets the version of the crate as defined in *your* `Cargo.toml`.
420///
421/// Will be `&str`
422#[macro_export]
423macro_rules! crate_version {
424    () => {
425        env!("CARGO_PKG_VERSION")
426    };
427}
428
429/// Gets the major version of the crate as defined in *your* `Cargo.toml`.
430///
431/// Will be `&str`
432#[macro_export]
433macro_rules! crate_major_version {
434    () => {
435        env!("CARGO_PKG_VERSION_MAJOR")
436    };
437}
438
439/// Gets the minor version of the crate as defined in *your* `Cargo.toml`.
440///
441/// Will be `&str`
442#[macro_export]
443macro_rules! crate_minor_version {
444    () => {
445        env!("CARGO_PKG_VERSION_MINOR")
446    };
447}
448
449/// Gets the patch version of the crate as defined in *your* `Cargo.toml`.
450///
451/// Will be `&str`
452#[macro_export]
453macro_rules! crate_patch {
454    () => {
455        env!("CARGO_PKG_VERSION_PATCH")
456    };
457}
458
459/// Defines an appropriate user agent for making requests.
460///
461/// `"<your-crate-name>/<version>"`
462#[macro_export]
463macro_rules! user_agent {
464    () => {
465        concat!($crate::crate_name!(), "/", $crate::crate_version!())
466    };
467}
468
469#[cfg(not(any(feature = "async", feature = "blocking")))]
470compile_error!(
471    "\
472`check-latest` is almost completely useless without either `async` or \
473`blocking` enabled"
474);
475
476#[cfg(test)]
477mod tests {
478    use super::*;
479    use chrono::NaiveDateTime;
480    use lazy_static::lazy_static;
481
482    lazy_static! {
483        static ref DONT_CARE_DATETIME: DateTime<Utc> = {
484            let naive = NaiveDateTime::from_timestamp(0, 0);
485            DateTime::from_utc(naive, Utc)
486        };
487    }
488
489    #[test]
490    fn is_greater_semver() {
491        let version = Version {
492            version: SemVer::parse("1.2.3").unwrap(),
493            yanked: false,
494            created_at: DONT_CARE_DATETIME.clone(),
495        };
496        let semver = SemVer::parse("1.2.0").unwrap();
497        assert!(version > semver);
498    }
499
500    #[test]
501    fn is_lesser_semver() {
502        let version = Version {
503            version: SemVer::parse("1.2.3").unwrap(),
504            yanked: false,
505            created_at: DONT_CARE_DATETIME.clone(),
506        };
507        let semver = SemVer::parse("1.3.0").unwrap();
508        assert!(version < semver);
509    }
510
511    #[test]
512    fn is_greater_str() {
513        let version = Version {
514            version: SemVer::parse("1.2.3").unwrap(),
515            yanked: false,
516            created_at: DONT_CARE_DATETIME.clone(),
517        };
518        assert!(version > "1.2.0");
519    }
520
521    #[test]
522    fn is_lesser_str() {
523        let version = Version {
524            version: SemVer::parse("1.2.3").unwrap(),
525            yanked: false,
526            created_at: DONT_CARE_DATETIME.clone(),
527        };
528        assert!(version < "1.3.0");
529    }
530}