version_number/version/
base.rs

1use crate::parsers::modular;
2use crate::{BaseVersionParser, FullVersion, ParserError};
3use std::fmt;
4
5/// A two-component `MAJOR.MINOR` version.
6///
7/// This version number is a subset of [`semver`]. In particular, it consists of the `MAJOR`
8/// and `MINOR` components, and leaves out the `PATCH` and additional labels for pre-release
9/// and build metadata.
10///
11/// If you require a version number which also includes the `PATCH` number,
12/// please see the [`FullVersion`] variant. For a [`semver`] compliant parser, you should use
13/// the `semver` [`crate`] instead.
14///
15/// [`semver`]: https://semver.org/spec/v2.0.0.html
16/// [`FullVersion`]: crate::FullVersion
17/// [`crate`]: https://crates.io/crates/semver
18#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
19pub struct BaseVersion {
20    /// A `major` version is incremented when backwards incompatible changes are made to a public
21    /// API.
22    ///
23    /// When this number equals `0`, the version is considered an *unstable initial development
24    /// version*.
25    pub major: u64,
26    /// The `minor` version is incremented when backwards compatibles changes are made to a public
27    /// API.
28    ///
29    /// When the version number is considered an *unstable initial development version*, it may also
30    /// be incremented for backwards incompatible changes.
31    pub minor: u64,
32}
33
34impl BaseVersion {
35    /// Instantiate a two component, version number with `MAJOR` and `MINOR` components.
36    ///
37    /// See [`BaseVersion`] for more.
38    ///
39    /// [`BaseVersion`]: crate::BaseVersion
40    pub fn new(major: u64, minor: u64) -> Self {
41        Self { major, minor }
42    }
43
44    /// Parse a two component, `major.minor` version number from a given input.
45    ///
46    /// Returns a [`ParserError`] if it fails to parse.
47    pub fn parse(input: &str) -> Result<Self, ParserError> {
48        modular::ModularParser.parse_base(input)
49    }
50
51    /// Convert this base version to a full version.
52    ///
53    /// This conversion is lossy because the `patch` value is not known to this BaseVersion, and
54    /// will initialize as `0`.
55    pub fn to_full_version_lossy(self) -> FullVersion {
56        FullVersion {
57            major: self.major,
58            minor: self.minor,
59            patch: 0,
60        }
61    }
62
63    /// Map a [`BaseVersion`] to `U`.
64    ///
65    /// # Example
66    ///
67    /// ```
68    /// use version_number::BaseVersion;
69    ///
70    /// // 🧑‍🔬
71    /// fn invert_version(v: BaseVersion) -> BaseVersion {
72    ///     BaseVersion::new(v.minor, v.major)
73    /// }
74    ///
75    /// let example = BaseVersion::new(1, 2);
76    ///
77    /// assert_eq!(example.map(invert_version), BaseVersion::new(2, 1));
78    /// ```
79    pub fn map<U, F>(self, fun: F) -> U
80    where
81        F: FnOnce(Self) -> U,
82    {
83        fun(self)
84    }
85}
86
87impl From<(u64, u64)> for BaseVersion {
88    fn from(tuple: (u64, u64)) -> Self {
89        BaseVersion {
90            major: tuple.0,
91            minor: tuple.1,
92        }
93    }
94}
95
96impl fmt::Display for BaseVersion {
97    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98        f.write_fmt(format_args!("{}.{}", self.major, self.minor))
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use crate::{BaseVersion, FullVersion};
105
106    #[test]
107    fn from_tuple() {
108        let major = 0;
109        let minor = 1;
110
111        assert_eq!(
112            BaseVersion { major, minor },
113            BaseVersion::from((major, minor))
114        );
115    }
116
117    #[yare::parameterized(
118        zeros = { BaseVersion { major: 0, minor: 0 }, "0.0" },
119        non_zero = { BaseVersion { major: 1, minor: 2 }, "1.2" },
120    )]
121    fn display(base_version: BaseVersion, expected: &str) {
122        let displayed = format!("{}", base_version);
123
124        assert_eq!(&displayed, expected);
125    }
126
127    #[yare::parameterized(
128        instance_0 = { BaseVersion::new(1, 0) },
129        instance_1 = { BaseVersion::new(1, 1) },
130        instance_m = { BaseVersion::new(1, u64::MAX) },
131    )]
132    fn to_full_version_lossy(base: BaseVersion) {
133        let converted = base.to_full_version_lossy();
134
135        assert_eq!(
136            converted,
137            FullVersion {
138                major: base.major,
139                minor: base.minor,
140                patch: 0,
141            }
142        )
143    }
144
145    #[test]
146    fn map() {
147        let version = BaseVersion::new(1, 2);
148        let mapped = version.map(|v| format!("Wowsies {}", v.major));
149
150        assert_eq!(mapped.as_str(), "Wowsies 1");
151    }
152}
153
154#[cfg(test)]
155mod ord_tests {
156    use crate::BaseVersion;
157    use std::cmp::Ordering;
158
159    #[yare::parameterized(
160        zero = { BaseVersion { major: 0, minor: 0 }, BaseVersion { major: 0, minor: 0 } },
161        ones = { BaseVersion { major: 1, minor: 1 }, BaseVersion { major: 1, minor: 1 } },
162    )]
163    fn equals(lhs: BaseVersion, rhs: BaseVersion) {
164        assert_eq!(lhs.cmp(&rhs), Ordering::Equal);
165    }
166
167    #[yare::parameterized(
168        minor_by_1 = { BaseVersion { major: 0, minor: 0 }, BaseVersion { major: 0, minor: 1 } },
169        major_by_1 = { BaseVersion { major: 1, minor: 0 }, BaseVersion { major: 2, minor: 0 } },
170    )]
171    fn less(lhs: BaseVersion, rhs: BaseVersion) {
172        assert_eq!(lhs.cmp(&rhs), Ordering::Less);
173    }
174
175    #[yare::parameterized(
176        minor_by_1 = { BaseVersion { major: 0, minor: 1 }, BaseVersion { major: 0, minor: 0 } },
177        major_by_1 = { BaseVersion { major: 1, minor: 0 }, BaseVersion { major: 0, minor: 0 } },
178    )]
179    fn greater(lhs: BaseVersion, rhs: BaseVersion) {
180        assert_eq!(lhs.cmp(&rhs), Ordering::Greater);
181    }
182}
183
184#[cfg(test)]
185mod partial_ord_tests {
186    use crate::BaseVersion;
187    use std::cmp::Ordering;
188
189    #[yare::parameterized(
190        zero = { BaseVersion { major: 0, minor: 0 }, BaseVersion { major: 0, minor: 0 } },
191        ones = { BaseVersion { major: 1, minor: 1 }, BaseVersion { major: 1, minor: 1 } },
192    )]
193    fn equals(lhs: BaseVersion, rhs: BaseVersion) {
194        assert_eq!(lhs.partial_cmp(&rhs), Some(Ordering::Equal));
195    }
196
197    #[yare::parameterized(
198        minor_by_1 = { BaseVersion { major: 0, minor: 0 }, BaseVersion { major: 0, minor: 1 } },
199        major_by_1 = { BaseVersion { major: 1, minor: 0 }, BaseVersion { major: 2, minor: 0 } },
200    )]
201    fn less(lhs: BaseVersion, rhs: BaseVersion) {
202        assert_eq!(lhs.partial_cmp(&rhs), Some(Ordering::Less));
203    }
204
205    #[yare::parameterized(
206        minor_by_1 = { BaseVersion { major: 0, minor: 1 }, BaseVersion { major: 0, minor: 0 } },
207        major_by_1 = { BaseVersion { major: 1, minor: 0 }, BaseVersion { major: 0, minor: 0 } },
208    )]
209    fn greater(lhs: BaseVersion, rhs: BaseVersion) {
210        assert_eq!(lhs.partial_cmp(&rhs), Some(Ordering::Greater));
211    }
212}
213
214#[cfg(test)]
215mod parse_base {
216    use crate::parsers::error::ExpectedError;
217    use crate::parsers::NumericError;
218    use crate::{BaseVersion, ParserError};
219
220    #[test]
221    fn ok() {
222        let version = BaseVersion::parse("1.2").unwrap();
223
224        assert_eq!(version, BaseVersion::new(1, 2));
225    }
226
227    #[test]
228    fn err_on_major_only() {
229        let result = BaseVersion::parse("1");
230
231        assert!(matches!(
232            result.unwrap_err(),
233            ParserError::Expected(ExpectedError::Separator { .. })
234        ));
235    }
236
237    #[test]
238    fn err_on_not_finished() {
239        let result = BaseVersion::parse("1.2.3");
240
241        assert!(matches!(
242            result.unwrap_err(),
243            ParserError::Expected(ExpectedError::EndOfInput { .. })
244        ));
245    }
246
247    #[test]
248    fn err_on_starts_with_0() {
249        let result = BaseVersion::parse("1.02");
250
251        assert!(matches!(
252            result.unwrap_err(),
253            ParserError::Numeric(NumericError::LeadingZero)
254        ));
255    }
256}