Skip to main content

version_number/
lib.rs

1#![deny(missing_docs)]
2
3//! # version-number
4//!
5//! ## Synopsis
6//!
7//! A crate to represent and parse two- and three-component version numbers of the form `major.minor`,
8//! and `major.minor.patch` respectively. These version numbers are often seen within the Rust
9//! project manifests.
10//!
11//! ## Semver compatibility
12//!
13//! The version numbers accepted by this crate are a subset of semver version numbers,
14//! with the exception of also allowing two component (shorthand) `major.minor` versions.
15//!
16//! For example, `1.0` and `1.0.0` are both accepted by this library, while the former is
17//! rejected by [`semver`].
18//!
19//! In addition [`Version`] does not accept extra labels such as build parameters, which are
20//! an extension of the [`semver`] version number itself.
21//!
22//! In this crate, we call a two component `major.minor` version number a [`BaseVersion`], and
23//! we call a three component `major.minor.patch` version number a [`FullVersion`].
24//!
25//! [`semver`]: https://semver.org/spec/v2.0.0.html
26//! [`Version`]: crate::Version
27//! [`BaseVersion`]: crate::BaseVersion
28//! [`FullVersion`]: crate::FullVersion
29
30use std::cmp::Ordering;
31use std::fmt;
32use std::str::FromStr;
33
34use crate::parsers::original;
35
36pub use parsers::{BaseVersionParser, FullVersionParser, ParserError, VersionParser};
37#[cfg(feature = "semver")]
38pub use version::FromSemverError;
39pub use version::{BaseVersion, FullVersion};
40
41/// This crate contains multiple parsers.
42///
43/// In general, it's easiest to use the well tested [`parsers::original::Parser`], which is also used
44/// (currently) by [`Version::parse`].
45pub mod parsers;
46
47#[cfg(feature = "serde")]
48mod serde_impl;
49mod version;
50
51/// Top level errors for version-numbers.
52#[derive(Debug, thiserror::Error)]
53pub enum Error {
54    /// An error which specifies failure to parse a version number.
55    #[error(transparent)]
56    ParserError(#[from] ParserError),
57}
58
59/// A numbered version which is a two-component `major.minor` version number,
60/// or a three-component `major.minor.patch` version number.
61#[derive(Clone, Debug, Hash, Eq, PartialEq)]
62pub enum Version {
63    /// A two-component `major.minor` version.
64    Base(BaseVersion),
65    /// A three-component `major.minor.patch` version.
66    Full(FullVersion),
67}
68
69impl Version {
70    /// Parse a two- or three-component, `major.minor` or `major.minor.patch` respectively,
71    /// version number from a given input.
72    ///
73    /// Returns a [`Error::ParserError`] if it fails to parse.
74    pub fn parse(input: &str) -> Result<Self, Error> {
75        original::Parser::from(input.as_bytes())
76            .parse()
77            .map_err(|e| Error::from(Into::<ParserError>::into(e)))
78    }
79
80    /// Create a new two-component `major.minor` version number.
81    pub const fn new_base_version(major: u64, minor: u64) -> Self {
82        Self::Base(BaseVersion { major, minor })
83    }
84
85    /// Create a new three-component `major.minor.patch` version number.
86    pub const fn new_full_version(major: u64, minor: u64, patch: u64) -> Self {
87        Self::Full(FullVersion {
88            major,
89            minor,
90            patch,
91        })
92    }
93
94    /// Returns the `major` version component.
95    ///
96    /// Both the two- and three-component version number variants have a major version.
97    /// This is the leading component.
98    pub fn major(&self) -> u64 {
99        match self {
100            Self::Base(inner) => inner.major,
101            Self::Full(inner) => inner.major,
102        }
103    }
104
105    /// Returns the `minor` version component.
106    ///
107    /// Both the two- and three-component version number variants have a minor version.
108    /// This is the middle component.
109    pub fn minor(&self) -> u64 {
110        match self {
111            Self::Base(inner) => inner.minor,
112            Self::Full(inner) => inner.minor,
113        }
114    }
115
116    /// Returns the `patch` version component, if any.
117    ///
118    /// A three component `major.minor.patch` version will return a `Some(<version>)`,
119    /// while a two component `major.minor` version will return `None` instead.
120    ///
121    /// If it exists, it is the last component.
122    pub fn patch(&self) -> Option<u64> {
123        match self {
124            Self::Base(_) => None,
125            Self::Full(inner) => Some(inner.patch),
126        }
127    }
128
129    /// Convert this version to a three-component [`FullVersion`].
130    ///
131    /// A two-component version gets a `patch` of `0`, so `1.2` becomes `1.2.0`.
132    pub fn to_full_version_lossy(&self) -> FullVersion {
133        match self {
134            Self::Base(inner) => inner.to_full_version_lossy(),
135            Self::Full(inner) => *inner,
136        }
137    }
138
139    /// Compare `self` with a [`FullVersion`], using only the components which `self` has.
140    ///
141    /// A two-component version ignores the `patch` of `other`, so `1.2` is equal to both `1.2.0`
142    /// and `1.2.9`, while `1.2.0` is only equal to `1.2.0`.
143    ///
144    /// # Example
145    ///
146    /// ```
147    /// use std::cmp::Ordering;
148    /// use version_number::{FullVersion, Version};
149    ///
150    /// let base = Version::new_base_version(1, 2);
151    /// let full = Version::new_full_version(1, 2, 0);
152    ///
153    /// assert_eq!(base.is_compatible_with(&FullVersion::new(1, 2, 9)), Ordering::Equal);
154    /// assert_eq!(full.is_compatible_with(&FullVersion::new(1, 2, 9)), Ordering::Less);
155    /// assert_eq!(base.is_compatible_with(&FullVersion::new(1, 1, 9)), Ordering::Greater);
156    /// ```
157    // This is a method instead of a `PartialOrd` implementation, because the matching
158    // `PartialEq` would not be transitive: `1.2.0 == 1.2` and `1.2 == 1.2.9`, but `1.2.0 != 1.2.9`.
159    pub fn is_compatible_with(&self, other: &FullVersion) -> Ordering {
160        match self {
161            Self::Base(inner) => inner.cmp(&other.to_base_version_lossy()),
162            Self::Full(inner) => inner.cmp(other),
163        }
164    }
165
166    /// Check whether `other` matches `self`, using only the components which `self` has.
167    ///
168    /// See [`Version::is_compatible_with`] for how the versions are compared.
169    ///
170    /// # Example
171    ///
172    /// ```
173    /// use version_number::{FullVersion, Version};
174    ///
175    /// let base = Version::new_base_version(1, 2);
176    ///
177    /// assert!(base.matches(&FullVersion::new(1, 2, 0)));
178    /// assert!(base.matches(&FullVersion::new(1, 2, 9)));
179    /// assert!(!base.matches(&FullVersion::new(1, 3, 0)));
180    /// ```
181    pub fn matches(&self, other: &FullVersion) -> bool {
182        self.is_compatible_with(other) == Ordering::Equal
183    }
184
185    /// Check of which variant `self` is.
186    pub fn is(&self, variant: Variant) -> bool {
187        match self {
188            Version::Base(_) => matches!(variant, Variant::Base),
189            Version::Full(_) => matches!(variant, Variant::Full),
190        }
191    }
192
193    /// Map a [`Version`] to `U`.
194    ///
195    /// # Example
196    ///
197    /// ```
198    /// use version_number::{BaseVersion, FullVersion, Version};
199    ///
200    /// // 🧑‍🔬
201    /// fn invert_version(v: Version) -> Version {
202    ///     match v {
203    ///         Version::Base(base) => Version::Base(BaseVersion::new(base.minor, base.major)),
204    ///         Version::Full(full) => Version::Full(FullVersion::new(full.patch, full.minor, full.major))
205    ///     }
206    /// }
207    ///
208    /// let base_example = Version::Base(BaseVersion::new(1, 2));
209    /// let full_example = Version::Full(FullVersion::new(1, 2, 3));
210    ///
211    /// assert_eq!(base_example.map(invert_version), Version::Base(BaseVersion::new(2, 1)));
212    /// assert_eq!(full_example.map(invert_version), Version::Full(FullVersion::new(3, 2, 1)));
213    /// ```
214    #[inline]
215    pub fn map<U, F>(self, fun: F) -> U
216    where
217        F: FnOnce(Self) -> U,
218    {
219        fun(self)
220    }
221
222    /// Map over the `major` version component of the [`Version`].
223    ///
224    /// # Example
225    ///
226    /// ```
227    /// use version_number::{BaseVersion, FullVersion, Version};
228    ///
229    /// let base_example = Version::Base(BaseVersion::new(0, 0));
230    /// let full_example = Version::Full(FullVersion::new(0, 0, 0));
231    ///
232    /// assert_eq!(base_example.map_major(|a| a + 1), Version::Base(BaseVersion::new(1, 0)));
233    /// assert_eq!(full_example.map_major(|a| a + 1), Version::Full(FullVersion::new(1, 0, 0)));
234    /// ```
235    #[inline]
236    pub fn map_major<F>(self, fun: F) -> Self
237    where
238        F: FnOnce(u64) -> u64,
239    {
240        self.map(|v| match v {
241            Self::Base(base) => Version::Base(BaseVersion::new(fun(base.major), base.minor)),
242            Self::Full(full) => {
243                Version::Full(FullVersion::new(fun(full.major), full.minor, full.patch))
244            }
245        })
246    }
247
248    /// Map over the `minor` version component of the [`Version`].
249    ///
250    /// # Example
251    ///
252    /// ```
253    /// use version_number::{BaseVersion, FullVersion, Version};
254    ///
255    /// let base_example = Version::Base(BaseVersion::new(0, 0));
256    /// let full_example = Version::Full(FullVersion::new(0, 0, 0));
257    ///
258    /// assert_eq!(base_example.map_minor(|a| a + 1), Version::Base(BaseVersion::new(0, 1)));
259    /// assert_eq!(full_example.map_minor(|a| a + 1), Version::Full(FullVersion::new(0, 1, 0)));
260    /// ```
261    #[inline]
262    pub fn map_minor<F>(self, fun: F) -> Self
263    where
264        F: FnOnce(u64) -> u64,
265    {
266        self.map(|v| match v {
267            Self::Base(base) => Version::Base(BaseVersion::new(base.major, fun(base.minor))),
268            Self::Full(full) => {
269                Version::Full(FullVersion::new(full.major, fun(full.minor), full.patch))
270            }
271        })
272    }
273
274    /// Map over the `patch` version component of the [`Version`].
275    /// If no `patch` version exists (in case the [`Version`] consists of two components),
276    /// then the original version is returned.
277    ///
278    /// # Example
279    ///
280    /// ```
281    /// use version_number::{BaseVersion, FullVersion, Version};
282    ///
283    /// let base_example = Version::Base(BaseVersion::new(0, 0));
284    /// let full_example = Version::Full(FullVersion::new(0, 0, 0));
285    ///
286    /// assert_eq!(base_example.map_patch(|a| a + 1), Version::Base(BaseVersion::new(0, 0)));
287    /// assert_eq!(full_example.map_patch(|a| a + 1), Version::Full(FullVersion::new(0, 0, 1)));
288    /// ```
289    #[inline]
290    pub fn map_patch<F>(self, fun: F) -> Self
291    where
292        F: FnOnce(u64) -> u64,
293    {
294        self.map(|v| match v {
295            Self::Base(base) => Version::Base(BaseVersion::new(base.major, base.minor)),
296            Self::Full(full) => {
297                Version::Full(FullVersion::new(full.major, full.minor, fun(full.patch)))
298            }
299        })
300    }
301}
302
303impl FromStr for Version {
304    type Err = Error;
305
306    fn from_str(input: &str) -> Result<Self, Error> {
307        original::Parser::from_slice(input.as_bytes())
308            .parse()
309            .map_err(|e| Error::from(Into::<ParserError>::into(e)))
310    }
311}
312
313impl fmt::Display for Version {
314    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
315        match self {
316            Self::Base(inner) => fmt::Display::fmt(&inner, f),
317            Self::Full(inner) => fmt::Display::fmt(&inner, f),
318        }
319    }
320}
321
322impl From<BaseVersion> for Version {
323    fn from(version: BaseVersion) -> Self {
324        Self::Base(version)
325    }
326}
327
328impl From<FullVersion> for Version {
329    fn from(version: FullVersion) -> Self {
330        Self::Full(version)
331    }
332}
333
334impl From<(u64, u64)> for Version {
335    fn from(tuple: (u64, u64)) -> Self {
336        Self::Base(BaseVersion::from(tuple))
337    }
338}
339
340impl From<(u64, u64, u64)> for Version {
341    fn from(tuple: (u64, u64, u64)) -> Self {
342        Self::Full(FullVersion::from(tuple))
343    }
344}
345
346/// Type used to indicate which variant of a [`Version`] is used.
347/// The options are [`Base`] for [`Version::Base`], and [`Full`] for [`Version::Full`].
348///
349/// [`Version`]: crate::Version
350/// [`Base`]: crate::Variant::Base
351/// [`Version::Base`]: crate::Version::Base
352/// [`Full`]: crate::Variant::Full
353/// [`Version::Full`]: crate::Version::Full
354#[derive(Copy, Clone, Debug)]
355pub enum Variant {
356    /// Indicates a [`Version::Base`] is used.
357    ///
358    /// [`Version::Base`]: crate::Version::Base
359    Base,
360    /// Indicates a [`Version::Full`] is used.
361    ///
362    /// [`Version::Full`]: crate::Version::Full
363    Full,
364}
365
366#[cfg(test)]
367mod tests {
368    use crate::{BaseVersion, FullVersion, Variant, Version};
369    use std::cmp::Ordering;
370
371    #[yare::parameterized(
372        base = { Version::new_base_version(1, 2), FullVersion::new(1, 2, 0) },
373        full = { Version::new_full_version(1, 2, 3), FullVersion::new(1, 2, 3) },
374    )]
375    fn to_full_version_lossy(version: Version, expected: FullVersion) {
376        assert_eq!(version.to_full_version_lossy(), expected);
377    }
378
379    #[yare::parameterized(
380        base_eq_patch_zero = { Version::new_base_version(1, 56), FullVersion::new(1, 56, 0), Ordering::Equal },
381        base_eq_patch_nonzero = { Version::new_base_version(1, 56), FullVersion::new(1, 56, 99), Ordering::Equal },
382        base_lt_minor = { Version::new_base_version(1, 56), FullVersion::new(1, 57, 0), Ordering::Less },
383        base_lt_major = { Version::new_base_version(1, 56), FullVersion::new(2, 0, 0), Ordering::Less },
384        base_gt_minor = { Version::new_base_version(1, 56), FullVersion::new(1, 55, 99), Ordering::Greater },
385        base_gt_major = { Version::new_base_version(2, 0), FullVersion::new(1, 99, 99), Ordering::Greater },
386        full_eq = { Version::new_full_version(1, 56, 1), FullVersion::new(1, 56, 1), Ordering::Equal },
387        full_lt_patch = { Version::new_full_version(1, 56, 0), FullVersion::new(1, 56, 1), Ordering::Less },
388        full_lt_minor = { Version::new_full_version(1, 56, 9), FullVersion::new(1, 57, 0), Ordering::Less },
389        full_lt_major = { Version::new_full_version(1, 99, 9), FullVersion::new(2, 0, 0), Ordering::Less },
390        full_gt_patch = { Version::new_full_version(1, 56, 1), FullVersion::new(1, 56, 0), Ordering::Greater },
391        full_gt_minor = { Version::new_full_version(1, 57, 0), FullVersion::new(1, 56, 9), Ordering::Greater },
392        full_gt_major = { Version::new_full_version(2, 0, 0), FullVersion::new(1, 99, 9), Ordering::Greater },
393    )]
394    fn is_compatible_with(version: Version, other: FullVersion, expected: Ordering) {
395        assert_eq!(version.is_compatible_with(&other), expected);
396    }
397
398    #[yare::parameterized(
399        base_patch_zero = { Version::new_base_version(1, 56), FullVersion::new(1, 56, 0), true },
400        base_patch_nonzero = { Version::new_base_version(1, 56), FullVersion::new(1, 56, 3), true },
401        base_other_minor = { Version::new_base_version(1, 56), FullVersion::new(1, 55, 0), false },
402        base_other_major = { Version::new_base_version(1, 56), FullVersion::new(2, 56, 0), false },
403        full_same = { Version::new_full_version(1, 56, 3), FullVersion::new(1, 56, 3), true },
404        full_other_patch = { Version::new_full_version(1, 56, 0), FullVersion::new(1, 56, 3), false },
405    )]
406    fn matches(version: Version, other: FullVersion, expected: bool) {
407        assert_eq!(version.matches(&other), expected);
408    }
409
410    #[test]
411    fn from_base_version() {
412        let version = Version::from(BaseVersion::new(1, 2));
413
414        assert_eq!(version, Version::Base(BaseVersion::new(1, 2)));
415    }
416
417    #[test]
418    fn from_full_version() {
419        let version = Version::from(FullVersion::new(1, 2, 3));
420
421        assert_eq!(version, Version::Full(FullVersion::new(1, 2, 3)));
422    }
423
424    #[test]
425    fn const_constructors() {
426        const BASE: Version = Version::new_base_version(1, 2);
427        const FULL: Version = Version::new_full_version(1, 2, 3);
428
429        assert_eq!(BASE, Version::Base(BaseVersion::new(1, 2)));
430        assert_eq!(FULL, Version::Full(FullVersion::new(1, 2, 3)));
431    }
432
433    #[test]
434    fn is_base_variant() {
435        let version = Version::Base(BaseVersion::new(0, 0));
436
437        assert!(version.is(Variant::Base));
438        assert!(!version.is(Variant::Full));
439    }
440
441    #[test]
442    fn is_full_variant() {
443        let version = Version::Full(FullVersion::new(0, 0, 0));
444
445        assert!(version.is(Variant::Full));
446        assert!(!version.is(Variant::Base));
447    }
448
449    #[test]
450    fn map() {
451        let version = Version::Base(BaseVersion::new(0, 0));
452
453        let mapped = version.map(|v| match v {
454            Version::Base(base) => Version::Full(FullVersion::new(base.major, base.minor, 999)),
455            v => v,
456        });
457
458        assert_eq!(mapped, Version::Full(FullVersion::new(0, 0, 999)));
459    }
460
461    #[yare::parameterized(
462        base = { Version::Base(BaseVersion::new(0, 0)) },
463        full = { Version::Full(FullVersion::new(0, 0, 0)) },
464    )]
465    fn map_major(version: Version) {
466        let mapped = version.map_major(|_v| 999);
467
468        assert_eq!(mapped.major(), 999);
469    }
470
471    #[yare::parameterized(
472        base = { Version::Base(BaseVersion::new(0, 0)) },
473        full = { Version::Full(FullVersion::new(0, 0, 0)) },
474    )]
475    fn map_minor(version: Version) {
476        let mapped = version.map_minor(|_v| 999);
477
478        assert_eq!(mapped.minor(), 999);
479    }
480
481    #[test]
482    fn map_patch_base() {
483        let version = Version::Base(BaseVersion::new(0, 0));
484        let mapped = version.map_patch(|_v| 999);
485
486        assert!(mapped.patch().is_none());
487    }
488
489    #[test]
490    fn map_patch_full() {
491        let version = Version::Full(FullVersion::new(0, 0, 0));
492        let mapped = version.map_patch(|_v| 999);
493
494        assert_eq!(mapped.patch().unwrap(), 999);
495    }
496}