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
18static SEMVER_REGEX: LazyLock<Regex> = LazyLock::new(|| {
26 Regex::new(
27 r"^(?:[a-z]+-)?v?(\d+)\.(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:(?:-([a-zA-Z0-9.-]+))|(?:([a-z]+)(\d+)))?(?:\+(.+))?$"
28 )
29 .expect("Invalid semver regex")
30});
31
32pub fn parse_semver(tag: &str) -> Option<SemverInfo> {
43 let caps = SEMVER_REGEX.captures(tag)?;
44
45 let major = caps.get(1)?.as_str().parse::<u32>().ok()?;
46 let minor = caps.get(2)?.as_str().parse::<u32>().ok()?;
47 let patch = caps.get(3).and_then(|m| m.as_str().parse::<u32>().ok());
48 let build = caps.get(4).and_then(|m| m.as_str().parse::<u32>().ok());
49
50 let pre_release = caps.get(5).map_or_else(
54 || {
55 caps.get(6).map(|py_pre| {
56 let py_num = caps
57 .get(7)
58 .map_or(String::new(), |m| m.as_str().to_string());
59 format!("{}{}", py_pre.as_str(), py_num)
60 })
61 },
62 |dash_pre| Some(dash_pre.as_str().to_string()),
63 );
64
65 let build_metadata = caps.get(8).map(|m| m.as_str().to_string());
66
67 Some(SemverInfo {
68 major,
69 minor,
70 patch,
71 build,
72 pre_release,
73 build_metadata,
74 })
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80
81 fn is_semver_tag(tag: &str) -> bool {
83 parse_semver(tag).is_some()
84 }
85
86 #[test]
87 fn test_parse_semver_2_part() {
88 let result = parse_semver("1.0");
89 assert!(result.is_some());
90 let semver = result.unwrap();
91 assert_eq!(semver.major, 1);
92 assert_eq!(semver.minor, 0);
93 assert_eq!(semver.patch, None);
94 assert_eq!(semver.build, None);
95 }
96
97 #[test]
98 fn test_parse_semver_2_part_with_v_prefix() {
99 let result = parse_semver("v2.1");
100 assert!(result.is_some());
101 let semver = result.unwrap();
102 assert_eq!(semver.major, 2);
103 assert_eq!(semver.minor, 1);
104 }
105
106 #[test]
107 fn test_parse_semver_3_part() {
108 let result = parse_semver("1.2.3");
109 assert!(result.is_some());
110 let semver = result.unwrap();
111 assert_eq!(semver.major, 1);
112 assert_eq!(semver.minor, 2);
113 assert_eq!(semver.patch, Some(3));
114 assert_eq!(semver.build, None);
115 }
116
117 #[test]
118 fn test_parse_semver_3_part_with_v_prefix() {
119 let result = parse_semver("v1.2.3");
120 assert!(result.is_some());
121 let semver = result.unwrap();
122 assert_eq!(semver.major, 1);
123 assert_eq!(semver.minor, 2);
124 assert_eq!(semver.patch, Some(3));
125 }
126
127 #[test]
128 fn test_parse_semver_4_part() {
129 let result = parse_semver("1.2.3.4");
130 assert!(result.is_some());
131 let semver = result.unwrap();
132 assert_eq!(semver.major, 1);
133 assert_eq!(semver.minor, 2);
134 assert_eq!(semver.patch, Some(3));
135 assert_eq!(semver.build, Some(4));
136 }
137
138 #[test]
139 fn test_parse_semver_with_pre_release() {
140 let result = parse_semver("1.0.0-alpha");
141 assert!(result.is_some());
142 let semver = result.unwrap();
143 assert_eq!(semver.major, 1);
144 assert_eq!(semver.minor, 0);
145 assert_eq!(semver.patch, Some(0));
146 assert_eq!(semver.pre_release, Some("alpha".to_string()));
147 }
148
149 #[test]
150 fn test_parse_semver_with_pre_release_numeric() {
151 let result = parse_semver("v2.0.0-rc.1");
152 assert!(result.is_some());
153 let semver = result.unwrap();
154 assert_eq!(semver.major, 2);
155 assert_eq!(semver.minor, 0);
156 assert_eq!(semver.patch, Some(0));
157 assert_eq!(semver.pre_release, Some("rc.1".to_string()));
158 }
159
160 #[test]
161 fn test_parse_semver_with_build_metadata() {
162 let result = parse_semver("1.0.0+build.123");
163 assert!(result.is_some());
164 let semver = result.unwrap();
165 assert_eq!(semver.major, 1);
166 assert_eq!(semver.minor, 0);
167 assert_eq!(semver.patch, Some(0));
168 assert_eq!(semver.build_metadata, Some("build.123".to_string()));
169 }
170
171 #[test]
172 fn test_parse_semver_with_pre_release_and_build() {
173 let result = parse_semver("v1.0.0-beta.2+20130313144700");
174 assert!(result.is_some());
175 let semver = result.unwrap();
176 assert_eq!(semver.major, 1);
177 assert_eq!(semver.minor, 0);
178 assert_eq!(semver.patch, Some(0));
179 assert_eq!(semver.pre_release, Some("beta.2".to_string()));
180 assert_eq!(semver.build_metadata, Some("20130313144700".to_string()));
181 }
182
183 #[test]
184 fn test_parse_semver_2_part_with_pre_release() {
185 let result = parse_semver("2.0-alpha");
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, None);
191 assert_eq!(semver.pre_release, Some("alpha".to_string()));
192 }
193
194 #[test]
195 fn test_parse_semver_invalid_single_part() {
196 assert!(parse_semver("1").is_none());
197 }
198
199 #[test]
200 fn test_parse_semver_invalid_non_numeric() {
201 assert!(parse_semver("abc.def").is_none());
202 assert!(parse_semver("1.x.3").is_none());
203 }
204
205 #[test]
206 fn test_parse_semver_invalid_too_many_parts() {
207 assert!(parse_semver("1.2.3.4.5").is_none());
208 }
209
210 #[test]
211 fn test_is_semver_tag() {
212 assert!(is_semver_tag("1.0"));
214 assert!(is_semver_tag("v1.0"));
215 assert!(is_semver_tag("1.2.3"));
216 assert!(is_semver_tag("v1.2.3"));
217 assert!(is_semver_tag("1.2.3.4"));
218
219 assert!(is_semver_tag("1.0.0-alpha"));
221 assert!(is_semver_tag("v2.0.0-rc.1"));
222 assert!(is_semver_tag("1.2.3-beta.2"));
223
224 assert!(is_semver_tag("1.2.3a1"));
226 assert!(is_semver_tag("1.2.3b1"));
227 assert!(is_semver_tag("1.2.3rc1"));
228
229 assert!(is_semver_tag("1.0.0+build"));
231
232 assert!(is_semver_tag("py-v1.0.0"));
234 assert!(is_semver_tag("rust-v1.2.3-beta.1"));
235 assert!(is_semver_tag("python-1.2.3b1"));
236
237 assert!(!is_semver_tag("v1"));
239 assert!(!is_semver_tag("abc"));
240 assert!(!is_semver_tag("1.2.3.4.5"));
241 assert!(!is_semver_tag("server-v-1.0.0")); }
243
244 #[test]
245 fn test_parse_semver_with_custom_prefix() {
246 let result = parse_semver("py-v1.0.0-beta.1");
248 assert!(result.is_some());
249 let semver = result.unwrap();
250 assert_eq!(semver.major, 1);
251 assert_eq!(semver.minor, 0);
252 assert_eq!(semver.patch, Some(0));
253 assert_eq!(semver.pre_release, Some("beta.1".to_string()));
254
255 let result = parse_semver("rust-v1.0.0-beta.2");
257 assert!(result.is_some());
258 let semver = result.unwrap();
259 assert_eq!(semver.major, 1);
260 assert_eq!(semver.minor, 0);
261 assert_eq!(semver.patch, Some(0));
262 assert_eq!(semver.pre_release, Some("beta.2".to_string()));
263
264 let result = parse_semver("python-2.1.0");
266 assert!(result.is_some());
267 let semver = result.unwrap();
268 assert_eq!(semver.major, 2);
269 assert_eq!(semver.minor, 1);
270 assert_eq!(semver.patch, Some(0));
271 }
272
273 #[test]
274 fn test_parse_semver_python_style() {
275 let result = parse_semver("1.2.3a1");
277 assert!(result.is_some());
278 let semver = result.unwrap();
279 assert_eq!(semver.major, 1);
280 assert_eq!(semver.minor, 2);
281 assert_eq!(semver.patch, Some(3));
282 assert_eq!(semver.pre_release, Some("a1".to_string()));
283
284 let result = parse_semver("v1.2.3b2");
286 assert!(result.is_some());
287 let semver = result.unwrap();
288 assert_eq!(semver.major, 1);
289 assert_eq!(semver.minor, 2);
290 assert_eq!(semver.patch, Some(3));
291 assert_eq!(semver.pre_release, Some("b2".to_string()));
292
293 let result = parse_semver("2.0.0rc1");
295 assert!(result.is_some());
296 let semver = result.unwrap();
297 assert_eq!(semver.major, 2);
298 assert_eq!(semver.minor, 0);
299 assert_eq!(semver.patch, Some(0));
300 assert_eq!(semver.pre_release, Some("rc1".to_string()));
301
302 let result = parse_semver("py-v1.0.0b1");
304 assert!(result.is_some());
305 let semver = result.unwrap();
306 assert_eq!(semver.major, 1);
307 assert_eq!(semver.minor, 0);
308 assert_eq!(semver.patch, Some(0));
309 assert_eq!(semver.pre_release, Some("b1".to_string()));
310 }
311
312 #[test]
313 fn test_parse_semver_rejects_garbage() {
314 assert!(parse_semver("server-v-config").is_none());
316 assert!(parse_semver("whatever-v-something").is_none());
317
318 assert!(parse_semver("v1").is_none());
320 assert!(parse_semver("1").is_none());
321 assert!(parse_semver("1.2.3.4.5").is_none());
322 assert!(parse_semver("abc.def").is_none());
323 }
324}