bump_bin/
version.rs

1use crate::fs::{read_file, read_from_stdin};
2use anyhow::Result;
3use anyhow::{anyhow, bail, Error};
4
5use log::*;
6use once_cell::sync::Lazy;
7use regex::Regex;
8use semver::{BuildMetadata, Prerelease, Version as SemVer};
9use std::fmt;
10use std::io::IsTerminal;
11use std::str::FromStr;
12
13static PREFIX: Lazy<Regex> = Lazy::new(|| {
14    Regex::new(r"^(?P<prefix>.*?)(?P<version>[0-9]+?.[0-9]+?.[0-9]+?(?:.*)$)").unwrap()
15});
16
17#[derive(PartialEq, Debug, Clone)]
18pub struct Version {
19    prefix: String,
20    ver: SemVer,
21}
22
23impl FromStr for Version {
24    type Err = Error;
25    fn from_str(s: &str) -> Result<Self, Self::Err> {
26        let caps = PREFIX
27            .captures(s)
28            .ok_or_else(|| anyhow!("Can't find semver format. value: {}", s))?;
29
30        let cap_pre = caps.name("prefix");
31        let cap_ver = caps.name("version");
32
33        let (prefix, version) = match (cap_pre, cap_ver) {
34            (Some(p), Some(v)) => (p.as_str(), v.as_str()),
35            (None, Some(v)) => ("", v.as_str()),
36            _ => bail!("Can't find semver format. value: {}", s),
37        };
38
39        debug!("prefix: {}", prefix);
40        debug!("version: {}", version);
41
42        Ok(Version {
43            prefix: prefix.to_string(),
44            ver: SemVer::parse(version)?,
45        })
46    }
47}
48
49impl fmt::Display for Version {
50    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51        write!(f, "{}{}", self.prefix, self.ver)
52    }
53}
54
55impl TryFrom<(Option<String>, Option<String>)> for Version {
56    type Error = Error;
57
58    fn try_from(value: (Option<String>, Option<String>)) -> std::result::Result<Self, Self::Error> {
59        let v = match value {
60            (Some(ref ver), None) if ver == "-" => {
61                let buf = read_from_stdin()?;
62                Version::from_str(buf.trim_end())?
63            }
64            (None, None) if !std::io::stdin().is_terminal() => {
65                let buf = read_from_stdin()?;
66                Version::from_str(buf.trim_end())?
67            }
68            (None, Some(ref ver)) => Version::from_str(ver)?,
69            (Some(_), Some(_)) => {
70                bail!("Invalid argument, specify either ver args or file option")
71            }
72            (Some(f), _) => {
73                let buf = String::from_utf8(read_file(f)?)?;
74                Version::from_str(buf.trim_end())?
75            }
76            _ => bail!("Invalid argument"),
77        };
78        Ok(v)
79    }
80}
81
82impl Version {
83    pub fn bump_patch(&self) -> Version {
84        let mut v = self.clone();
85        v.ver.patch += 1;
86        v.ver.pre = Prerelease::EMPTY;
87        v.ver.build = BuildMetadata::EMPTY;
88        v
89    }
90
91    pub fn bump_minor(&self) -> Version {
92        let mut v = self.clone();
93        v.ver.minor += 1;
94        v.ver.patch = 0;
95        v.ver.pre = Prerelease::EMPTY;
96        v.ver.build = BuildMetadata::EMPTY;
97        v
98    }
99
100    pub fn bump_major(&self) -> Version {
101        let mut v = self.clone();
102        v.ver.major += 1;
103        v.ver.minor = 0;
104        v.ver.patch = 0;
105        v.ver.pre = Prerelease::EMPTY;
106        v.ver.build = BuildMetadata::EMPTY;
107        v
108    }
109
110    pub fn update_pre_release(&self, pre: impl Into<String>) -> Result<Version> {
111        let mut v = self.clone();
112        v.ver.pre = Prerelease::new(pre.into().as_str())?;
113        Ok(v)
114    }
115
116    pub fn update_build(&self, build: impl Into<String>) -> Result<Version> {
117        let mut v = self.clone();
118        v.ver.build = BuildMetadata::new(build.into().as_str())?;
119        Ok(v)
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn from_str_ok() {
129        let result = Version::from_str("0.0.0");
130        assert!(result.is_ok());
131    }
132
133    #[test]
134    fn from_str_ng() {
135        let inputs = vec!["x.x.x", "vx.x.x", "x", "x.x"];
136        for input in inputs {
137            let result = Version::from_str(input);
138            assert!(result.is_err());
139            let msg = result.expect_err("").to_string();
140            assert!(msg.contains("Can't find semver format"));
141        }
142    }
143
144    #[test]
145    fn try_from_ok() {
146        let result = Version::try_from((None, Some(String::from("0.0.0"))));
147
148        let prefix = String::from("");
149        let ver = SemVer::new(0, 0, 0);
150        let _version = Version { prefix, ver };
151        assert!(matches!(result, Ok(_version)));
152    }
153
154    #[test]
155    fn try_from_ng() {
156        let inputs = vec![
157            (Some(String::from("test.md")), Some(String::from("x.x.x"))),
158            (None, None),
159        ];
160        for (file, ver) in inputs {
161            let result = Version::try_from((file, ver));
162            assert!(matches!(result, Err(_)));
163        }
164    }
165}