blue_build_utils/
semver.rs1use std::{ops::Not, str::FromStr};
2
3use lazy_regex::regex_captures;
4use miette::bail;
5use semver::{BuildMetadata, Prerelease};
6use serde::{Deserialize, Serialize, de::Error};
7
8#[derive(Debug, Clone, Serialize)]
9pub struct Version {
10 prefix: Option<String>,
11 version: semver::Version,
12}
13
14impl std::ops::Deref for Version {
15 type Target = semver::Version;
16
17 fn deref(&self) -> &Self::Target {
18 &self.version
19 }
20}
21
22impl std::fmt::Display for Version {
23 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24 self.version.fmt(f)
25 }
26}
27
28impl<'de> Deserialize<'de> for Version {
29 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
30 where
31 D: serde::Deserializer<'de>,
32 {
33 let ver = String::deserialize(deserializer)?;
34 ver.parse()
35 .map_err(|e: miette::Error| D::Error::custom(e.to_string()))
36 }
37}
38
39impl FromStr for Version {
40 type Err = miette::Error;
41
42 fn from_str(s: &str) -> Result<Self, Self::Err> {
43 let Some((_whole, prefix, major, minor, patch)) = regex_captures!(
44 r"(?:(?<prefix>[a-zA-Z0-9_-]+)-)?v?(?<major>\d+)(?:\.(?<minor>\d+))?(?:\.(?<patch>\d+))?(?:[-_].*)?",
45 s.trim()
46 ) else {
47 bail!("Failed to parse version {s} with regex");
48 };
49
50 let Ok(mut version) = semver::Version::parse(&format!(
51 "{major}.{minor}.{patch}",
52 minor = if minor.is_empty() { "0" } else { minor },
53 patch = if patch.is_empty() { "0" } else { patch }
54 )) else {
55 bail!("Failed to parse version {s}");
56 };
57
58 version.pre = Prerelease::EMPTY;
60 version.build = BuildMetadata::EMPTY;
61
62 let prefix = prefix.is_empty().not().then(|| prefix.into());
63
64 Ok(Self { prefix, version })
65 }
66}
67
68#[cfg(test)]
69mod test {
70 use rstest::rstest;
71
72 use crate::semver::Version;
73
74 #[rstest]
75 #[case("42", None, 42, 0, 0)]
76 #[case("42.20251020.1", None, 42, 20_251_020, 1)]
77 #[case("42.20251020", None, 42, 20_251_020, 0)]
78 #[case("latest-42.20251020.1", Some("latest"), 42, 20_251_020, 1)]
79 #[case("42-beta.0", None, 42, 0, 0)]
80 #[case("stable-42-beta.0", Some("stable"), 42, 0, 0)]
81 #[case("v42", None, 42, 0, 0)]
82 #[case("v42.20251020.1", None, 42, 20_251_020, 1)]
83 #[case("v42.20251020", None, 42, 20_251_020, 0)]
84 #[case("latest-v42.20251020.1", Some("latest"), 42, 20_251_020, 1)]
85 #[case("v42-beta.0", None, 42, 0, 0)]
86 #[case("stable-v42-beta.0", Some("stable"), 42, 0, 0)]
87 fn parse_version(
88 #[case] version: &str,
89 #[case] prefix: Option<&str>,
90 #[case] major: u64,
91 #[case] minor: u64,
92 #[case] patch: u64,
93 ) {
94 let version = version.parse::<Version>().unwrap();
95 assert_eq!(version.prefix.as_deref(), prefix);
96 assert_eq!(version.version.major, major);
97 assert_eq!(version.version.minor, minor);
98 assert_eq!(version.version.patch, patch);
99 }
100}