1use std::cmp::Ordering;
4use std::fmt;
5use std::str::FromStr;
6
7use serde::{Deserialize, Serialize};
8
9use crate::Error;
10
11#[derive(Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
13pub struct Version {
14 pub major: u64,
16 pub minor: u64,
18 pub patch: u64,
20 pub pre: Prerelease,
22 pub build: BuildMetadata,
24}
25
26impl Version {
27 pub const fn new(major: u64, minor: u64, patch: u64) -> Self {
29 Self {
30 major,
31 minor,
32 patch,
33 pre: Prerelease::EMPTY,
34 build: BuildMetadata::EMPTY,
35 }
36 }
37
38 pub fn parse(text: &str) -> Result<Self, Error> {
40 let text = text.trim();
41
42 let text = text
44 .strip_prefix('v')
45 .or_else(|| text.strip_prefix('V'))
46 .unwrap_or(text);
47
48 if text.is_empty() {
49 return Err(Error::InvalidVersion("empty version string".to_string()));
50 }
51
52 let (version_pre, build) = match text.find('+') {
54 Some(pos) => {
55 let build = BuildMetadata::new(&text[pos + 1..])?;
56 (&text[..pos], build)
57 }
58 None => (text, BuildMetadata::EMPTY),
59 };
60
61 let (version, pre) = match version_pre.find('-') {
63 Some(pos) => {
64 let pre = Prerelease::new(&version_pre[pos + 1..])?;
65 (&version_pre[..pos], pre)
66 }
67 None => (version_pre, Prerelease::EMPTY),
68 };
69
70 let mut parts = version.split('.');
72
73 let major = parts
74 .next()
75 .ok_or_else(|| Error::InvalidVersion("missing major version".to_string()))?
76 .parse::<u64>()
77 .map_err(|_| Error::InvalidVersion("invalid major version".to_string()))?;
78
79 let minor = parts
80 .next()
81 .map(|s| s.parse::<u64>())
82 .transpose()
83 .map_err(|_| Error::InvalidVersion("invalid minor version".to_string()))?
84 .unwrap_or(0);
85
86 let patch = parts
87 .next()
88 .map(|s| s.parse::<u64>())
89 .transpose()
90 .map_err(|_| Error::InvalidVersion("invalid patch version".to_string()))?
91 .unwrap_or(0);
92
93 if parts.next().is_some() {
95 return Err(Error::InvalidVersion("too many version parts".to_string()));
96 }
97
98 Ok(Self {
99 major,
100 minor,
101 patch,
102 pre,
103 build,
104 })
105 }
106
107 pub fn is_prerelease(&self) -> bool {
109 !self.pre.is_empty()
110 }
111
112 pub fn bump_major(&self) -> Self {
114 Self::new(self.major + 1, 0, 0)
115 }
116
117 pub fn bump_minor(&self) -> Self {
119 Self::new(self.major, self.minor + 1, 0)
120 }
121
122 pub fn bump_patch(&self) -> Self {
124 Self::new(self.major, self.minor, self.patch + 1)
125 }
126
127 pub fn with_prerelease(mut self, pre: Prerelease) -> Self {
129 self.pre = pre;
130 self
131 }
132
133 pub fn with_build(mut self, build: BuildMetadata) -> Self {
135 self.build = build;
136 self
137 }
138
139 pub fn base(&self) -> Self {
141 Self::new(self.major, self.minor, self.patch)
142 }
143}
144
145impl fmt::Display for Version {
146 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
147 write!(f, "{}.{}.{}", self.major, self.minor, self.patch)?;
148 if !self.pre.is_empty() {
149 write!(f, "-{}", self.pre)?;
150 }
151 if !self.build.is_empty() {
152 write!(f, "+{}", self.build)?;
153 }
154 Ok(())
155 }
156}
157
158impl fmt::Debug for Version {
159 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
160 write!(f, "Version({})", self)
161 }
162}
163
164impl FromStr for Version {
165 type Err = Error;
166
167 fn from_str(s: &str) -> Result<Self, Self::Err> {
168 Self::parse(s)
169 }
170}
171
172impl Ord for Version {
173 fn cmp(&self, other: &Self) -> Ordering {
174 match self.major.cmp(&other.major) {
176 Ordering::Equal => {}
177 ord => return ord,
178 }
179 match self.minor.cmp(&other.minor) {
180 Ordering::Equal => {}
181 ord => return ord,
182 }
183 match self.patch.cmp(&other.patch) {
184 Ordering::Equal => {}
185 ord => return ord,
186 }
187
188 self.pre.cmp(&other.pre)
192 }
193}
194
195impl PartialOrd for Version {
196 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
197 Some(self.cmp(other))
198 }
199}
200
201impl Default for Version {
202 fn default() -> Self {
203 Self::new(0, 1, 0)
204 }
205}
206
207#[derive(Clone, Eq, PartialEq, Hash, Default, Serialize, Deserialize)]
209pub struct Prerelease {
210 identifier: String,
211}
212
213impl Prerelease {
214 pub const EMPTY: Self = Self {
216 identifier: String::new(),
217 };
218
219 pub fn new(s: &str) -> Result<Self, Error> {
221 if s.is_empty() {
222 return Ok(Self::EMPTY);
223 }
224
225 for part in s.split('.') {
227 if part.is_empty() {
228 return Err(Error::InvalidVersion(
229 "empty prerelease identifier".to_string(),
230 ));
231 }
232 if !part.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') {
233 return Err(Error::InvalidVersion(format!(
234 "invalid prerelease identifier: {}",
235 part
236 )));
237 }
238 }
239
240 Ok(Self {
241 identifier: s.to_string(),
242 })
243 }
244
245 pub fn is_empty(&self) -> bool {
247 self.identifier.is_empty()
248 }
249
250 pub fn as_str(&self) -> &str {
252 &self.identifier
253 }
254
255 fn parts(&self) -> impl Iterator<Item = PrereleasePart<'_>> {
257 self.identifier.split('.').map(|s| {
258 if let Ok(n) = s.parse::<u64>() {
259 PrereleasePart::Numeric(n)
260 } else {
261 PrereleasePart::Alphanumeric(s)
262 }
263 })
264 }
265}
266
267#[derive(Eq, PartialEq)]
268enum PrereleasePart<'a> {
269 Numeric(u64),
270 Alphanumeric(&'a str),
271}
272
273impl Ord for PrereleasePart<'_> {
274 fn cmp(&self, other: &Self) -> Ordering {
275 match (self, other) {
276 (PrereleasePart::Numeric(_), PrereleasePart::Alphanumeric(_)) => Ordering::Less,
278 (PrereleasePart::Alphanumeric(_), PrereleasePart::Numeric(_)) => Ordering::Greater,
279 (PrereleasePart::Numeric(a), PrereleasePart::Numeric(b)) => a.cmp(b),
281 (PrereleasePart::Alphanumeric(a), PrereleasePart::Alphanumeric(b)) => a.cmp(b),
283 }
284 }
285}
286
287impl PartialOrd for PrereleasePart<'_> {
288 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
289 Some(self.cmp(other))
290 }
291}
292
293impl Ord for Prerelease {
294 fn cmp(&self, other: &Self) -> Ordering {
295 match (self.is_empty(), other.is_empty()) {
297 (true, true) => Ordering::Equal,
298 (true, false) => Ordering::Greater,
299 (false, true) => Ordering::Less,
300 (false, false) => {
301 let mut self_parts = self.parts();
303 let mut other_parts = other.parts();
304
305 loop {
306 match (self_parts.next(), other_parts.next()) {
307 (None, None) => return Ordering::Equal,
308 (None, Some(_)) => return Ordering::Less,
309 (Some(_), None) => return Ordering::Greater,
310 (Some(a), Some(b)) => match a.cmp(&b) {
311 Ordering::Equal => continue,
312 ord => return ord,
313 },
314 }
315 }
316 }
317 }
318 }
319}
320
321impl PartialOrd for Prerelease {
322 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
323 Some(self.cmp(other))
324 }
325}
326
327impl fmt::Display for Prerelease {
328 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
329 f.write_str(&self.identifier)
330 }
331}
332
333impl fmt::Debug for Prerelease {
334 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
335 if self.is_empty() {
336 write!(f, "Prerelease::EMPTY")
337 } else {
338 write!(f, "Prerelease({})", self.identifier)
339 }
340 }
341}
342
343#[derive(Clone, Eq, PartialEq, Hash, Default, Serialize, Deserialize)]
345pub struct BuildMetadata {
346 identifier: String,
347}
348
349impl BuildMetadata {
350 pub const EMPTY: Self = Self {
352 identifier: String::new(),
353 };
354
355 pub fn new(s: &str) -> Result<Self, Error> {
357 if s.is_empty() {
358 return Ok(Self::EMPTY);
359 }
360
361 for part in s.split('.') {
363 if part.is_empty() {
364 return Err(Error::InvalidVersion("empty build metadata".to_string()));
365 }
366 if !part.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') {
367 return Err(Error::InvalidVersion(format!(
368 "invalid build metadata: {}",
369 part
370 )));
371 }
372 }
373
374 Ok(Self {
375 identifier: s.to_string(),
376 })
377 }
378
379 pub fn is_empty(&self) -> bool {
381 self.identifier.is_empty()
382 }
383
384 pub fn as_str(&self) -> &str {
386 &self.identifier
387 }
388}
389
390impl fmt::Display for BuildMetadata {
391 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
392 f.write_str(&self.identifier)
393 }
394}
395
396impl fmt::Debug for BuildMetadata {
397 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
398 if self.is_empty() {
399 write!(f, "BuildMetadata::EMPTY")
400 } else {
401 write!(f, "BuildMetadata({})", self.identifier)
402 }
403 }
404}
405
406#[cfg(test)]
407mod tests {
408 use super::*;
409
410 #[test]
411 fn test_parse_simple() {
412 let v = Version::parse("1.2.3").unwrap();
413 assert_eq!(v.major, 1);
414 assert_eq!(v.minor, 2);
415 assert_eq!(v.patch, 3);
416 assert!(v.pre.is_empty());
417 assert!(v.build.is_empty());
418 }
419
420 #[test]
421 fn test_parse_with_v_prefix() {
422 let v = Version::parse("v1.2.3").unwrap();
423 assert_eq!(v.major, 1);
424 assert_eq!(v.minor, 2);
425 assert_eq!(v.patch, 3);
426 }
427
428 #[test]
429 fn test_parse_partial() {
430 let v = Version::parse("1").unwrap();
431 assert_eq!(v.major, 1);
432 assert_eq!(v.minor, 0);
433 assert_eq!(v.patch, 0);
434
435 let v = Version::parse("1.2").unwrap();
436 assert_eq!(v.major, 1);
437 assert_eq!(v.minor, 2);
438 assert_eq!(v.patch, 0);
439 }
440
441 #[test]
442 fn test_parse_prerelease() {
443 let v = Version::parse("1.0.0-alpha").unwrap();
444 assert_eq!(v.pre.as_str(), "alpha");
445
446 let v = Version::parse("1.0.0-alpha.1").unwrap();
447 assert_eq!(v.pre.as_str(), "alpha.1");
448
449 let v = Version::parse("1.0.0-0.3.7").unwrap();
450 assert_eq!(v.pre.as_str(), "0.3.7");
451
452 let v = Version::parse("1.0.0-x.7.z.92").unwrap();
453 assert_eq!(v.pre.as_str(), "x.7.z.92");
454 }
455
456 #[test]
457 fn test_parse_build_metadata() {
458 let v = Version::parse("1.0.0+build.123").unwrap();
459 assert_eq!(v.build.as_str(), "build.123");
460
461 let v = Version::parse("1.0.0-alpha+001").unwrap();
462 assert_eq!(v.pre.as_str(), "alpha");
463 assert_eq!(v.build.as_str(), "001");
464 }
465
466 #[test]
467 fn test_display() {
468 assert_eq!(Version::new(1, 2, 3).to_string(), "1.2.3");
469 assert_eq!(
470 Version::parse("1.0.0-alpha").unwrap().to_string(),
471 "1.0.0-alpha"
472 );
473 assert_eq!(
474 Version::parse("1.0.0+build").unwrap().to_string(),
475 "1.0.0+build"
476 );
477 assert_eq!(
478 Version::parse("1.0.0-alpha+build").unwrap().to_string(),
479 "1.0.0-alpha+build"
480 );
481 }
482
483 #[test]
484 fn test_ordering_basic() {
485 assert!(Version::new(2, 0, 0) > Version::new(1, 0, 0));
486 assert!(Version::new(1, 1, 0) > Version::new(1, 0, 0));
487 assert!(Version::new(1, 0, 1) > Version::new(1, 0, 0));
488 }
489
490 #[test]
491 fn test_ordering_prerelease() {
492 assert!(Version::new(1, 0, 0) > Version::parse("1.0.0-alpha").unwrap());
494
495 let alpha = Version::parse("1.0.0-alpha").unwrap();
497 let beta = Version::parse("1.0.0-beta").unwrap();
498 let rc = Version::parse("1.0.0-rc").unwrap();
499 assert!(alpha < beta);
500 assert!(beta < rc);
501
502 let alpha1 = Version::parse("1.0.0-alpha.1").unwrap();
504 let alpha2 = Version::parse("1.0.0-alpha.2").unwrap();
505 let alpha10 = Version::parse("1.0.0-alpha.10").unwrap();
506 assert!(alpha1 < alpha2);
507 assert!(alpha2 < alpha10);
508
509 let pre_1 = Version::parse("1.0.0-1").unwrap();
511 let pre_alpha = Version::parse("1.0.0-alpha").unwrap();
512 assert!(pre_1 < pre_alpha);
513 }
514
515 #[test]
516 fn test_ordering_build_metadata_ignored() {
517 let v1 = Version::parse("1.0.0+build1").unwrap();
518 let v2 = Version::parse("1.0.0+build2").unwrap();
519 assert_eq!(v1.cmp(&v2), Ordering::Equal);
520 }
521
522 #[test]
523 fn test_bump() {
524 let v = Version::new(1, 2, 3);
525 assert_eq!(v.bump_major(), Version::new(2, 0, 0));
526 assert_eq!(v.bump_minor(), Version::new(1, 3, 0));
527 assert_eq!(v.bump_patch(), Version::new(1, 2, 4));
528 }
529
530 #[test]
531 fn test_bump_clears_prerelease() {
532 let v = Version::parse("1.0.0-alpha").unwrap();
533 assert!(v.bump_patch().pre.is_empty());
534 }
535
536 #[test]
537 fn test_is_prerelease() {
538 assert!(!Version::new(1, 0, 0).is_prerelease());
539 assert!(Version::parse("1.0.0-alpha").unwrap().is_prerelease());
540 }
541
542 #[test]
543 fn test_base() {
544 let v = Version::parse("1.2.3-alpha+build").unwrap();
545 let base = v.base();
546 assert_eq!(base, Version::new(1, 2, 3));
547 assert!(base.pre.is_empty());
548 assert!(base.build.is_empty());
549 }
550}