version_spec/
version_types.rs

1use crate::get_calver_regex;
2use crate::spec_error::SpecError;
3use semver::Version;
4use serde::{Deserialize, Serialize};
5use std::fmt;
6use std::ops::Deref;
7
8/// Container for a semantic version.
9#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
10pub struct SemVer(pub Version);
11
12impl SemVer {
13    /// Parse the string into a [`semver::Version`] type.
14    pub fn parse(value: &str) -> Result<Self, SpecError> {
15        Ok(Self(Version::parse(value)?))
16    }
17}
18
19impl Deref for SemVer {
20    type Target = Version;
21
22    fn deref(&self) -> &Self::Target {
23        &self.0
24    }
25}
26
27impl fmt::Display for SemVer {
28    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29        write!(f, "{}", self.0)
30    }
31}
32
33/// Container for a calendar version.
34#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
35pub struct CalVer(pub Version);
36
37impl CalVer {
38    /// If the provided value is a calver-like version string,
39    /// parse and convert it to a semver compatible version string,
40    /// so that we can utilize the [`semver::Version`] type.
41    pub fn parse(value: &str) -> Result<Self, SpecError> {
42        let Some(caps) = get_calver_regex().captures(value) else {
43            return Err(SpecError::InvalidCalverFormat);
44        };
45
46        // Short years (less than 4 characters) are relative
47        // from the year 2000, so let's enforce it. Is this correct?
48        // https://calver.org/#scheme
49        let year = caps
50            .name("year")
51            .map(|cap| cap.as_str().trim_start_matches('0'))
52            .unwrap_or("0");
53        let mut year_no: usize = year.parse().unwrap();
54
55        if year.len() < 4 {
56            year_no += 2000;
57        }
58
59        // Strip leading zeros from months and days. If the value is
60        // not provided, fallback to a zero, as calver is 1-index based
61        // and we can use this 0 for comparison.
62        let month = caps
63            .name("month")
64            .map(|cap| cap.as_str().trim_start_matches('0'))
65            .unwrap_or("0");
66
67        let day = caps
68            .name("day")
69            .map(|cap| cap.as_str().trim_start_matches('0'))
70            .unwrap_or("0");
71
72        let mut version = format!("{year_no}.{month}.{day}");
73
74        if let Some(pre) = caps.name("pre") {
75            version.push_str(pre.as_str());
76        }
77
78        if let Some(micro) = caps.name("micro") {
79            version.push('+');
80            version.push_str(micro.as_str());
81        }
82
83        Ok(Self(Version::parse(&version)?))
84    }
85}
86
87impl Deref for CalVer {
88    type Target = Version;
89
90    fn deref(&self) -> &Self::Target {
91        &self.0
92    }
93}
94
95impl fmt::Display for CalVer {
96    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97        let version = &self.0;
98
99        write!(f, "{:0>4}-{:0>2}", version.major, version.minor)?;
100
101        if version.patch > 0 {
102            write!(f, "-{:0>2}", version.patch)?;
103        }
104
105        // micro
106        if !version.build.is_empty() {
107            write!(f, ".{}", version.build)?;
108        }
109
110        if !version.pre.is_empty() {
111            write!(f, "-{}", version.pre)?;
112        }
113
114        Ok(())
115    }
116}