utils_box/
versions.rs

1//! # Versioning utilities
2//! A toolbox of small utilities based on `semver.org`.
3//! Useful for version control operations.
4
5use anyhow::{Result, bail};
6use regex::Regex;
7use semver::Version;
8
9use crate::log_trace;
10
11/// Parse a provide slice and get a semver version in the form of <major>.<minor>.<patch>
12/// If the input has only <major>.<minor>, we expand to <major>.<minor>.0
13pub fn semver_parse(str: &str) -> Result<Version> {
14    let full: Regex = Regex::new(r"^\d+\.\d+\.\d+$")?;
15    let missing_patch: Regex = Regex::new(r"^\d+\.\d+$")?;
16    let full_with_pre = Regex::new(r"^(\d+\.\d+\.\d+)\-(.*)")?;
17    let missing_patch_with_pre = Regex::new(r"^(\d+\.\d+)\-(.*)")?;
18
19    if full.is_match(str) {
20        let cap = full.captures(str).unwrap();
21        let result = cap[0].to_string();
22
23        log_trace!("FULL: {}", result);
24
25        if result.is_empty() {
26            bail!(
27                "[utils][semver_parse] Failed to capture X.X.X regex from [{}]!",
28                str
29            )
30        } else {
31            Ok(Version::parse(&result)?)
32        }
33    } else if missing_patch.is_match(str) {
34        let cap = missing_patch.captures(str).unwrap();
35        let result = cap[0].to_string();
36
37        log_trace!("MISSING: {}", result);
38
39        if result.is_empty() {
40            bail!(
41                "[utils][semver_parse] Failed to capture X.X regex from [{}]!",
42                str
43            )
44        } else {
45            Ok(Version::parse(&format!("{result}.0"))?)
46        }
47    } else if full_with_pre.is_match(str) {
48        let cap = full_with_pre.captures(str).unwrap();
49        let result = cap[0].to_string();
50
51        log_trace!("full_with_pre: {}", result);
52
53        if result.is_empty() {
54            bail!(
55                "[utils][semver_parse] Failed to capture X.X.X-PRE regex from [{}]!",
56                str
57            )
58        } else {
59            Ok(Version::parse(&result)?)
60        }
61    } else if missing_patch_with_pre.is_match(str) {
62        let cap = missing_patch_with_pre.captures(str).unwrap();
63        let result = cap[1].to_string();
64        let pre = cap[2].to_string();
65
66        log_trace!("missing_patch_with_pre: {}", result);
67
68        if result.is_empty() {
69            bail!(
70                "[utils][semver_parse] Failed to capture X.X-PRE regex from [{}]!",
71                str
72            )
73        } else {
74            Ok(Version::parse(&format!("{result}.0-{pre}"))?)
75        }
76    } else {
77        bail!(
78            "[utils][semver_parse] Failed to capture any vesion regex from [{}]!",
79            str
80        )
81    }
82}
83
84/// Parse a provide slice and get a semver version in the form of <major>.<minor>.<patch>
85/// If the input has only <major>.<minor>, we expand to <major>.<minor>.0
86/// In case of failure, return 0.0.0
87pub fn semver_parse_or_default(str: &str) -> Version {
88    semver_parse(str).unwrap_or(Version::new(0, 0, 0))
89}
90
91/// Extract version based on provided pattern capture and delimiter
92/// Example Pattern: "btsys_intbrd_fw_v_(.*).hex"
93/// Example delimiter: '_' to parse "btsys_intbrd_fw_v_0_9.hex" as version 0.9
94pub fn semver_parse_regex(str: &str, pattern: &str, delimiter: &str) -> Result<Version> {
95    let re: Regex = Regex::new(pattern)?;
96
97    if re.is_match(str) {
98        let cap = re.captures(str).unwrap();
99
100        if cap.len() != 2 {
101            bail!(
102                "[utils][semver_parse_regex] Failed to capture any version group from [{}]!",
103                str
104            )
105        }
106
107        let result = cap[1].to_string();
108
109        log_trace!("version capture from str: {}", result);
110
111        let fix_delimiters = result.replace(delimiter, ".");
112
113        semver_parse(&fix_delimiters)
114    } else {
115        bail!(
116            "[utils][semver_parse_regex] Failed to capture any vesion regex from [{}]!",
117            str
118        )
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use semver::{Prerelease, Version};
125
126    use super::{semver_parse, semver_parse_regex};
127
128    #[test]
129    fn semver_parse_test() {
130        let version = "0.9.0";
131        assert_eq!(semver_parse(version).unwrap(), Version::new(0, 9, 0));
132
133        let version = "0.9";
134        assert_eq!(semver_parse(version).unwrap(), Version::new(0, 9, 0));
135
136        let version = "0.9.2-1e341234";
137        let mut expected = Version::new(0, 9, 2);
138        expected.pre = Prerelease::new("1e341234").unwrap();
139        assert_eq!(semver_parse(version).unwrap(), expected);
140
141        let version = "0.9-1-e341234";
142        let mut expected = Version::new(0, 9, 0);
143        expected.pre = Prerelease::new("1-e341234").unwrap();
144        assert_eq!(semver_parse(version).unwrap(), expected);
145
146        let version = "0.9-2";
147        let mut expected = Version::new(0, 9, 0);
148        expected.pre = Prerelease::new("2").unwrap();
149        assert_eq!(semver_parse(version).unwrap(), expected);
150    }
151
152    #[test]
153    fn semver_parse_regex_test() {
154        let input = "btsys_intbrd_boot_config_v_0_9.hex";
155
156        let pattern = "btsys_intbrd_boot_config_v_(.*).hex";
157
158        let expected_version = Version::new(0, 9, 0);
159
160        assert_eq!(
161            semver_parse_regex(&input, &pattern, "_").unwrap(),
162            expected_version
163        );
164    }
165}