1use serde::{Deserialize, Serialize};
2use std::cmp::Ordering;
3use std::fmt;
4use std::str::FromStr;
5use thiserror::Error;
6
7#[derive(Error, Debug)]
8pub enum VersionError {
9 #[error("Invalid version string: {0}")]
10 InvalidVersion(String),
11 #[error("Invalid version specifier: {0}")]
12 InvalidSpecifier(String),
13}
14
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
17pub struct Version {
18 pub major: u64,
19 pub minor: u64,
20 pub patch: u64,
21 pub pre_release: Option<String>,
22 pub local: Option<String>,
23 pub original: String,
25}
26
27impl Version {
28 pub fn new(major: u64, minor: u64, patch: u64) -> Self {
29 Self {
30 major,
31 minor,
32 patch,
33 pre_release: None,
34 local: None,
35 original: format!("{}.{}.{}", major, minor, patch),
36 }
37 }
38
39 pub fn is_prerelease(&self) -> bool {
41 self.pre_release.is_some()
42 }
43
44 pub fn same_major(&self, other: &Version) -> bool {
46 self.major == other.major
47 }
48
49 pub fn same_minor(&self, other: &Version) -> bool {
51 self.major == other.major && self.minor == other.minor
52 }
53}
54
55impl FromStr for Version {
56 type Err = VersionError;
57
58 fn from_str(s: &str) -> Result<Self, Self::Err> {
59 let s = s.trim();
60
61 let (version_part, local) = if let Some(idx) = s.find('+') {
63 (&s[..idx], Some(s[idx + 1..].to_string()))
64 } else {
65 (s, None)
66 };
67
68 let (base_part, pre_release) = parse_prerelease(version_part);
70
71 let parts: Vec<&str> = base_part.split('.').collect();
73
74 let major = parts
75 .first()
76 .and_then(|s| s.parse().ok())
77 .ok_or_else(|| VersionError::InvalidVersion(s.to_string()))?;
78
79 let minor = parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0);
80
81 let patch = parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(0);
82
83 Ok(Version {
84 major,
85 minor,
86 patch,
87 pre_release,
88 local,
89 original: s.to_string(),
90 })
91 }
92}
93
94fn parse_prerelease(s: &str) -> (&str, Option<String>) {
95 let patterns = [
97 "dev", "post", "alpha", "beta", "rc", "a", "b", "c", "-",
98 ];
99
100 for pattern in patterns {
101 if let Some(idx) = s.to_lowercase().find(pattern) {
102 if idx > 0 {
103 return (&s[..idx], Some(s[idx..].to_string()));
104 }
105 }
106 }
107
108 (s, None)
109}
110
111impl Ord for Version {
112 fn cmp(&self, other: &Self) -> Ordering {
113 match self.major.cmp(&other.major) {
114 Ordering::Equal => {}
115 ord => return ord,
116 }
117 match self.minor.cmp(&other.minor) {
118 Ordering::Equal => {}
119 ord => return ord,
120 }
121 match self.patch.cmp(&other.patch) {
122 Ordering::Equal => {}
123 ord => return ord,
124 }
125
126 match (&self.pre_release, &other.pre_release) {
128 (None, Some(_)) => Ordering::Greater,
129 (Some(_), None) => Ordering::Less,
130 (Some(a), Some(b)) => a.cmp(b),
131 (None, None) => Ordering::Equal,
132 }
133 }
134}
135
136impl PartialOrd for Version {
137 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
138 Some(self.cmp(other))
139 }
140}
141
142impl fmt::Display for Version {
143 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144 write!(f, "{}", self.original)
145 }
146}
147
148#[derive(Debug, Clone, PartialEq, Eq)]
150pub enum VersionSpec {
151 Pinned(Version),
153 Minimum(Version),
155 Maximum(Version),
157 GreaterThan(Version),
159 LessThan(Version),
161 Range { min: Version, max: Version },
163 Caret(Version),
165 Tilde(Version),
167 Compatible(Version),
169 Wildcard { prefix: String, pattern: String },
171 NotEqual(Version),
173 Complex(String),
175 Any,
177}
178
179impl VersionSpec {
180 pub fn parse(s: &str) -> Result<Self, VersionError> {
182 let s = s.trim();
183
184 if s.is_empty() || s == "*" {
185 return Ok(VersionSpec::Any);
186 }
187
188 if let Some(version_str) = s.strip_prefix('^') {
190 let version = Version::from_str(version_str)?;
191 return Ok(VersionSpec::Caret(version));
192 }
193
194 if let Some(version_str) = s.strip_prefix("~=") {
196 let version = Version::from_str(version_str)?;
197 return Ok(VersionSpec::Compatible(version));
198 }
199 if let Some(version_str) = s.strip_prefix('~') {
200 let version = Version::from_str(version_str)?;
201 return Ok(VersionSpec::Tilde(version));
202 }
203
204 if s.contains('*') {
206 if let Some(prefix) = s.strip_prefix("==") {
207 return Ok(VersionSpec::Wildcard {
208 prefix: prefix.replace(".*", "").replace("*", ""),
209 pattern: s.to_string(),
210 });
211 }
212 return Ok(VersionSpec::Wildcard {
213 prefix: s.replace(".*", "").replace("*", ""),
214 pattern: s.to_string(),
215 });
216 }
217
218 if s.contains(',') {
220 let parts: Vec<&str> = s.split(',').collect();
221 if parts.len() == 2 {
222 let min_part = parts[0].trim();
223 let max_part = parts[1].trim();
224
225 if let (Some(min_str), Some(max_str)) = (
226 min_part.strip_prefix(">="),
227 max_part.strip_prefix('<'),
228 ) {
229 let min = Version::from_str(min_str)?;
230 let max = Version::from_str(max_str)?;
231 return Ok(VersionSpec::Range { min, max });
232 }
233 }
234 return Ok(VersionSpec::Complex(s.to_string()));
236 }
237
238 if let Some(version_str) = s.strip_prefix("==") {
240 let version = Version::from_str(version_str)?;
241 return Ok(VersionSpec::Pinned(version));
242 }
243 if let Some(version_str) = s.strip_prefix(">=") {
244 let version = Version::from_str(version_str)?;
245 return Ok(VersionSpec::Minimum(version));
246 }
247 if let Some(version_str) = s.strip_prefix("<=") {
248 let version = Version::from_str(version_str)?;
249 return Ok(VersionSpec::Maximum(version));
250 }
251 if let Some(version_str) = s.strip_prefix("!=") {
252 let version = Version::from_str(version_str)?;
253 return Ok(VersionSpec::NotEqual(version));
254 }
255 if let Some(version_str) = s.strip_prefix('>') {
256 let version = Version::from_str(version_str)?;
257 return Ok(VersionSpec::GreaterThan(version));
258 }
259 if let Some(version_str) = s.strip_prefix('<') {
260 let version = Version::from_str(version_str)?;
261 return Ok(VersionSpec::LessThan(version));
262 }
263
264 if let Ok(version) = Version::from_str(s) {
266 return Ok(VersionSpec::Pinned(version));
267 }
268
269 Ok(VersionSpec::Complex(s.to_string()))
270 }
271
272 pub fn satisfies(&self, version: &Version) -> bool {
274 match self {
275 VersionSpec::Any => true,
276 VersionSpec::Pinned(v) => version == v,
277 VersionSpec::Minimum(v) => version >= v,
278 VersionSpec::Maximum(v) => version <= v,
279 VersionSpec::GreaterThan(v) => version > v,
280 VersionSpec::LessThan(v) => version < v,
281 VersionSpec::Range { min, max } => version >= min && version < max,
282 VersionSpec::Caret(v) => version >= v && version.major == v.major,
283 VersionSpec::Tilde(v) => {
284 version >= v && version.major == v.major && version.minor == v.minor
285 }
286 VersionSpec::Compatible(v) => {
287 version >= v && version.major == v.major && version.minor == v.minor
288 }
289 VersionSpec::Wildcard { prefix, .. } => {
290 version.original.starts_with(prefix)
291 }
292 VersionSpec::NotEqual(v) => version != v,
293 VersionSpec::Complex(_) => true, }
295 }
296
297 pub fn base_version(&self) -> Option<&Version> {
299 match self {
300 VersionSpec::Pinned(v)
301 | VersionSpec::Minimum(v)
302 | VersionSpec::Maximum(v)
303 | VersionSpec::GreaterThan(v)
304 | VersionSpec::LessThan(v)
305 | VersionSpec::Caret(v)
306 | VersionSpec::Tilde(v)
307 | VersionSpec::Compatible(v)
308 | VersionSpec::NotEqual(v) => Some(v),
309 VersionSpec::Range { min, .. } => Some(min),
310 VersionSpec::Wildcard { .. } | VersionSpec::Complex(_) | VersionSpec::Any => None,
311 }
312 }
313
314 pub fn max_major(&self) -> Option<u64> {
316 match self {
317 VersionSpec::Range { max, .. } => Some(max.major),
318 VersionSpec::Caret(v) => Some(v.major),
319 VersionSpec::LessThan(v) | VersionSpec::Maximum(v) => Some(v.major),
320 VersionSpec::Minimum(v)
322 | VersionSpec::GreaterThan(v)
323 | VersionSpec::Pinned(v)
324 | VersionSpec::Compatible(v)
325 | VersionSpec::Tilde(v) => Some(v.major),
326 VersionSpec::NotEqual(v) => Some(v.major),
327 VersionSpec::Wildcard { prefix, .. } => {
328 prefix.split('.').next().and_then(|s| s.parse().ok())
329 }
330 VersionSpec::Complex(_) | VersionSpec::Any => None,
331 }
332 }
333
334 pub fn with_version(&self, new_version: &Version) -> VersionSpec {
336 match self {
337 VersionSpec::Pinned(_) => VersionSpec::Pinned(new_version.clone()),
338 VersionSpec::Minimum(_) => VersionSpec::Minimum(new_version.clone()),
339 VersionSpec::Maximum(_) => VersionSpec::Maximum(new_version.clone()),
340 VersionSpec::GreaterThan(_) => VersionSpec::GreaterThan(new_version.clone()),
341 VersionSpec::LessThan(_) => VersionSpec::LessThan(new_version.clone()),
342 VersionSpec::Range { max, .. } => {
343 if new_version >= max {
345 VersionSpec::Range {
346 min: new_version.clone(),
347 max: Version::new(new_version.major + 1, 0, 0),
348 }
349 } else {
350 VersionSpec::Range {
351 min: new_version.clone(),
352 max: max.clone(),
353 }
354 }
355 }
356 VersionSpec::Caret(_) => VersionSpec::Caret(new_version.clone()),
357 VersionSpec::Tilde(_) => VersionSpec::Tilde(new_version.clone()),
358 VersionSpec::Compatible(_) => VersionSpec::Compatible(new_version.clone()),
359 VersionSpec::Wildcard { pattern, .. } => {
360 VersionSpec::Wildcard {
362 prefix: format!("{}.{}", new_version.major, new_version.minor),
363 pattern: pattern.clone(),
364 }
365 }
366 VersionSpec::NotEqual(_) => VersionSpec::NotEqual(new_version.clone()),
367 VersionSpec::Complex(s) => VersionSpec::Complex(s.clone()),
368 VersionSpec::Any => VersionSpec::Any,
369 }
370 }
371}
372
373impl fmt::Display for VersionSpec {
374 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
375 match self {
376 VersionSpec::Any => write!(f, "*"),
377 VersionSpec::Pinned(v) => write!(f, "=={}", v),
378 VersionSpec::Minimum(v) => write!(f, ">={}", v),
379 VersionSpec::Maximum(v) => write!(f, "<={}", v),
380 VersionSpec::GreaterThan(v) => write!(f, ">{}", v),
381 VersionSpec::LessThan(v) => write!(f, "<{}", v),
382 VersionSpec::Range { min, max } => write!(f, ">={},<{}", min, max),
383 VersionSpec::Caret(v) => write!(f, "^{}", v),
384 VersionSpec::Tilde(v) => write!(f, "~{}", v),
385 VersionSpec::Compatible(v) => write!(f, "~={}", v),
386 VersionSpec::Wildcard { prefix, .. } => write!(f, "=={}.*", prefix),
387 VersionSpec::NotEqual(v) => write!(f, "!={}", v),
388 VersionSpec::Complex(s) => write!(f, "{}", s),
389 }
390 }
391}
392
393#[cfg(test)]
394mod tests {
395 use super::*;
396
397 #[test]
398 fn test_parse_version() {
399 let v = Version::from_str("1.2.3").unwrap();
400 assert_eq!(v.major, 1);
401 assert_eq!(v.minor, 2);
402 assert_eq!(v.patch, 3);
403
404 let v = Version::from_str("2.0").unwrap();
405 assert_eq!(v.major, 2);
406 assert_eq!(v.minor, 0);
407 assert_eq!(v.patch, 0);
408 }
409
410 #[test]
411 fn test_version_comparison() {
412 let v1 = Version::from_str("1.2.3").unwrap();
413 let v2 = Version::from_str("1.2.4").unwrap();
414 let v3 = Version::from_str("2.0.0").unwrap();
415
416 assert!(v1 < v2);
417 assert!(v2 < v3);
418 assert!(v1 < v3);
419 }
420
421 #[test]
422 fn test_parse_version_spec() {
423 assert!(matches!(
424 VersionSpec::parse("==1.2.3").unwrap(),
425 VersionSpec::Pinned(_)
426 ));
427 assert!(matches!(
428 VersionSpec::parse(">=1.2.3").unwrap(),
429 VersionSpec::Minimum(_)
430 ));
431 assert!(matches!(
432 VersionSpec::parse("^1.2.3").unwrap(),
433 VersionSpec::Caret(_)
434 ));
435 assert!(matches!(
436 VersionSpec::parse(">=1.0.0,<2.0.0").unwrap(),
437 VersionSpec::Range { .. }
438 ));
439 }
440
441 #[test]
442 fn test_satisfies() {
443 let spec = VersionSpec::parse(">=1.0.0,<2.0.0").unwrap();
444 assert!(spec.satisfies(&Version::from_str("1.5.0").unwrap()));
445 assert!(!spec.satisfies(&Version::from_str("2.0.0").unwrap()));
446 assert!(!spec.satisfies(&Version::from_str("0.9.0").unwrap()));
447 }
448}