Skip to main content

git_claw/core/
semver.rs

1//! Semantic versioning validation.
2
3/// Validates whether a version string matches SemVer 2.0.0 (with optional leading 'v').
4pub fn validate_semver(version: &str) -> bool {
5    let s = version.strip_prefix('v').unwrap_or(version);
6    if s.is_empty() {
7        return false;
8    }
9
10    // Split off build metadata (+)
11    let (s, _build) = match s.split_once('+') {
12        Some((core, b)) => {
13            if b.is_empty()
14                || !b
15                    .chars()
16                    .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-')
17            {
18                return false;
19            }
20            (core, Some(b))
21        }
22        None => (s, None),
23    };
24
25    // Split off pre-release (-)
26    let (core, _pre) = match s.split_once('-') {
27        Some((c, p)) => {
28            if p.is_empty()
29                || !p
30                    .chars()
31                    .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-')
32            {
33                return false;
34            }
35            (c, Some(p))
36        }
37        None => (s, None),
38    };
39
40    // Core must be MAJOR.MINOR.PATCH
41    let parts: Vec<&str> = core.split('.').collect();
42    if parts.len() != 3 {
43        return false;
44    }
45
46    for part in parts {
47        if part.is_empty() || !part.chars().all(|c| c.is_ascii_digit()) {
48            return false;
49        }
50        // No leading zero if length > 1
51        if part.len() > 1 && part.starts_with('0') {
52            return false;
53        }
54    }
55
56    true
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62
63    #[test]
64    fn test_valid_semver() {
65        assert!(validate_semver("1.0.0"));
66        assert!(validate_semver("v1.0.0"));
67        assert!(validate_semver("v0.1.0"));
68        assert!(validate_semver("v1.2.3-alpha.1"));
69        assert!(validate_semver("v1.2.3+build.42"));
70        assert!(validate_semver("v1.2.3-rc.1+build.1"));
71    }
72
73    #[test]
74    fn test_invalid_semver() {
75        assert!(!validate_semver(""));
76        assert!(!validate_semver("v"));
77        assert!(!validate_semver("1.0"));
78        assert!(!validate_semver("v1.0"));
79        assert!(!validate_semver("1.0.0.0"));
80        assert!(!validate_semver("v01.0.0"));
81        assert!(!validate_semver("latest"));
82        assert!(!validate_semver("v1.0.0-"));
83    }
84}