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}