1use crate::{Error, Result};
4use serde::{Deserialize, Serialize};
5use std::cmp::Ordering;
6use std::fmt;
7
8#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
10pub struct Version {
11 pub major: u64,
13 pub minor: u64,
15 pub patch: u64,
17 pub prerelease: Option<String>,
19 pub build: Option<String>,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct VersionRange {
26 pub expression: String,
28 pub components: Vec<VersionRangeComponent>,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct VersionRangeComponent {
35 pub operator: VersionOperator,
37 pub version: Version,
39}
40
41#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
43pub enum VersionOperator {
44 Equal,
46 GreaterThan,
48 GreaterThanOrEqual,
50 LessThan,
52 LessThanOrEqual,
54 Caret,
56 Tilde,
58}
59
60pub struct VersionMatcher;
62
63impl Version {
64 pub fn parse(version_str: &str) -> Result<Self> {
66 let version_str = version_str.trim();
67
68 let version_str = version_str.strip_prefix('v').unwrap_or(version_str);
70
71 let (version_part, build) = if let Some(pos) = version_str.find('+') {
73 let (v, b) = version_str.split_at(pos);
74 (v, Some(b[1..].to_string()))
75 } else {
76 (version_str, None)
77 };
78
79 let (core_version, prerelease) = if let Some(pos) = version_part.find('-') {
81 let (v, p) = version_part.split_at(pos);
82 (v, Some(p[1..].to_string()))
83 } else {
84 (version_part, None)
85 };
86
87 let parts: Vec<&str> = core_version.split('.').collect();
89 if parts.len() < 2 {
90 return Err(Error::InvalidVersionConstraint {
91 constraint: version_str.to_string(),
92 });
93 }
94
95 let major = parts[0]
96 .parse()
97 .map_err(|_| Error::InvalidVersionConstraint {
98 constraint: version_str.to_string(),
99 })?;
100
101 let minor = parts[1]
102 .parse()
103 .map_err(|_| Error::InvalidVersionConstraint {
104 constraint: version_str.to_string(),
105 })?;
106
107 let patch = if parts.len() > 2 {
108 parts[2]
109 .parse()
110 .map_err(|_| Error::InvalidVersionConstraint {
111 constraint: version_str.to_string(),
112 })?
113 } else {
114 0
115 };
116
117 Ok(Version {
118 major,
119 minor,
120 patch,
121 prerelease,
122 build,
123 })
124 }
125
126 pub fn is_prerelease(&self) -> bool {
128 self.prerelease.is_some()
129 }
130
131 pub fn core_version(&self) -> String {
133 format!("{}.{}.{}", self.major, self.minor, self.patch)
134 }
135}
136
137impl fmt::Display for Version {
138 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139 write!(f, "{}.{}.{}", self.major, self.minor, self.patch)?;
140
141 if let Some(ref prerelease) = self.prerelease {
142 write!(f, "-{}", prerelease)?;
143 }
144
145 if let Some(ref build) = self.build {
146 write!(f, "+{}", build)?;
147 }
148
149 Ok(())
150 }
151}
152
153impl PartialOrd for Version {
154 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
155 Some(self.cmp(other))
156 }
157}
158
159impl Ord for Version {
160 fn cmp(&self, other: &Self) -> Ordering {
161 match self.major.cmp(&other.major) {
163 Ordering::Equal => {}
164 other => return other,
165 }
166
167 match self.minor.cmp(&other.minor) {
168 Ordering::Equal => {}
169 other => return other,
170 }
171
172 match self.patch.cmp(&other.patch) {
173 Ordering::Equal => {}
174 other => return other,
175 }
176
177 match (&self.prerelease, &other.prerelease) {
179 (None, None) => Ordering::Equal,
180 (Some(_), None) => Ordering::Less, (None, Some(_)) => Ordering::Greater, (Some(a), Some(b)) => a.cmp(b), }
184 }
185}
186
187impl VersionRange {
188 pub fn parse(expression: &str) -> Result<Self> {
190 let expression = expression.trim();
191 let components = Self::parse_components(expression)?;
192
193 Ok(VersionRange {
194 expression: expression.to_string(),
195 components,
196 })
197 }
198
199 pub fn satisfies(&self, version: &Version) -> bool {
201 self.components
202 .iter()
203 .all(|component| component.satisfies(version))
204 }
205
206 fn parse_components(expression: &str) -> Result<Vec<VersionRangeComponent>> {
207 let mut components = Vec::new();
209
210 let (operator, version_str) = if expression.starts_with(">=") {
212 (VersionOperator::GreaterThanOrEqual, &expression[2..])
213 } else if expression.starts_with("<=") {
214 (VersionOperator::LessThanOrEqual, &expression[2..])
215 } else if expression.starts_with('>') {
216 (VersionOperator::GreaterThan, &expression[1..])
217 } else if expression.starts_with('<') {
218 (VersionOperator::LessThan, &expression[1..])
219 } else if expression.starts_with('^') {
220 (VersionOperator::Caret, &expression[1..])
221 } else if expression.starts_with('~') {
222 (VersionOperator::Tilde, &expression[1..])
223 } else if expression.starts_with('=') {
224 (VersionOperator::Equal, &expression[1..])
225 } else {
226 (VersionOperator::Equal, expression)
227 };
228
229 let version = Version::parse(version_str.trim())?;
230 components.push(VersionRangeComponent { operator, version });
231
232 Ok(components)
233 }
234}
235
236impl VersionRangeComponent {
237 pub fn satisfies(&self, version: &Version) -> bool {
239 match self.operator {
240 VersionOperator::Equal => version == &self.version,
241 VersionOperator::GreaterThan => version > &self.version,
242 VersionOperator::GreaterThanOrEqual => version >= &self.version,
243 VersionOperator::LessThan => version < &self.version,
244 VersionOperator::LessThanOrEqual => version <= &self.version,
245 VersionOperator::Caret => self.satisfies_caret(version),
246 VersionOperator::Tilde => self.satisfies_tilde(version),
247 }
248 }
249
250 fn satisfies_caret(&self, version: &Version) -> bool {
251 if version < &self.version {
253 return false;
254 }
255
256 if self.version.major == 0 {
257 version.major == self.version.major && version.minor == self.version.minor
259 } else {
260 version.major == self.version.major
262 }
263 }
264
265 fn satisfies_tilde(&self, version: &Version) -> bool {
266 version >= &self.version
268 && version.major == self.version.major
269 && version.minor == self.version.minor
270 }
271}
272
273impl VersionMatcher {
274 pub fn matches(version_str: &str, constraint: &str) -> Result<bool> {
276 let version = Version::parse(version_str)?;
277 let range = VersionRange::parse(constraint)?;
278 Ok(range.satisfies(&version))
279 }
280
281 pub fn find_best_match(versions: &[String], constraint: &str) -> Result<Option<String>> {
283 let range = VersionRange::parse(constraint)?;
284 let mut matching_versions = Vec::new();
285
286 for version_str in versions {
287 if let Ok(version) = Version::parse(version_str) {
288 if range.satisfies(&version) {
289 matching_versions.push((version, version_str.clone()));
290 }
291 }
292 }
293
294 matching_versions.sort_by(|a, b| b.0.cmp(&a.0));
296 Ok(matching_versions
297 .first()
298 .map(|(_, version_str)| version_str.clone()))
299 }
300}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305
306 #[test]
307 fn test_version_parsing() {
308 let v = Version::parse("1.2.3").unwrap();
309 assert_eq!(v.major, 1);
310 assert_eq!(v.minor, 2);
311 assert_eq!(v.patch, 3);
312 assert!(v.prerelease.is_none());
313
314 let v_pre = Version::parse("2.0.0-beta.1").unwrap();
315 assert_eq!(v_pre.major, 2);
316 assert_eq!(v_pre.prerelease, Some("beta.1".to_string()));
317 assert!(v_pre.is_prerelease());
318
319 let v_build = Version::parse("1.0.0+build.123").unwrap();
320 assert_eq!(v_build.build, Some("build.123".to_string()));
321 }
322
323 #[test]
324 fn test_version_comparison() {
325 let v1 = Version::parse("1.0.0").unwrap();
326 let v2 = Version::parse("1.0.1").unwrap();
327 let v3 = Version::parse("1.1.0").unwrap();
328 let v_pre = Version::parse("1.0.0-beta").unwrap();
329
330 assert!(v1 < v2);
331 assert!(v2 < v3);
332 assert!(v_pre < v1); }
334
335 #[test]
336 fn test_version_range_parsing() {
337 let range = VersionRange::parse(">=1.0.0").unwrap();
338 assert_eq!(range.components.len(), 1);
339 assert_eq!(
340 range.components[0].operator,
341 VersionOperator::GreaterThanOrEqual
342 );
343
344 let caret_range = VersionRange::parse("^1.2.3").unwrap();
345 assert_eq!(caret_range.components[0].operator, VersionOperator::Caret);
346 }
347
348 #[test]
349 fn test_version_matching() {
350 assert!(VersionMatcher::matches("1.2.3", ">=1.0.0").unwrap());
351 assert!(!VersionMatcher::matches("0.9.0", ">=1.0.0").unwrap());
352
353 assert!(VersionMatcher::matches("1.2.5", "^1.2.3").unwrap());
354 assert!(!VersionMatcher::matches("2.0.0", "^1.2.3").unwrap());
355
356 assert!(VersionMatcher::matches("1.2.5", "~1.2.3").unwrap());
357 assert!(!VersionMatcher::matches("1.3.0", "~1.2.3").unwrap());
358 }
359
360 #[test]
361 fn test_find_best_match() {
362 let versions = vec![
363 "1.0.0".to_string(),
364 "1.1.0".to_string(),
365 "1.2.0".to_string(),
366 "2.0.0".to_string(),
367 ];
368
369 let best = VersionMatcher::find_best_match(&versions, "^1.0.0").unwrap();
370 assert_eq!(best, Some("1.2.0".to_string())); let best_exact = VersionMatcher::find_best_match(&versions, "=1.1.0").unwrap();
373 assert_eq!(best_exact, Some("1.1.0".to_string()));
374 }
375}