1use std::sync::LazyLock;
4
5use regex::Regex;
6
7#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct SemverInfo {
10 pub major: u32,
11 pub minor: u32,
12 pub patch: Option<u32>,
13 pub build: Option<u32>,
14 pub pre_release: Option<String>,
15 pub build_metadata: Option<String>,
16}
17
18impl Ord for SemverInfo {
19 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
20 match self.major.cmp(&other.major) {
21 std::cmp::Ordering::Equal => {}
22 ord => return ord,
23 }
24 match self.minor.cmp(&other.minor) {
25 std::cmp::Ordering::Equal => {}
26 ord => return ord,
27 }
28 match self.patch.cmp(&other.patch) {
29 std::cmp::Ordering::Equal => {}
30 ord => return ord,
31 }
32 match self.build.cmp(&other.build) {
33 std::cmp::Ordering::Equal => {}
34 ord => return ord,
35 }
36 match (&self.pre_release, &other.pre_release) {
38 (None, Some(_)) => std::cmp::Ordering::Greater,
39 (Some(_), None) => std::cmp::Ordering::Less,
40 (Some(a), Some(b)) => a.cmp(b),
41 (None, None) => std::cmp::Ordering::Equal,
42 }
43 }
44}
45
46impl PartialOrd for SemverInfo {
47 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
48 Some(self.cmp(other))
49 }
50}
51
52static SEMVER_REGEX: LazyLock<Regex> = LazyLock::new(|| {
60 Regex::new(
61 r"^(?:[a-z]+-)?v?(\d+)\.(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:(?:-([a-zA-Z0-9.-]+))|(?:([a-z]+)(\d+)))?(?:\+(.+))?$"
62 )
63 .expect("Invalid semver regex")
64});
65
66pub fn parse_semver(tag: &str) -> Option<SemverInfo> {
77 let caps = SEMVER_REGEX.captures(tag)?;
78
79 let major = caps.get(1)?.as_str().parse::<u32>().ok()?;
80 let minor = caps.get(2)?.as_str().parse::<u32>().ok()?;
81 let patch = caps.get(3).and_then(|m| m.as_str().parse::<u32>().ok());
82 let build = caps.get(4).and_then(|m| m.as_str().parse::<u32>().ok());
83
84 let pre_release = caps.get(5).map_or_else(
88 || {
89 caps.get(6).map(|py_pre| {
90 let py_num = caps
91 .get(7)
92 .map_or(String::new(), |m| m.as_str().to_string());
93 format!("{}{}", py_pre.as_str(), py_num)
94 })
95 },
96 |dash_pre| Some(dash_pre.as_str().to_string()),
97 );
98
99 let build_metadata = caps.get(8).map(|m| m.as_str().to_string());
100
101 Some(SemverInfo {
102 major,
103 minor,
104 patch,
105 build,
106 pre_release,
107 build_metadata,
108 })
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 fn is_semver_tag(tag: &str) -> bool {
117 parse_semver(tag).is_some()
118 }
119
120 #[test]
121 fn test_parse_semver_2_part() {
122 let result = parse_semver("1.0");
123 assert!(result.is_some());
124 let semver = result.unwrap();
125 assert_eq!(semver.major, 1);
126 assert_eq!(semver.minor, 0);
127 assert_eq!(semver.patch, None);
128 assert_eq!(semver.build, None);
129 }
130
131 #[test]
132 fn test_parse_semver_2_part_with_v_prefix() {
133 let result = parse_semver("v2.1");
134 assert!(result.is_some());
135 let semver = result.unwrap();
136 assert_eq!(semver.major, 2);
137 assert_eq!(semver.minor, 1);
138 }
139
140 #[test]
141 fn test_parse_semver_3_part() {
142 let result = parse_semver("1.2.3");
143 assert!(result.is_some());
144 let semver = result.unwrap();
145 assert_eq!(semver.major, 1);
146 assert_eq!(semver.minor, 2);
147 assert_eq!(semver.patch, Some(3));
148 assert_eq!(semver.build, None);
149 }
150
151 #[test]
152 fn test_parse_semver_3_part_with_v_prefix() {
153 let result = parse_semver("v1.2.3");
154 assert!(result.is_some());
155 let semver = result.unwrap();
156 assert_eq!(semver.major, 1);
157 assert_eq!(semver.minor, 2);
158 assert_eq!(semver.patch, Some(3));
159 }
160
161 #[test]
162 fn test_parse_semver_4_part() {
163 let result = parse_semver("1.2.3.4");
164 assert!(result.is_some());
165 let semver = result.unwrap();
166 assert_eq!(semver.major, 1);
167 assert_eq!(semver.minor, 2);
168 assert_eq!(semver.patch, Some(3));
169 assert_eq!(semver.build, Some(4));
170 }
171
172 #[test]
173 fn test_parse_semver_with_pre_release() {
174 let result = parse_semver("1.0.0-alpha");
175 assert!(result.is_some());
176 let semver = result.unwrap();
177 assert_eq!(semver.major, 1);
178 assert_eq!(semver.minor, 0);
179 assert_eq!(semver.patch, Some(0));
180 assert_eq!(semver.pre_release, Some("alpha".to_string()));
181 }
182
183 #[test]
184 fn test_parse_semver_with_pre_release_numeric() {
185 let result = parse_semver("v2.0.0-rc.1");
186 assert!(result.is_some());
187 let semver = result.unwrap();
188 assert_eq!(semver.major, 2);
189 assert_eq!(semver.minor, 0);
190 assert_eq!(semver.patch, Some(0));
191 assert_eq!(semver.pre_release, Some("rc.1".to_string()));
192 }
193
194 #[test]
195 fn test_parse_semver_with_build_metadata() {
196 let result = parse_semver("1.0.0+build.123");
197 assert!(result.is_some());
198 let semver = result.unwrap();
199 assert_eq!(semver.major, 1);
200 assert_eq!(semver.minor, 0);
201 assert_eq!(semver.patch, Some(0));
202 assert_eq!(semver.build_metadata, Some("build.123".to_string()));
203 }
204
205 #[test]
206 fn test_parse_semver_with_pre_release_and_build() {
207 let result = parse_semver("v1.0.0-beta.2+20130313144700");
208 assert!(result.is_some());
209 let semver = result.unwrap();
210 assert_eq!(semver.major, 1);
211 assert_eq!(semver.minor, 0);
212 assert_eq!(semver.patch, Some(0));
213 assert_eq!(semver.pre_release, Some("beta.2".to_string()));
214 assert_eq!(semver.build_metadata, Some("20130313144700".to_string()));
215 }
216
217 #[test]
218 fn test_parse_semver_2_part_with_pre_release() {
219 let result = parse_semver("2.0-alpha");
220 assert!(result.is_some());
221 let semver = result.unwrap();
222 assert_eq!(semver.major, 2);
223 assert_eq!(semver.minor, 0);
224 assert_eq!(semver.patch, None);
225 assert_eq!(semver.pre_release, Some("alpha".to_string()));
226 }
227
228 #[test]
229 fn test_parse_semver_invalid_single_part() {
230 assert!(parse_semver("1").is_none());
231 }
232
233 #[test]
234 fn test_parse_semver_invalid_non_numeric() {
235 assert!(parse_semver("abc.def").is_none());
236 assert!(parse_semver("1.x.3").is_none());
237 }
238
239 #[test]
240 fn test_parse_semver_invalid_too_many_parts() {
241 assert!(parse_semver("1.2.3.4.5").is_none());
242 }
243
244 #[test]
245 fn test_is_semver_tag() {
246 assert!(is_semver_tag("1.0"));
248 assert!(is_semver_tag("v1.0"));
249 assert!(is_semver_tag("1.2.3"));
250 assert!(is_semver_tag("v1.2.3"));
251 assert!(is_semver_tag("1.2.3.4"));
252
253 assert!(is_semver_tag("1.0.0-alpha"));
255 assert!(is_semver_tag("v2.0.0-rc.1"));
256 assert!(is_semver_tag("1.2.3-beta.2"));
257
258 assert!(is_semver_tag("1.2.3a1"));
260 assert!(is_semver_tag("1.2.3b1"));
261 assert!(is_semver_tag("1.2.3rc1"));
262
263 assert!(is_semver_tag("1.0.0+build"));
265
266 assert!(is_semver_tag("py-v1.0.0"));
268 assert!(is_semver_tag("rust-v1.2.3-beta.1"));
269 assert!(is_semver_tag("python-1.2.3b1"));
270
271 assert!(!is_semver_tag("v1"));
273 assert!(!is_semver_tag("abc"));
274 assert!(!is_semver_tag("1.2.3.4.5"));
275 assert!(!is_semver_tag("server-v-1.0.0")); }
277
278 #[test]
279 fn test_parse_semver_with_custom_prefix() {
280 let result = parse_semver("py-v1.0.0-beta.1");
282 assert!(result.is_some());
283 let semver = result.unwrap();
284 assert_eq!(semver.major, 1);
285 assert_eq!(semver.minor, 0);
286 assert_eq!(semver.patch, Some(0));
287 assert_eq!(semver.pre_release, Some("beta.1".to_string()));
288
289 let result = parse_semver("rust-v1.0.0-beta.2");
291 assert!(result.is_some());
292 let semver = result.unwrap();
293 assert_eq!(semver.major, 1);
294 assert_eq!(semver.minor, 0);
295 assert_eq!(semver.patch, Some(0));
296 assert_eq!(semver.pre_release, Some("beta.2".to_string()));
297
298 let result = parse_semver("python-2.1.0");
300 assert!(result.is_some());
301 let semver = result.unwrap();
302 assert_eq!(semver.major, 2);
303 assert_eq!(semver.minor, 1);
304 assert_eq!(semver.patch, Some(0));
305 }
306
307 #[test]
308 fn test_parse_semver_python_style() {
309 let result = parse_semver("1.2.3a1");
311 assert!(result.is_some());
312 let semver = result.unwrap();
313 assert_eq!(semver.major, 1);
314 assert_eq!(semver.minor, 2);
315 assert_eq!(semver.patch, Some(3));
316 assert_eq!(semver.pre_release, Some("a1".to_string()));
317
318 let result = parse_semver("v1.2.3b2");
320 assert!(result.is_some());
321 let semver = result.unwrap();
322 assert_eq!(semver.major, 1);
323 assert_eq!(semver.minor, 2);
324 assert_eq!(semver.patch, Some(3));
325 assert_eq!(semver.pre_release, Some("b2".to_string()));
326
327 let result = parse_semver("2.0.0rc1");
329 assert!(result.is_some());
330 let semver = result.unwrap();
331 assert_eq!(semver.major, 2);
332 assert_eq!(semver.minor, 0);
333 assert_eq!(semver.patch, Some(0));
334 assert_eq!(semver.pre_release, Some("rc1".to_string()));
335
336 let result = parse_semver("py-v1.0.0b1");
338 assert!(result.is_some());
339 let semver = result.unwrap();
340 assert_eq!(semver.major, 1);
341 assert_eq!(semver.minor, 0);
342 assert_eq!(semver.patch, Some(0));
343 assert_eq!(semver.pre_release, Some("b1".to_string()));
344 }
345
346 #[test]
347 fn test_parse_semver_rejects_garbage() {
348 assert!(parse_semver("server-v-config").is_none());
350 assert!(parse_semver("whatever-v-something").is_none());
351
352 assert!(parse_semver("v1").is_none());
354 assert!(parse_semver("1").is_none());
355 assert!(parse_semver("1.2.3.4.5").is_none());
356 assert!(parse_semver("abc.def").is_none());
357 }
358
359 #[test]
360 fn test_semver_ordering() {
361 let v1_0_0 = parse_semver("1.0.0").unwrap();
362 let v1_0_1 = parse_semver("1.0.1").unwrap();
363 let v1_0_0_alpha = parse_semver("1.0.0-alpha").unwrap();
364 let v1_0_0_beta = parse_semver("1.0.0-beta").unwrap();
365
366 assert!(v1_0_0_alpha < v1_0_0_beta);
368 assert!(v1_0_0_beta < v1_0_0);
370 assert!(v1_0_0 < v1_0_1);
372 }
373
374 #[test]
375 fn test_semver_ordering_with_build() {
376 let v1_2_3_4 = parse_semver("1.2.3.4").unwrap();
377 let v1_2_3_5 = parse_semver("1.2.3.5").unwrap();
378 let v1_2_3 = parse_semver("1.2.3").unwrap();
379
380 assert!(v1_2_3_4 < v1_2_3_5);
382 assert!(v1_2_3 < v1_2_3_4);
384 }
385}