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}