Skip to main content

version_number/version/
base.rs

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