Skip to main content

aster/plugins/
version.rs

1//! 版本检查工具
2//!
3//! 提供 semver 版本比较和范围检查功能
4
5use std::cmp::Ordering;
6
7/// 解析后的版本号
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct Version {
10    pub major: u32,
11    pub minor: u32,
12    pub patch: u32,
13}
14
15impl Version {
16    /// 解析版本字符串
17    pub fn parse(version: &str) -> Option<Self> {
18        let parts: Vec<&str> = version.split('.').collect();
19        if parts.len() < 3 {
20            return None;
21        }
22
23        Some(Self {
24            major: parts[0].parse().ok()?,
25            minor: parts[1].parse().ok()?,
26            patch: parts[2].split('-').next()?.parse().ok()?,
27        })
28    }
29}
30
31impl PartialOrd for Version {
32    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
33        Some(self.cmp(other))
34    }
35}
36
37impl Ord for Version {
38    fn cmp(&self, other: &Self) -> Ordering {
39        match self.major.cmp(&other.major) {
40            Ordering::Equal => match self.minor.cmp(&other.minor) {
41                Ordering::Equal => self.patch.cmp(&other.patch),
42                ord => ord,
43            },
44            ord => ord,
45        }
46    }
47}
48
49/// 版本检查器
50pub struct VersionChecker;
51
52impl VersionChecker {
53    /// 检查版本是否满足范围要求
54    /// 支持: ^1.0.0, ~1.0.0, >=1.0.0, >1.0.0, <=1.0.0, <1.0.0, 1.0.0, *
55    pub fn satisfies(version: &str, range: &str) -> bool {
56        if range == "*" || range == "latest" {
57            return true;
58        }
59
60        let v = match Version::parse(version) {
61            Some(v) => v,
62            None => return false,
63        };
64
65        // ^1.0.0 - 兼容主版本
66        if let Some(range_ver) = range.strip_prefix('^') {
67            if let Some(r) = Version::parse(range_ver) {
68                return v.major == r.major && v >= r;
69            }
70            return false;
71        }
72
73        // ~1.0.0 - 兼容次版本
74        if let Some(range_ver) = range.strip_prefix('~') {
75            if let Some(r) = Version::parse(range_ver) {
76                return v.major == r.major && v.minor == r.minor && v.patch >= r.patch;
77            }
78            return false;
79        }
80
81        // >=1.0.0
82        if let Some(range_ver) = range.strip_prefix(">=") {
83            if let Some(r) = Version::parse(range_ver) {
84                return v >= r;
85            }
86            return false;
87        }
88
89        // >1.0.0
90        if let Some(range_ver) = range.strip_prefix('>') {
91            if let Some(r) = Version::parse(range_ver) {
92                return v > r;
93            }
94            return false;
95        }
96
97        // <=1.0.0
98        if let Some(range_ver) = range.strip_prefix("<=") {
99            if let Some(r) = Version::parse(range_ver) {
100                return v <= r;
101            }
102            return false;
103        }
104
105        // <1.0.0
106        if let Some(range_ver) = range.strip_prefix('<') {
107            if let Some(r) = Version::parse(range_ver) {
108                return v < r;
109            }
110            return false;
111        }
112
113        // 精确匹配
114        version == range
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn test_version_parse() {
124        let v = Version::parse("1.2.3").unwrap();
125        assert_eq!(v.major, 1);
126        assert_eq!(v.minor, 2);
127        assert_eq!(v.patch, 3);
128    }
129
130    #[test]
131    fn test_version_parse_with_prerelease() {
132        let v = Version::parse("1.2.3-beta.1").unwrap();
133        assert_eq!(v.major, 1);
134        assert_eq!(v.minor, 2);
135        assert_eq!(v.patch, 3);
136    }
137
138    #[test]
139    fn test_version_parse_invalid() {
140        assert!(Version::parse("1.2").is_none());
141        assert!(Version::parse("invalid").is_none());
142        assert!(Version::parse("").is_none());
143    }
144
145    #[test]
146    fn test_version_ordering() {
147        let v1 = Version::parse("1.0.0").unwrap();
148        let v2 = Version::parse("1.0.1").unwrap();
149        let v3 = Version::parse("1.1.0").unwrap();
150        let v4 = Version::parse("2.0.0").unwrap();
151
152        assert!(v1 < v2);
153        assert!(v2 < v3);
154        assert!(v3 < v4);
155        assert!(v1 == Version::parse("1.0.0").unwrap());
156    }
157
158    #[test]
159    fn test_caret_range() {
160        assert!(VersionChecker::satisfies("1.2.3", "^1.0.0"));
161        assert!(VersionChecker::satisfies("1.9.9", "^1.0.0"));
162        assert!(!VersionChecker::satisfies("2.0.0", "^1.0.0"));
163        assert!(!VersionChecker::satisfies("0.9.9", "^1.0.0"));
164    }
165
166    #[test]
167    fn test_tilde_range() {
168        assert!(VersionChecker::satisfies("1.2.3", "~1.2.0"));
169        assert!(VersionChecker::satisfies("1.2.9", "~1.2.0"));
170        assert!(!VersionChecker::satisfies("1.3.0", "~1.2.0"));
171        assert!(!VersionChecker::satisfies("1.1.9", "~1.2.0"));
172    }
173
174    #[test]
175    fn test_comparison_ranges() {
176        // >=
177        assert!(VersionChecker::satisfies("1.0.0", ">=1.0.0"));
178        assert!(VersionChecker::satisfies("2.0.0", ">=1.0.0"));
179        assert!(!VersionChecker::satisfies("0.9.9", ">=1.0.0"));
180
181        // >
182        assert!(VersionChecker::satisfies("1.0.1", ">1.0.0"));
183        assert!(!VersionChecker::satisfies("1.0.0", ">1.0.0"));
184
185        // <=
186        assert!(VersionChecker::satisfies("1.0.0", "<=1.0.0"));
187        assert!(VersionChecker::satisfies("0.9.9", "<=1.0.0"));
188        assert!(!VersionChecker::satisfies("1.0.1", "<=1.0.0"));
189
190        // <
191        assert!(VersionChecker::satisfies("0.9.9", "<1.0.0"));
192        assert!(!VersionChecker::satisfies("1.0.0", "<1.0.0"));
193    }
194
195    #[test]
196    fn test_wildcard_range() {
197        assert!(VersionChecker::satisfies("1.0.0", "*"));
198        assert!(VersionChecker::satisfies("99.99.99", "*"));
199        assert!(VersionChecker::satisfies("0.0.1", "latest"));
200    }
201
202    #[test]
203    fn test_exact_match() {
204        assert!(VersionChecker::satisfies("1.2.3", "1.2.3"));
205        assert!(!VersionChecker::satisfies("1.2.4", "1.2.3"));
206    }
207}