1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
mod release_type;

use anyhow::{anyhow, Result};
use regex::Regex;
pub use release_type::ReleaseType;

/// <https://regex101.com/r/VX7uQk>
pub const SEMVER_REGEX: &str =
  r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([a-zA-Z-]+)(?:\.(\d+)))?$";

#[derive(Clone, Debug)]
pub struct Version {
  pub major: usize,
  pub minor: usize,
  pub patch: usize,
  pub pre_id: Option<String>,
  pub pre_version: Option<usize>,
}

impl Version {
  pub fn new(raw: &str) -> Result<Self> {
    let raw = raw.trim().to_owned();
    if !is_valid(&raw) {
      return Err(anyhow!("Invalid semver: {}", raw));
    }

    let regex = Regex::new(SEMVER_REGEX).unwrap();
    let groups = regex.captures(&raw).unwrap();

    let major = groups.get(1).ok_or(anyhow!("Invalid major: {}", raw))?;
    let minor = groups.get(2).ok_or(anyhow!("Invalid minor: {}", raw))?;
    let patch = groups.get(3).ok_or(anyhow!("Invalid patch: {}", raw))?;

    let pre_id = groups.get(4).map(|id| id.as_str().to_owned());

    let pre_version = match groups.get(5) {
      Some(v) if pre_id.is_some() => Some(v.as_str().parse::<usize>()?),
      _ => None,
    };

    let version = Self {
      major: major.as_str().parse::<usize>()?,
      minor: minor.as_str().parse::<usize>()?,
      patch: patch.as_str().parse::<usize>()?,
      pre_id,
      pre_version,
    };

    Ok(version)
  }

  /// Increment the version by a release type.
  pub fn inc(&self, release_type: &ReleaseType, pre_id: Option<&str>) -> Result<Version> {
    let version = match release_type {
      ReleaseType::Major => Version {
        major: self.major + 1,
        minor: 0,
        patch: 0,
        pre_id: None,
        pre_version: None,
      },
      ReleaseType::Minor => Version {
        major: self.major,
        minor: self.minor + 1,
        patch: 0,
        pre_id: None,
        pre_version: None,
      },
      ReleaseType::Patch => Version {
        major: self.major,
        minor: self.minor,
        patch: self.patch + 1,
        pre_id: None,
        pre_version: None,
      },
      ReleaseType::PreRelease if self.pre_id.is_none() => {
        self.inc_pre(ReleaseType::Patch, pre_id)?
      }
      ReleaseType::PreRelease if self.pre_version.is_some() => {
        let mut version = self.clone();
        let pre_version = self.pre_version.unwrap();
        version.pre_version = Some(pre_version + 1);
        version
      }
      ReleaseType::PreRelease => self.inc_pre(ReleaseType::Major, pre_id)?,
      ReleaseType::PreMajor => self.inc_pre(ReleaseType::Major, pre_id)?,
      ReleaseType::PreMinor => self.inc_pre(ReleaseType::Minor, pre_id)?,
      ReleaseType::PrePatch => self.inc_pre(ReleaseType::Patch, pre_id)?,
    };

    Ok(version)
  }

  fn inc_pre(&self, release_type: ReleaseType, pre_id: Option<&str>) -> Result<Version> {
    match pre_id {
      Some(id) => {
        let mut version = self.inc(&release_type, None)?;
        version.pre_id = Some(id.to_string());
        version.pre_version = Some(1);
        Ok(version)
      }
      None => Err(anyhow!("Missing id for prerelease.")),
    }
  }

  pub fn raw(&self) -> String {
    let mut version = format!("{}.{}.{}", self.major, self.minor, self.patch);
    if let Some(id) = &self.pre_id {
      let pre = format!("-{}", id);
      version.push_str(pre.as_str());
    }

    if let Some(v) = &self.pre_version {
      let pre = format!(".{}", v);
      version.push_str(pre.as_str());
    }

    version
  }
}

pub fn is_valid(version: &str) -> bool {
  let regex = Regex::new(SEMVER_REGEX).unwrap();
  regex.is_match(version)
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn should_be_valid() {
    let versions = ["1.0.0", "0.2.3-beta.1"];
    for version in versions {
      assert!(is_valid(version));
    }
  }

  #[test]
  fn should_be_invalid() {
    let versions = ["1", "3.5", "1.0.0-beta", "2.0.0-rc.1+build.123"];
    for version in versions {
      assert!(!is_valid(version));
    }
  }

  #[test]
  fn should_build_version_struct() {
    let version = Version::new("6.2.3-beta.1").unwrap();
    assert_eq!(6, version.major);
    assert_eq!(2, version.minor);
    assert_eq!(3, version.patch);
    assert_eq!("beta", version.pre_id.unwrap());
    assert_eq!(1, version.pre_version.unwrap());
  }
}