1use std::cmp::Ordering;
6
7#[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 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
49pub struct VersionChecker;
51
52impl VersionChecker {
53 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 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 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 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 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 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 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 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 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 assert!(VersionChecker::satisfies("1.0.1", ">1.0.0"));
183 assert!(!VersionChecker::satisfies("1.0.0", ">1.0.0"));
184
185 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 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}