Skip to main content

version_number/version/
full.rs

1use crate::parsers::modular;
2use crate::{BaseVersion, FullVersionParser, ParserError};
3#[cfg(feature = "semver")]
4use std::convert::TryFrom;
5use std::fmt;
6use std::str::FromStr;
7
8/// A three-component `MAJOR.MINOR.PATCH` version.
9///
10/// This version number is a subset of [`semver`]. In particular, it consists of the `MAJOR`.
11/// `MINOR` and `PATCH` components, and leaves out the additional labels for pre-release and build
12/// metadata.
13///
14/// If you require a version number which also discards the `PATCH` number,
15/// please see the [`BaseVersion`] variant.
16///
17/// For a [`semver`] compliant parser, you should use the `semver` [`crate`] instead.
18///
19/// # Converting to a semver::Version
20///
21/// This version type may be converted to a [`semver::Version`] using the [`From`] trait, assuming
22/// the `semver` feature is enabled.
23///
24///
25/// [`semver`]: https://semver.org/spec/v2.0.0.html
26/// [`BaseVersion`]: crate::BaseVersion
27/// [`crate`]: https://crates.io/crates/semver
28/// [`semver::Version`]: https://docs.rs/semver/1/semver/struct.Version.html
29/// [`From`]: https://doc.rust-lang.org/std/convert/trait.From.html
30#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
31pub struct FullVersion {
32    /// A `major` version is incremented when backwards incompatible changes are made to a public
33    /// API.
34    ///
35    /// When this number equals `0`, the version is considered an *unstable initial development
36    /// version*.
37    pub major: u64,
38    /// The `minor` version is incremented when backwards compatibles changes are made to a public
39    /// API.
40    ///
41    /// When the version number is considered an *unstable initial development version*, it may also
42    /// be incremented for backwards incompatible changes.
43    pub minor: u64,
44    /// The `patch` version is incremented when backwards compatibles bug fixes are made.
45    pub patch: u64,
46}
47
48impl FullVersion {
49    /// Instantiate a three component, version number with `MAJOR`, `MINOR` and `PATCH` components.
50    ///
51    /// See [`FullVersion`] for more.
52    ///
53    /// [`FullVersion`]: crate::FullVersion
54    pub const fn new(major: u64, minor: u64, patch: u64) -> Self {
55        Self {
56            major,
57            minor,
58            patch,
59        }
60    }
61
62    /// Parse a three component, `major.minor.patch` version number from a given input.
63    ///
64    /// Returns a [`ParserError`] if it fails to parse.
65    pub fn parse(input: &str) -> Result<Self, ParserError> {
66        modular::ModularParser.parse_full(input)
67    }
68
69    /// Convert this full version to a base version.
70    ///
71    /// This conversion is lossy because the `patch` value is lost upon conversion.
72    pub fn to_base_version_lossy(self) -> BaseVersion {
73        BaseVersion {
74            major: self.major,
75            minor: self.minor,
76        }
77    }
78
79    /// Map a [`FullVersion`] to `U`.
80    ///
81    /// # Example
82    ///
83    /// ```
84    /// use version_number::FullVersion;
85    ///
86    /// // 🧑‍🔬
87    /// fn invert_version(v: FullVersion) -> FullVersion {
88    ///     FullVersion::new(v.patch, v.minor, v.major)
89    /// }
90    ///
91    /// let example = FullVersion::new(1, 2, 3);
92    ///
93    /// assert_eq!(example.map(invert_version), FullVersion::new(3, 2, 1));
94    /// ```
95    pub fn map<U, F>(self, fun: F) -> U
96    where
97        F: FnOnce(Self) -> U,
98    {
99        fun(self)
100    }
101}
102
103#[cfg(feature = "semver")]
104impl From<FullVersion> for semver::Version {
105    /// Convert the given [`FullVersion`] to a [`semver::Version`].
106    ///
107    /// Requires the `semver` feature to be enabled.
108    ///
109    /// # Example
110    ///
111    /// ```
112    /// # use version_number::FullVersion;
113    ///
114    /// let version = FullVersion::new(1, 2, 3);
115    /// let converted: semver::Version = version.into();
116    ///
117    /// assert_eq!(converted, semver::Version::new(1, 2, 3));
118    /// ```
119    ///
120    /// [`FullVersion`]: crate::FullVersion
121    /// [`semver::Version`]: https://docs.rs/semver/1/semver/struct.Version.html
122    fn from(version: FullVersion) -> Self {
123        semver::Version::new(version.major, version.minor, version.patch)
124    }
125}
126
127#[cfg(feature = "semver")]
128impl TryFrom<&semver::Version> for FullVersion {
129    type Error = FromSemverError;
130
131    /// Convert the given [`semver::Version`] to a [`FullVersion`].
132    ///
133    /// Requires the `semver` feature to be enabled.
134    ///
135    /// Fails if the version has a pre-release or build metadata label, since a [`FullVersion`]
136    /// can't hold those.
137    ///
138    /// # Example
139    ///
140    /// ```
141    /// # use version_number::FullVersion;
142    /// # use std::convert::TryFrom;
143    ///
144    /// let version = semver::Version::new(1, 2, 3);
145    /// let converted = FullVersion::try_from(&version).unwrap();
146    ///
147    /// assert_eq!(converted, FullVersion::new(1, 2, 3));
148    ///
149    /// let pre_release = semver::Version::parse("1.2.3-beta.1").unwrap();
150    /// assert!(FullVersion::try_from(&pre_release).is_err());
151    /// ```
152    ///
153    /// [`FullVersion`]: crate::FullVersion
154    /// [`semver::Version`]: https://docs.rs/semver/1/semver/struct.Version.html
155    fn try_from(version: &semver::Version) -> Result<Self, Self::Error> {
156        if !version.pre.is_empty() {
157            return Err(FromSemverError::PreRelease(version.pre.clone()));
158        }
159
160        if !version.build.is_empty() {
161            return Err(FromSemverError::BuildMetadata(version.build.clone()));
162        }
163
164        Ok(FullVersion::new(
165            version.major,
166            version.minor,
167            version.patch,
168        ))
169    }
170}
171
172/// The error returned when a [`semver::Version`] can't be converted to a [`FullVersion`].
173///
174/// [`FullVersion`]: crate::FullVersion
175/// [`semver::Version`]: https://docs.rs/semver/1/semver/struct.Version.html
176#[cfg(feature = "semver")]
177#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
178pub enum FromSemverError {
179    /// The version has a pre-release label, like the `beta.1` in `1.2.3-beta.1`.
180    #[error("Pre-release labels are not supported, but got '{0}'")]
181    PreRelease(semver::Prerelease),
182
183    /// The version has a build metadata label, like the `abc` in `1.2.3+abc`.
184    #[error("Build metadata labels are not supported, but got '{0}'")]
185    BuildMetadata(semver::BuildMetadata),
186}
187
188impl From<(u64, u64, u64)> for FullVersion {
189    fn from(tuple: (u64, u64, u64)) -> Self {
190        FullVersion {
191            major: tuple.0,
192            minor: tuple.1,
193            patch: tuple.2,
194        }
195    }
196}
197
198impl fmt::Display for FullVersion {
199    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200        f.write_fmt(format_args!("{}.{}.{}", self.major, self.minor, self.patch))
201    }
202}
203
204impl FromStr for FullVersion {
205    type Err = ParserError;
206
207    fn from_str(input: &str) -> Result<Self, Self::Err> {
208        Self::parse(input)
209    }
210}
211
212#[cfg(all(test, feature = "semver"))]
213mod semver_tests {
214    use crate::{FromSemverError, FullVersion};
215    use std::convert::TryFrom;
216
217    #[yare::parameterized(
218        zeros = { "0.0.0", FullVersion::new(0, 0, 0) },
219        regular = { "1.2.3", FullVersion::new(1, 2, 3) },
220        large = { "18446744073709551615.0.1", FullVersion::new(u64::MAX, 0, 1) },
221    )]
222    fn try_from_semver_ok(input: &str, expected: FullVersion) {
223        let version = semver::Version::parse(input).unwrap();
224
225        assert_eq!(FullVersion::try_from(&version).unwrap(), expected);
226    }
227
228    #[yare::parameterized(
229        nightly = { "1.2.3-nightly", "nightly" },
230        beta = { "1.2.3-beta.1", "beta.1" },
231        pre_release_and_build = { "1.2.3-beta.1+abc", "beta.1" },
232    )]
233    fn try_from_semver_pre_release(input: &str, expected: &str) {
234        let version = semver::Version::parse(input).unwrap();
235
236        assert_eq!(
237            FullVersion::try_from(&version).unwrap_err(),
238            FromSemverError::PreRelease(semver::Prerelease::new(expected).unwrap())
239        );
240    }
241
242    #[test]
243    fn try_from_semver_build_metadata() {
244        let version = semver::Version::parse("1.2.3+abc").unwrap();
245
246        assert_eq!(
247            FullVersion::try_from(&version).unwrap_err(),
248            FromSemverError::BuildMetadata(semver::BuildMetadata::new("abc").unwrap())
249        );
250    }
251
252    #[test]
253    fn round_trip() {
254        let version = FullVersion::new(1, 2, 3);
255        let converted = semver::Version::from(version);
256
257        assert_eq!(FullVersion::try_from(&converted).unwrap(), version);
258    }
259}
260
261#[cfg(test)]
262mod tests {
263    use crate::{BaseVersion, FullVersion};
264
265    #[test]
266    fn from_tuple() {
267        let major = 0;
268        let minor = 1;
269        let patch = 2;
270
271        assert_eq!(
272            FullVersion {
273                major,
274                minor,
275                patch
276            },
277            FullVersion::from((major, minor, patch))
278        );
279    }
280
281    #[yare::parameterized(
282        zeros = { FullVersion { major: 0, minor: 0, patch: 0 }, "0.0.0" },
283        non_zero = { FullVersion { major: 1, minor: 2, patch: 3 }, "1.2.3" },
284    )]
285    fn display(base_version: FullVersion, expected: &str) {
286        let displayed = format!("{}", base_version);
287
288        assert_eq!(&displayed, expected);
289    }
290
291    #[test]
292    fn to_base_version_lossy() {
293        let full = FullVersion {
294            major: 1,
295            minor: 2,
296            patch: 3,
297        };
298        let converted = full.to_base_version_lossy();
299
300        assert_eq!(BaseVersion { major: 1, minor: 2 }, converted)
301    }
302
303    #[test]
304    fn map() {
305        let version = BaseVersion::new(1, 2);
306        let mapped = version.map(|v| ("Everything is awesome", v.major, v.minor));
307
308        assert_eq!(mapped.1, 1);
309        assert_eq!(mapped.2, 2);
310    }
311}
312
313#[cfg(test)]
314mod ord_tests {
315    use crate::FullVersion;
316    use std::cmp::Ordering;
317
318    #[yare::parameterized(
319        zero = { FullVersion { major: 0, minor: 0, patch: 0 }, FullVersion { major: 0, minor: 0, patch: 0 } },
320        ones = { FullVersion { major: 1, minor: 1, patch: 1 }, FullVersion { major: 1, minor: 1, patch: 1 } },
321    )]
322    fn equals(lhs: FullVersion, rhs: FullVersion) {
323        assert_eq!(lhs.cmp(&rhs), Ordering::Equal);
324    }
325
326    #[yare::parameterized(
327        major_by_1 = { FullVersion { major: 0, minor: 0, patch: 0 }, FullVersion { major: 1, minor: 0, patch: 0 } },
328        minor_by_1 = { FullVersion { major: 0, minor: 0, patch: 0 }, FullVersion { major: 0, minor: 1, patch: 0 } },
329        patch_by_1 = { FullVersion { major: 0, minor: 0, patch: 0 }, FullVersion { major: 0, minor: 0, patch: 1 } },
330    )]
331    fn less(lhs: FullVersion, rhs: FullVersion) {
332        assert_eq!(lhs.cmp(&rhs), Ordering::Less);
333    }
334
335    #[yare::parameterized(
336        major_by_1 = { FullVersion { major: 1, minor: 0, patch: 0 }, FullVersion { major: 0, minor: 0, patch: 0 } },
337        minor_by_1 = { FullVersion { major: 0, minor: 1, patch: 0 }, FullVersion { major: 0, minor: 0, patch: 0 } },
338        patch_by_1 = { FullVersion { major: 0, minor: 0, patch: 1 }, FullVersion { major: 0, minor: 0, patch: 0 } },
339    )]
340    fn greater(lhs: FullVersion, rhs: FullVersion) {
341        assert_eq!(lhs.cmp(&rhs), Ordering::Greater);
342    }
343}
344
345#[cfg(test)]
346mod partial_ord_tests {
347    use crate::FullVersion;
348    use std::cmp::Ordering;
349
350    #[yare::parameterized(
351        zero = { FullVersion { major: 0, minor: 0, patch: 0 }, FullVersion { major: 0, minor: 0, patch: 0 } },
352        ones = { FullVersion { major: 1, minor: 1, patch: 1 }, FullVersion { major: 1, minor: 1, patch: 1 } },
353    )]
354    fn equals(lhs: FullVersion, rhs: FullVersion) {
355        assert_eq!(lhs.partial_cmp(&rhs), Some(Ordering::Equal));
356    }
357
358    #[yare::parameterized(
359        major_by_1 = { FullVersion { major: 0, minor: 0, patch: 0 }, FullVersion { major: 1, minor: 0, patch: 0 } },
360        minor_by_1 = { FullVersion { major: 0, minor: 0, patch: 0 }, FullVersion { major: 0, minor: 1, patch: 0 } },
361        patch_by_1 = { FullVersion { major: 0, minor: 0, patch: 0 }, FullVersion { major: 0, minor: 0, patch: 1 } },
362    )]
363    fn less(lhs: FullVersion, rhs: FullVersion) {
364        assert_eq!(lhs.partial_cmp(&rhs), Some(Ordering::Less));
365    }
366
367    #[yare::parameterized(
368        major_by_1 = { FullVersion { major: 1, minor: 0, patch: 0 }, FullVersion { major: 0, minor: 0, patch: 0 } },
369        minor_by_1 = { FullVersion { major: 0, minor: 1, patch: 0 }, FullVersion { major: 0, minor: 0, patch: 0 } },
370        patch_by_1 = { FullVersion { major: 0, minor: 0, patch: 1 }, FullVersion { major: 0, minor: 0, patch: 0 } },
371    )]
372    fn greater(lhs: FullVersion, rhs: FullVersion) {
373        assert_eq!(lhs.partial_cmp(&rhs), Some(Ordering::Greater));
374    }
375}
376
377#[cfg(test)]
378mod parse_full {
379    use crate::parsers::NumericError;
380    use crate::parsers::error::ExpectedError;
381    use crate::{FullVersion, ParserError};
382
383    #[test]
384    fn ok() {
385        let version = FullVersion::parse("1.2.3").unwrap();
386
387        assert_eq!(version, FullVersion::new(1, 2, 3));
388    }
389
390    #[test]
391    fn err_on_base_only() {
392        let result = FullVersion::parse("1.2");
393
394        assert!(matches!(
395            result.unwrap_err(),
396            ParserError::Expected(ExpectedError::Separator { .. })
397        ));
398    }
399
400    #[test]
401    fn err_on_not_finished() {
402        let result = FullVersion::parse("1.2.3.");
403
404        assert!(matches!(
405            result.unwrap_err(),
406            ParserError::Expected(ExpectedError::EndOfInput { .. })
407        ));
408    }
409
410    #[test]
411    fn err_on_starts_with_0() {
412        let result = FullVersion::parse("1.2.03");
413
414        assert!(matches!(
415            result.unwrap_err(),
416            ParserError::Numeric(NumericError::LeadingZero)
417        ));
418    }
419}