nreplops_tool/
version.rs

1// version.rs
2// Copyright 2024 Matti Hänninen
3//
4// Licensed under the Apache License, Version 2.0 (the "License"); you may not
5// use this file except in compliance with the License. You may obtain a copy of
6// the License at
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13// License for the specific language governing permissions and limitations under
14// the License.
15
16use std::{cmp, env, fmt, str};
17
18pub fn crate_version() -> Version {
19  let major = env!("CARGO_PKG_VERSION_MAJOR").parse::<u16>().unwrap();
20  let minor = env!("CARGO_PKG_VERSION_MINOR").parse::<u16>().unwrap();
21  let patch = env!("CARGO_PKG_VERSION_PATCH").parse::<u16>().unwrap();
22  Version::MajorMinorPatch(major, minor, patch)
23}
24
25#[derive(Clone, Debug)]
26pub enum Version {
27  Major(u16),
28  MajorMinor(u16, u16),
29  MajorMinorPatch(u16, u16, u16),
30}
31
32impl Version {
33  fn to_triplet(&self) -> (u16, u16, u16) {
34    match self {
35      Version::Major(a) => (*a, 0, 0),
36      Version::MajorMinor(a, b) => (*a, *b, 0),
37      Version::MajorMinorPatch(a, b, c) => (*a, *b, *c),
38    }
39  }
40
41  pub fn next_breaking(&self) -> Self {
42    use Version::*;
43    match self {
44      Major(0) => MajorMinorPatch(0, 1, 0),
45      Major(a) => MajorMinorPatch(a + 1, 0, 0),
46      MajorMinor(0, b) => MajorMinorPatch(0, b + 1, 0),
47      MajorMinor(a, _) => MajorMinorPatch(a + 1, 0, 0),
48      MajorMinorPatch(0, b, _) => MajorMinorPatch(0, b + 1, 0),
49      MajorMinorPatch(a, _, _) => MajorMinorPatch(a + 1, 0, 0),
50    }
51  }
52
53  pub fn cmp_to_range(&self, range: &VersionRange) -> cmp::Ordering {
54    use cmp::Ordering::*;
55    if let Some(ref start) = range.start {
56      if *self < *start {
57        return Less;
58      }
59    }
60    if let Some(ref end) = range.end {
61      if *end < *self || (!range.inclusive && *end == *self) {
62        return Greater;
63      }
64    }
65    Equal
66  }
67}
68
69impl cmp::PartialEq for Version {
70  fn eq(&self, rhs: &Self) -> bool {
71    self.to_triplet() == rhs.to_triplet()
72  }
73}
74
75impl cmp::PartialOrd for Version {
76  fn partial_cmp(&self, rhs: &Self) -> Option<cmp::Ordering> {
77    self.to_triplet().partial_cmp(&rhs.to_triplet())
78  }
79}
80
81impl str::FromStr for Version {
82  type Err = ParseVersionError;
83
84  fn from_str(s: &str) -> Result<Self, Self::Err> {
85    let mut it = s.split('.');
86    let Some(major_str) = it.next() else {
87      return Err(ParseVersionError)
88    };
89    let major = major_str.parse::<u16>().map_err(|_| ParseVersionError)?;
90    let Some(minor_str) = it.next() else {
91      return Ok(Version::Major(major));
92    };
93    let minor = minor_str.parse::<u16>().map_err(|_| ParseVersionError)?;
94    let Some(patch_str) = it.next() else {
95      return Ok(Version::MajorMinor(major, minor))
96    };
97    let patch = patch_str.parse::<u16>().map_err(|_| ParseVersionError)?;
98    if it.next().is_none() {
99      Ok(Version::MajorMinorPatch(major, minor, patch))
100    } else {
101      Err(ParseVersionError)
102    }
103  }
104}
105
106#[derive(Clone, Copy, Debug, PartialEq)]
107pub struct ParseVersionError;
108
109impl fmt::Display for Version {
110  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
111    use Version::*;
112    match self {
113      Major(a) => write!(f, "{}.0.0", a),
114      MajorMinor(a, b) => write!(f, "{}.{}.0", a, b),
115      MajorMinorPatch(a, b, c) => write!(f, "{}.{}.{}", a, b, c),
116    }
117  }
118}
119
120#[derive(Clone, Debug)]
121pub struct VersionRange {
122  pub start: Option<Version>,
123  pub end: Option<Version>,
124  pub inclusive: bool,
125}
126
127impl VersionRange {
128  pub fn non_breaking_from(start: &Version) -> Self {
129    Self {
130      start: Some(start.clone()),
131      end: Some(start.next_breaking()),
132      inclusive: false,
133    }
134  }
135}
136
137impl str::FromStr for VersionRange {
138  type Err = ParseVersionRangeError;
139
140  fn from_str(s: &str) -> Result<Self, Self::Err> {
141    fn parse_version(
142      s: &str,
143    ) -> Result<Option<Version>, ParseVersionRangeError> {
144      match s.parse() {
145        Ok(v) => Ok(Some(v)),
146        Err(_) => Err(ParseVersionRangeError),
147      }
148    }
149
150    fn parse_opt_version(
151      s: &str,
152    ) -> Result<Option<Version>, ParseVersionRangeError> {
153      if s.is_empty() {
154        Ok(None)
155      } else {
156        parse_version(s)
157      }
158    }
159
160    if let Some((start_str, end_str)) = s.split_once("..=") {
161      let start = parse_opt_version(start_str)?;
162      let end = parse_version(end_str)?;
163      if start.is_some() && end.is_some() && start > end {
164        Err(ParseVersionRangeError)
165      } else {
166        Ok(Self {
167          start,
168          end,
169          inclusive: true,
170        })
171      }
172    } else if let Some((start_str, end_str)) = s.split_once("..") {
173      let start = parse_opt_version(start_str)?;
174      let end = parse_opt_version(end_str)?;
175      if start.is_some() && end.is_some() && start >= end {
176        Err(ParseVersionRangeError)
177      } else {
178        Ok(Self {
179          start,
180          end,
181          inclusive: false,
182        })
183      }
184    } else {
185      Err(ParseVersionRangeError)
186    }
187  }
188}
189
190#[derive(Clone, Copy, Debug, PartialEq)]
191pub struct ParseVersionRangeError;
192
193impl fmt::Display for VersionRange {
194  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
195    if let Some(ref v) = self.start {
196      write!(f, "{}", v)?;
197    }
198    write!(f, "{}", if self.inclusive { "..=" } else { ".." })?;
199    if let Some(ref v) = self.end {
200      write!(f, "{}", v)?;
201    }
202    Ok(())
203  }
204}
205
206#[cfg(test)]
207mod test {
208
209  use super::*;
210
211  #[test]
212  fn version_equivalence() {
213    use Version::*;
214
215    assert_eq!(Major(1), Major(1));
216    assert_eq!(Major(1), MajorMinor(1, 0));
217    assert_eq!(Major(1), MajorMinorPatch(1, 0, 0));
218
219    assert_eq!(MajorMinor(1, 2), MajorMinor(1, 2));
220    assert_eq!(MajorMinor(1, 2), MajorMinorPatch(1, 2, 0));
221
222    assert_eq!(MajorMinorPatch(1, 2, 3), MajorMinorPatch(1, 2, 3));
223  }
224
225  #[test]
226  fn version_ordering() {
227    use Version::*;
228
229    assert!(Major(1) < Major(2));
230    assert!(Major(2) > Major(1));
231
232    assert!(MajorMinor(1, 2) < MajorMinor(1, 3));
233    assert!(MajorMinor(1, 3) > MajorMinor(1, 2));
234
235    assert!(MajorMinor(1, 2) < MajorMinor(2, 1));
236    assert!(MajorMinor(2, 1) > MajorMinor(1, 2));
237
238    assert!(Major(1) < MajorMinor(1, 2));
239    assert!(MajorMinor(1, 2) < Major(2));
240
241    assert!(MajorMinor(1, 2) > Major(1));
242    assert!(Major(2) > MajorMinor(1, 2));
243
244    assert!(MajorMinorPatch(1, 2, 3) < MajorMinorPatch(1, 2, 4));
245    assert!(MajorMinorPatch(1, 2, 4) > MajorMinorPatch(1, 2, 3));
246
247    assert!(MajorMinor(1, 2) < MajorMinorPatch(1, 2, 3));
248    assert!(Major(1) < MajorMinorPatch(1, 2, 3));
249    assert!(MajorMinorPatch(1, 2, 3) < MajorMinor(1, 3));
250    assert!(MajorMinorPatch(1, 2, 3) < Major(2));
251
252    assert!(MajorMinorPatch(1, 2, 3) > MajorMinor(1, 2));
253    assert!(MajorMinorPatch(1, 2, 3) > Major(1));
254    assert!(MajorMinor(1, 3) > MajorMinorPatch(1, 2, 3));
255    assert!(Major(2) > MajorMinorPatch(1, 2, 3));
256  }
257
258  #[test]
259  fn parse_good_version_strings() {
260    use Version::*;
261
262    assert!(match "1".parse::<Version>() {
263      Ok(Major(1)) => true,
264      _ => false,
265    });
266    assert!(match "1.2".parse::<Version>() {
267      Ok(MajorMinor(1, 2)) => true,
268      _ => false,
269    });
270    assert!(match "1.2.3".parse::<Version>() {
271      Ok(MajorMinorPatch(1, 2, 3)) => true,
272      _ => false,
273    });
274    assert!(match "123.456.789".parse::<Version>() {
275      Ok(MajorMinorPatch(123, 456, 789)) => true,
276      _ => false,
277    });
278    assert!(match "0.0.0".parse::<Version>() {
279      Ok(MajorMinorPatch(0, 0, 0)) => true,
280      _ => false,
281    });
282  }
283
284  #[test]
285  fn try_parsing_bad_version_strings() {
286    assert_eq!("".parse::<Version>(), Err(ParseVersionError));
287    assert_eq!("1 ".parse::<Version>(), Err(ParseVersionError));
288    assert_eq!(" 1".parse::<Version>(), Err(ParseVersionError));
289    assert_eq!(" ".parse::<Version>(), Err(ParseVersionError));
290    assert_eq!("1.".parse::<Version>(), Err(ParseVersionError));
291    assert_eq!("1.".parse::<Version>(), Err(ParseVersionError));
292    assert_eq!("1.2.".parse::<Version>(), Err(ParseVersionError));
293    assert_eq!("1.2.3.".parse::<Version>(), Err(ParseVersionError));
294    assert_eq!("1.2.3.4".parse::<Version>(), Err(ParseVersionError));
295    assert_eq!("whatever".parse::<Version>(), Err(ParseVersionError));
296  }
297
298  #[test]
299  fn display_version_string() {
300    use Version::*;
301
302    assert_eq!(format!("{}", Major(123)), "123.0.0");
303    assert_eq!(format!("{}", MajorMinor(123, 456)), "123.456.0");
304    assert_eq!(format!("{}", MajorMinorPatch(123, 456, 789)), "123.456.789");
305  }
306
307  #[test]
308  fn parse_good_version_ranges() {
309    use Version::*;
310
311    assert!(match "1..=1".parse() {
312      Ok(VersionRange {
313        start: Some(Major(1)),
314        end: Some(Major(1)),
315        inclusive: true,
316      }) => true,
317      bad => panic!("bad parse: {:#?}", bad),
318    });
319
320    assert!(match "1..=1.0.0".parse() {
321      Ok(VersionRange {
322        start: Some(Major(1)),
323        end: Some(MajorMinorPatch(1, 0, 0)),
324        inclusive: true,
325      }) => true,
326      bad => panic!("bad parse: {:#?}", bad),
327    });
328
329    assert!(match "1..1.0.1".parse() {
330      Ok(VersionRange {
331        start: Some(Major(1)),
332        end: Some(MajorMinorPatch(1, 0, 1)),
333        inclusive: false,
334      }) => true,
335      bad => panic!("bad parse: {:#?}", bad),
336    });
337
338    assert!(match "1..".parse() {
339      Ok(VersionRange {
340        start: Some(Major(1)),
341        end: None,
342        inclusive: false,
343      }) => true,
344      bad => panic!("bad parse: {:#?}", bad),
345    });
346
347    assert!(match "..1".parse() {
348      Ok(VersionRange {
349        start: None,
350        end: Some(Major(1)),
351        inclusive: false,
352      }) => true,
353      bad => panic!("bad parse: {:#?}", bad),
354    });
355
356    assert!(match "..=1".parse() {
357      Ok(VersionRange {
358        start: None,
359        end: Some(Major(1)),
360        inclusive: true,
361      }) => true,
362      bad => panic!("bad parse: {:#?}", bad),
363    });
364
365    assert!(match "..".parse() {
366      Ok(VersionRange {
367        start: None,
368        end: None,
369        inclusive: false,
370      }) => true,
371      bad => panic!("bad parse: {:#?}", bad),
372    });
373  }
374
375  #[test]
376  fn try_parsing_bad_version_ranges() {
377    assert_eq!(
378      "1..1".parse::<VersionRange>().unwrap_err(),
379      ParseVersionRangeError
380    );
381    assert_eq!(
382      "1.2.3..1.2.2".parse::<VersionRange>().unwrap_err(),
383      ParseVersionRangeError
384    );
385    assert_eq!(
386      "1.2.3..=1.2.2".parse::<VersionRange>().unwrap_err(),
387      ParseVersionRangeError
388    );
389    assert_eq!(
390      "..=".parse::<VersionRange>().unwrap_err(),
391      ParseVersionRangeError
392    );
393  }
394
395  #[test]
396  fn compare_version_against_range() {
397    use cmp::Ordering::*;
398
399    let point: VersionRange = "1.2.3..=1.2.3".parse().unwrap();
400
401    assert_eq!(
402      "1.2.2".parse::<Version>().unwrap().cmp_to_range(&point),
403      Less
404    );
405    assert_eq!(
406      "1.2.3".parse::<Version>().unwrap().cmp_to_range(&point),
407      Equal
408    );
409    assert_eq!(
410      "1.2.4".parse::<Version>().unwrap().cmp_to_range(&point),
411      Greater
412    );
413
414    let open: VersionRange = "1..2".parse().unwrap();
415
416    assert_eq!(
417      "0.65535.65535"
418        .parse::<Version>()
419        .unwrap()
420        .cmp_to_range(&open),
421      Less
422    );
423    assert_eq!(
424      "1.0.0".parse::<Version>().unwrap().cmp_to_range(&open),
425      Equal
426    );
427    assert_eq!(
428      "1.65535.65535"
429        .parse::<Version>()
430        .unwrap()
431        .cmp_to_range(&open),
432      Equal
433    );
434    assert_eq!(
435      "2.0.0".parse::<Version>().unwrap().cmp_to_range(&open),
436      Greater
437    );
438    assert_eq!(
439      "2.0.1".parse::<Version>().unwrap().cmp_to_range(&open),
440      Greater
441    );
442
443    let closed: VersionRange = "1..=2".parse().unwrap();
444
445    assert_eq!(
446      "0.65535.65535"
447        .parse::<Version>()
448        .unwrap()
449        .cmp_to_range(&closed),
450      Less
451    );
452    assert_eq!(
453      "1.0.0".parse::<Version>().unwrap().cmp_to_range(&closed),
454      Equal
455    );
456    assert_eq!(
457      "1.65535.65535"
458        .parse::<Version>()
459        .unwrap()
460        .cmp_to_range(&closed),
461      Equal
462    );
463    assert_eq!(
464      "2.0.0".parse::<Version>().unwrap().cmp_to_range(&closed),
465      Equal
466    );
467    assert_eq!(
468      "2.0.1".parse::<Version>().unwrap().cmp_to_range(&closed),
469      Greater
470    );
471  }
472}