1use std::fmt;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
7pub enum BumpType {
8 Patch,
10 Minor,
12 Major,
14}
15
16#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct SemVer {
19 pub major: u64,
21 pub minor: u64,
23 pub patch: u64,
25 pub has_v_prefix: bool,
27}
28
29impl SemVer {
30 pub fn bump(&self, bump: BumpType) -> SemVer {
49 match bump {
50 BumpType::Major => SemVer {
51 major: self.major + 1,
52 minor: 0,
53 patch: 0,
54 has_v_prefix: self.has_v_prefix,
55 },
56 BumpType::Minor => SemVer {
57 major: self.major,
58 minor: self.minor + 1,
59 patch: 0,
60 has_v_prefix: self.has_v_prefix,
61 },
62 BumpType::Patch => SemVer {
63 major: self.major,
64 minor: self.minor,
65 patch: self.patch + 1,
66 has_v_prefix: self.has_v_prefix,
67 },
68 }
69 }
70}
71
72impl fmt::Display for SemVer {
73 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74 if self.has_v_prefix {
75 write!(f, "v{}.{}.{}", self.major, self.minor, self.patch)
76 } else {
77 write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
78 }
79 }
80}
81
82pub fn parse_semver(version: &str) -> Option<SemVer> {
105 if !validate_semver(version) {
106 return None;
107 }
108
109 let has_v_prefix = version.starts_with('v');
110 let s = version.strip_prefix('v').unwrap_or(version);
111 let core = s.split(['+', '-']).next().unwrap_or(s);
112 let parts: Vec<&str> = core.split('.').collect();
113 if parts.len() != 3 {
114 return None;
115 }
116
117 let major = parts[0].parse::<u64>().ok()?;
118 let minor = parts[1].parse::<u64>().ok()?;
119 let patch = parts[2].parse::<u64>().ok()?;
120
121 Some(SemVer {
122 major,
123 minor,
124 patch,
125 has_v_prefix,
126 })
127}
128
129pub fn validate_semver(version: &str) -> bool {
145 let s = version.strip_prefix('v').unwrap_or(version);
146 if s.is_empty() {
147 return false;
148 }
149
150 let (s, _build) = match s.split_once('+') {
152 Some((core, b)) => {
153 if b.is_empty()
154 || !b
155 .chars()
156 .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-')
157 {
158 return false;
159 }
160 (core, Some(b))
161 }
162 None => (s, None),
163 };
164
165 let (core, _pre) = match s.split_once('-') {
167 Some((c, p)) => {
168 if p.is_empty()
169 || !p
170 .chars()
171 .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-')
172 {
173 return false;
174 }
175 (c, Some(p))
176 }
177 None => (s, None),
178 };
179
180 let parts: Vec<&str> = core.split('.').collect();
182 if parts.len() != 3 {
183 return false;
184 }
185
186 for part in parts {
187 if part.is_empty() || !part.chars().all(|c| c.is_ascii_digit()) {
188 return false;
189 }
190 if part.len() > 1 && part.starts_with('0') {
192 return false;
193 }
194 }
195
196 true
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202
203 #[test]
204 fn test_valid_semver() {
205 assert!(validate_semver("1.0.0"));
206 assert!(validate_semver("v1.0.0"));
207 assert!(validate_semver("v0.1.0"));
208 assert!(validate_semver("v1.2.3-alpha.1"));
209 assert!(validate_semver("v1.2.3+build.42"));
210 assert!(validate_semver("v1.2.3-rc.1+build.1"));
211 }
212
213 #[test]
214 fn test_invalid_semver() {
215 assert!(!validate_semver(""));
216 assert!(!validate_semver("v"));
217 assert!(!validate_semver("1.0"));
218 assert!(!validate_semver("v1.0"));
219 assert!(!validate_semver("1.0.0.0"));
220 assert!(!validate_semver("v01.0.0"));
221 assert!(!validate_semver("latest"));
222 assert!(!validate_semver("v1.0.0-"));
223 }
224
225 #[test]
226 fn test_parse_semver() {
227 let v1 = parse_semver("v1.2.3").expect("valid");
228 assert_eq!(
229 v1,
230 SemVer {
231 major: 1,
232 minor: 2,
233 patch: 3,
234 has_v_prefix: true
235 }
236 );
237 assert_eq!(v1.to_string(), "v1.2.3");
238
239 let v2 = parse_semver("0.4.12").expect("valid");
240 assert_eq!(
241 v2,
242 SemVer {
243 major: 0,
244 minor: 4,
245 patch: 12,
246 has_v_prefix: false
247 }
248 );
249 assert_eq!(v2.to_string(), "0.4.12");
250
251 let v3 = parse_semver("v2.0.0-rc.1+build.5").expect("valid");
252 assert_eq!(
253 v3,
254 SemVer {
255 major: 2,
256 minor: 0,
257 patch: 0,
258 has_v_prefix: true
259 }
260 );
261
262 assert!(parse_semver("not-semver").is_none());
263 assert!(parse_semver("v1.0").is_none());
264 }
265
266 #[test]
267 fn test_semver_bump() {
268 let v = SemVer {
269 major: 1,
270 minor: 2,
271 patch: 3,
272 has_v_prefix: true,
273 };
274 assert_eq!(v.bump(BumpType::Patch).to_string(), "v1.2.4");
275 assert_eq!(v.bump(BumpType::Minor).to_string(), "v1.3.0");
276 assert_eq!(v.bump(BumpType::Major).to_string(), "v2.0.0");
277
278 let v_plain = SemVer {
279 major: 0,
280 minor: 1,
281 patch: 0,
282 has_v_prefix: false,
283 };
284 assert_eq!(v_plain.bump(BumpType::Patch).to_string(), "0.1.1");
285 assert_eq!(v_plain.bump(BumpType::Minor).to_string(), "0.2.0");
286 assert_eq!(v_plain.bump(BumpType::Major).to_string(), "1.0.0");
287 }
288}