Skip to main content

semver/
semver.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2#![cfg_attr(not(test), no_std)]
3
4//! A [Semantic Versioning 2.0.0] parser and comparator.
5//!
6//! # Usage
7//!
8//! ```
9//! use semver::parse;
10//!
11//! let v = parse("1.2.3").unwrap();
12//! assert_eq!(v.major, 1);
13//! assert_eq!(v.minor, 2);
14//! assert_eq!(v.patch, 3);
15//!
16//! let v = parse("1.0.0-alpha.1+build.123").unwrap();
17//! assert_eq!(v.to_string(), "1.0.0-alpha.1+build.123");
18//! ```
19//!
20//! Parsing follows the full SemVer 2.0.0 BNF grammar, including pre-release
21//! identifiers with hyphens, leading-zero rejection on numeric identifiers,
22//! and build metadata validation.
23//!
24//! # Version struct
25//!
26//! The [`Version`] struct borrows its pre-release and build-metadata strings
27//! from the input, so no allocation is needed for parsing.
28//!
29//! # Precedence
30//!
31//! [`Version`] implements `PartialEq`, `Eq`, `PartialOrd`, and `Ord`
32//! following the SemVer precedence rules:
33//!
34//! - Compare `major`, `minor`, `patch` numerically.
35//! - A pre-release version has lower precedence than a normal version.
36//! - Pre-release identifiers are compared left-to-right: numeric identifiers
37//!   are compared numerically, alphanumeric identifiers are compared lexically
38//!   (ASCII), and numeric always precedes alpha.
39//! - A longer pre-release has higher precedence when all preceding identifiers
40//!   are equal.
41//! - Build metadata is ignored for equality and ordering.
42//!
43//! ```
44//! use semver::parse;
45//!
46//! assert!(parse("1.0.0-alpha").unwrap() < parse("1.0.0").unwrap());
47//! assert!(parse("1.0.0-beta.2").unwrap() < parse("1.0.0-beta.11").unwrap());
48//! assert!(parse("1.0.0-1").unwrap() < parse("1.0.0-alpha").unwrap());
49//! assert_eq!(parse("1.0.0+build1").unwrap(), parse("1.0.0+build2").unwrap());
50//! ```
51//!
52//! # Serde support
53//!
54//! Enable the `serde` feature to serialize/deserialize [`Version`] as a string:
55//!
56//! ```toml
57//! [dependencies]
58//! semver = { path = "../semver", features = ["serde"] }
59//! ```
60//!
61//! ```
62//! # #[cfg(feature = "serde")] fn _serde_example() {
63//! use serde_json;
64//! use semver::parse;
65//!
66//! let v: semver::Version<'_> = serde_json::from_str("\"1.2.3-alpha+build\"").unwrap();
67//! assert_eq!(v, parse("1.2.3-alpha+build").unwrap());
68//! # }
69//! ```
70//!
71//! # Error handling
72//!
73//! ```
74//! use semver::parse;
75//!
76//! assert!(parse("01.2.3").is_err());
77//! assert!(parse("1.0.0-").is_err());
78//! assert!(parse("").is_err());
79//! ```
80//!
81//! [Semantic Versioning 2.0.0]: https://semver.org/spec/v2.0.0.html
82
83extern crate alloc;
84
85#[cfg(feature = "serde")]
86mod serde;
87
88use alloc::vec::Vec;
89use core::{cmp::Ordering, fmt};
90
91/// Errors that can occur when parsing a SemVer version string.
92#[derive(Debug, Clone, PartialEq, Eq)]
93pub enum ParseError {
94    EmptyInput,
95    InvalidCharacter,
96    LeadingZero,
97    EmptyIdentifier,
98    InvalidNumber,
99    InvalidFormat,
100    TrailingData,
101}
102
103impl fmt::Display for ParseError {
104    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105        match self {
106            ParseError::EmptyInput => f.write_str("empty input"),
107            ParseError::InvalidCharacter => f.write_str("invalid character in version string"),
108            ParseError::LeadingZero => f.write_str("numeric identifier contains leading zero"),
109            ParseError::EmptyIdentifier => f.write_str("empty identifier"),
110            ParseError::InvalidNumber => f.write_str("invalid numeric identifier"),
111            ParseError::InvalidFormat => f.write_str("invalid version format"),
112            ParseError::TrailingData => f.write_str("unexpected trailing data"),
113        }
114    }
115}
116
117impl core::error::Error for ParseError {}
118
119/// A parsed pre-release identifier: either a numeric value or an alphanumeric string.
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121pub enum Identifier<'a> {
122    Numeric(u64),
123    Alpha(&'a str),
124}
125
126impl<'a> Identifier<'a> {
127    fn parse(input: &'a str) -> Result<Self, ParseError> {
128        if input.is_empty() {
129            return Err(ParseError::EmptyIdentifier);
130        }
131        for c in input.chars() {
132            if !c.is_ascii_alphanumeric() && c != '-' {
133                return Err(ParseError::InvalidCharacter);
134            }
135        }
136        let all_digits = input.chars().all(|c| c.is_ascii_digit());
137        if all_digits {
138            if input.len() > 1 && input.starts_with('0') {
139                return Err(ParseError::LeadingZero);
140            }
141            let n = input.parse::<u64>().map_err(|_| ParseError::InvalidNumber)?;
142            Ok(Identifier::Numeric(n))
143        } else {
144            Ok(Identifier::Alpha(input))
145        }
146    }
147}
148
149impl<'a> PartialOrd for Identifier<'a> {
150    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
151        Some(self.cmp(other))
152    }
153}
154
155impl<'a> Ord for Identifier<'a> {
156    fn cmp(&self, other: &Self) -> Ordering {
157        match (self, other) {
158            (Identifier::Numeric(_), Identifier::Alpha(_)) => Ordering::Less,
159            (Identifier::Alpha(_), Identifier::Numeric(_)) => Ordering::Greater,
160            (Identifier::Numeric(a), Identifier::Numeric(b)) => a.cmp(b),
161            (Identifier::Alpha(a), Identifier::Alpha(b)) => a.cmp(b),
162        }
163    }
164}
165
166/// The pre-release portion of a SemVer version (after the `-`).
167#[derive(Debug, Clone, PartialEq, Eq)]
168pub struct PreRelease<'a> {
169    identifiers: Vec<Identifier<'a>>,
170}
171
172impl<'a> PreRelease<'a> {
173    fn parse(input: &'a str) -> Result<Self, ParseError> {
174        if input.is_empty() {
175            return Err(ParseError::EmptyIdentifier);
176        }
177        let identifiers = input.split('.').map(Identifier::parse).collect::<Result<Vec<_>, _>>()?;
178        Ok(PreRelease {
179            identifiers,
180        })
181    }
182
183    pub fn identifiers(&self) -> &[Identifier<'a>] {
184        &self.identifiers
185    }
186}
187
188impl<'a> fmt::Display for PreRelease<'a> {
189    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
190        for (i, ident) in self.identifiers.iter().enumerate() {
191            if i > 0 {
192                f.write_str(".")?;
193            }
194            match ident {
195                Identifier::Numeric(n) => write!(f, "{n}"),
196                Identifier::Alpha(s) => f.write_str(s),
197            }?;
198        }
199        Ok(())
200    }
201}
202
203impl<'a> PartialOrd for PreRelease<'a> {
204    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
205        Some(self.cmp(other))
206    }
207}
208
209impl<'a> Ord for PreRelease<'a> {
210    fn cmp(&self, other: &Self) -> Ordering {
211        for (a, b) in self.identifiers.iter().zip(other.identifiers.iter()) {
212            match a.cmp(b) {
213                Ordering::Equal => continue,
214                non_eq => return non_eq,
215            }
216        }
217        self.identifiers.len().cmp(&other.identifiers.len())
218    }
219}
220
221/// The build metadata portion of a SemVer version (after the `+`).
222#[derive(Debug, Clone, Copy, PartialEq, Eq)]
223pub struct BuildMetadata<'a> {
224    raw: &'a str,
225}
226
227impl<'a> BuildMetadata<'a> {
228    fn parse(input: &'a str) -> Result<Self, ParseError> {
229        if input.is_empty() {
230            return Err(ParseError::EmptyIdentifier);
231        }
232        for part in input.split('.') {
233            if part.is_empty() {
234                return Err(ParseError::EmptyIdentifier);
235            }
236            for c in part.chars() {
237                if !c.is_ascii_alphanumeric() && c != '-' {
238                    return Err(ParseError::InvalidCharacter);
239                }
240            }
241        }
242        Ok(BuildMetadata {
243            raw: input,
244        })
245    }
246
247    pub fn as_str(&self) -> &'a str {
248        self.raw
249    }
250}
251
252impl<'a> fmt::Display for BuildMetadata<'a> {
253    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
254        f.write_str(self.raw)
255    }
256}
257
258/// A parsed SemVer version (Semantic Versioning 2.0.0).
259///
260/// ```
261/// use semver::{parse, Version};
262///
263/// let v = parse("1.2.3-alpha.1+build.42").unwrap();
264/// assert_eq!(v.major, 1);
265/// assert_eq!(v.minor, 2);
266/// assert_eq!(v.patch, 3);
267/// assert_eq!(v.to_string(), "1.2.3-alpha.1+build.42");
268/// ```
269/// use semver::parse;
270///
271/// let a = parse("1.0.0+build1").unwrap();
272/// let b = parse("1.0.0+build2").unwrap();
273/// assert_eq!(a, b); // build metadata ignored
274///
275/// let a = parse("1.0.0-alpha").unwrap();
276/// let b = parse("1.0.0").unwrap();
277/// assert_ne!(a, b); // pre-release is significant
278/// assert!(a < b);   // pre-release < release
279/// ```
280#[derive(Debug, Clone)]
281pub struct Version<'a> {
282    pub major: u64,
283    pub minor: u64,
284    pub patch: u64,
285    pub pre_release: Option<PreRelease<'a>>,
286    pub build: Option<BuildMetadata<'a>>,
287}
288
289/// Parses a SemVer version string.
290///
291/// ```
292/// use semver::parse;
293///
294/// let v = parse("1.0.0").unwrap();
295/// assert_eq!(v.major, 1);
296/// assert_eq!(v.to_string(), "1.0.0");
297///
298/// let v = parse("1.0.0-alpha.1+build.123").unwrap();
299/// assert_eq!(v.to_string(), "1.0.0-alpha.1+build.123");
300///
301/// assert!(parse("01.2.3").is_err());
302/// assert!(parse("").is_err());
303/// ```
304pub fn parse(input: &str) -> Result<Version<'_>, ParseError> {
305    if input.is_empty() {
306        return Err(ParseError::EmptyInput);
307    }
308
309    let after_major = check_digits(input)?;
310    let (major_str, rest) = input.split_at(after_major);
311    let major = parse_number(major_str)?;
312    let rest = expect_dot(rest)?;
313
314    let after_minor = check_digits(rest)?;
315    let (minor_str, rest) = rest.split_at(after_minor);
316    let minor = parse_number(minor_str)?;
317    let rest = expect_dot(rest)?;
318
319    let after_patch = check_digits(rest)?;
320    let (patch_str, rest) = rest.split_at(after_patch);
321    let patch = parse_number(patch_str)?;
322
323    let (pre_release, after_pre_release) = match rest.strip_prefix('-') {
324        Some(r) => {
325            let end = r.find('+').unwrap_or(r.len());
326            let pre_str = &r[..end];
327            (Some(PreRelease::parse(pre_str)?), &r[end..])
328        }
329        None => (None, rest),
330    };
331
332    let build = match after_pre_release.strip_prefix('+') {
333        Some(r) => Some(BuildMetadata::parse(r)?),
334        None => {
335            if !after_pre_release.is_empty() {
336                return Err(ParseError::TrailingData);
337            }
338            None
339        }
340    };
341
342    Ok(Version {
343        major,
344        minor,
345        patch,
346        pre_release,
347        build,
348    })
349}
350
351fn check_digits(s: &str) -> Result<usize, ParseError> {
352    if s.is_empty() {
353        return Err(ParseError::InvalidCharacter);
354    }
355    match s.find(|c: char| !c.is_ascii_digit()) {
356        Some(0) => Err(ParseError::InvalidCharacter),
357        Some(pos) => Ok(pos),
358        None => Ok(s.len()),
359    }
360}
361
362fn parse_number(s: &str) -> Result<u64, ParseError> {
363    if s.len() > 1 && s.starts_with('0') {
364        return Err(ParseError::LeadingZero);
365    }
366    s.parse::<u64>().map_err(|_| ParseError::InvalidNumber)
367}
368
369fn expect_dot(s: &str) -> Result<&str, ParseError> {
370    match s.as_bytes().first() {
371        Some(b'.') => Ok(&s[1..]),
372        _ => Err(ParseError::InvalidFormat),
373    }
374}
375
376impl<'a> fmt::Display for Version<'a> {
377    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
378        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)?;
379        if let Some(ref pre_release) = self.pre_release {
380            write!(f, "-{pre_release}")?;
381        }
382        if let Some(ref build) = self.build {
383            write!(f, "+{build}")?;
384        }
385        Ok(())
386    }
387}
388
389impl<'a> PartialEq for Version<'a> {
390    fn eq(&self, other: &Self) -> bool {
391        self.major == other.major
392            && self.minor == other.minor
393            && self.patch == other.patch
394            && self.pre_release == other.pre_release
395    }
396}
397
398impl<'a> Eq for Version<'a> {}
399
400impl<'a> PartialOrd for Version<'a> {
401    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
402        Some(self.cmp(other))
403    }
404}
405
406impl<'a> Ord for Version<'a> {
407    fn cmp(&self, other: &Self) -> Ordering {
408        match self.major.cmp(&other.major) {
409            Ordering::Equal => {}
410            non_eq => return non_eq,
411        }
412        match self.minor.cmp(&other.minor) {
413            Ordering::Equal => {}
414            non_eq => return non_eq,
415        }
416        match self.patch.cmp(&other.patch) {
417            Ordering::Equal => {}
418            non_eq => return non_eq,
419        }
420        match (&self.pre_release, &other.pre_release) {
421            (None, Some(_)) => Ordering::Greater,
422            (Some(_), None) => Ordering::Less,
423            (None, None) => Ordering::Equal,
424            (Some(a), Some(b)) => a.cmp(b),
425        }
426    }
427}
428
429#[cfg(test)]
430mod tests {
431    use alloc::{string::ToString, vec};
432
433    use super::*;
434
435    #[test]
436    fn parse_basic() {
437        let v = parse("1.2.3").unwrap();
438        assert_eq!(v.major, 1);
439        assert_eq!(v.minor, 2);
440        assert_eq!(v.patch, 3);
441        assert!(v.pre_release.is_none());
442        assert!(v.build.is_none());
443    }
444
445    #[test]
446    fn parse_zero() {
447        let v = parse("0.0.0").unwrap();
448        assert_eq!(v.major, 0);
449        assert_eq!(v.minor, 0);
450        assert_eq!(v.patch, 0);
451    }
452
453    #[test]
454    fn parse_large_numbers() {
455        let v = parse("999999999999999999.0.0").unwrap();
456        assert_eq!(v.major, 999999999999999999);
457    }
458
459    #[test]
460    fn parse_prerelease() {
461        let v = parse("1.0.0-alpha").unwrap();
462        assert_eq!(v.pre_release.unwrap().identifiers(), &[Identifier::Alpha("alpha")]);
463    }
464
465    #[test]
466    fn parse_prerelease_dotted() {
467        let v = parse("1.0.0-alpha.1").unwrap();
468        assert_eq!(
469            v.pre_release.as_ref().unwrap().identifiers(),
470            &[Identifier::Alpha("alpha"), Identifier::Numeric(1)]
471        );
472    }
473
474    #[test]
475    fn parse_prerelease_numeric() {
476        let v = parse("1.0.0-1.2.3").unwrap();
477        assert_eq!(
478            v.pre_release.as_ref().unwrap().identifiers(),
479            &[Identifier::Numeric(1), Identifier::Numeric(2), Identifier::Numeric(3)]
480        );
481    }
482
483    #[test]
484    fn parse_build_metadata() {
485        let v = parse("1.0.0+001").unwrap();
486        assert_eq!(v.build.unwrap().as_str(), "001");
487    }
488
489    #[test]
490    fn parse_prerelease_and_build() {
491        let v = parse("1.0.0-alpha.1+build.123").unwrap();
492        assert!(v.pre_release.is_some());
493        assert_eq!(v.build.unwrap().as_str(), "build.123");
494    }
495
496    #[test]
497    fn parse_complex_prerelease() {
498        let v = parse("1.0.0-0.3.7").unwrap();
499        assert_eq!(
500            v.pre_release.as_ref().unwrap().identifiers(),
501            &[Identifier::Numeric(0), Identifier::Numeric(3), Identifier::Numeric(7)]
502        );
503    }
504
505    #[test]
506    fn parse_prerelease_with_hyphens() {
507        let v = parse("1.0.0-x-y-z.--").unwrap();
508        let idents = v.pre_release.unwrap().identifiers().to_vec();
509        assert_eq!(idents.len(), 2);
510        assert_eq!(idents[0], Identifier::Alpha("x-y-z"));
511        assert_eq!(idents[1], Identifier::Alpha("--"));
512    }
513
514    #[test]
515    fn parse_build_with_dots() {
516        let v = parse("1.0.0+21AF26D3----117B344092BD").unwrap();
517        assert_eq!(v.build.unwrap().as_str(), "21AF26D3----117B344092BD");
518    }
519
520    #[test]
521    fn parse_build_with_multiple_dots() {
522        let v = parse("1.0.0+20130313144700").unwrap();
523        assert_eq!(v.build.unwrap().as_str(), "20130313144700");
524    }
525
526    #[test]
527    fn parse_build_after_prerelease() {
528        let v = parse("1.0.0-beta+exp.sha.5114f85").unwrap();
529        assert_eq!(v.build.unwrap().as_str(), "exp.sha.5114f85");
530    }
531
532    #[test]
533    fn error_empty_input() {
534        assert_eq!(parse(""), Err(ParseError::EmptyInput));
535    }
536
537    #[test]
538    fn error_leading_zero_major() {
539        assert_eq!(parse("01.2.3"), Err(ParseError::LeadingZero));
540    }
541
542    #[test]
543    fn error_leading_zero_minor() {
544        assert_eq!(parse("1.02.3"), Err(ParseError::LeadingZero));
545    }
546
547    #[test]
548    fn error_leading_zero_patch() {
549        assert_eq!(parse("1.2.03"), Err(ParseError::LeadingZero));
550    }
551
552    #[test]
553    fn error_leading_zero_in_prerelease() {
554        assert_eq!(parse("1.0.0-01"), Err(ParseError::LeadingZero));
555    }
556
557    #[test]
558    fn error_invalid_char() {
559        assert!(parse("1.2.3!").is_err());
560    }
561
562    #[test]
563    fn error_invalid_char_in_prerelease() {
564        assert!(parse("1.0.0-alpha$").is_err());
565    }
566
567    #[test]
568    fn error_empty_prerelease() {
569        assert_eq!(parse("1.0.0-"), Err(ParseError::EmptyIdentifier));
570    }
571
572    #[test]
573    fn error_empty_build() {
574        assert_eq!(parse("1.0.0+"), Err(ParseError::EmptyIdentifier));
575    }
576
577    #[test]
578    fn error_missing_dot() {
579        assert_eq!(parse("1.2"), Err(ParseError::InvalidFormat));
580    }
581
582    #[test]
583    fn error_non_digit_major() {
584        assert_eq!(parse("a.2.3"), Err(ParseError::InvalidCharacter));
585    }
586
587    #[test]
588    fn display_basic() {
589        assert_eq!(parse("1.2.3").unwrap().to_string(), "1.2.3");
590    }
591
592    #[test]
593    fn display_with_prerelease() {
594        assert_eq!(parse("1.0.0-alpha.1").unwrap().to_string(), "1.0.0-alpha.1");
595    }
596
597    #[test]
598    fn display_with_build() {
599        assert_eq!(parse("1.0.0+build.42").unwrap().to_string(), "1.0.0+build.42");
600    }
601
602    #[test]
603    fn display_with_both() {
604        assert_eq!(parse("1.0.0-rc.2+sha.abc123").unwrap().to_string(), "1.0.0-rc.2+sha.abc123");
605    }
606
607    #[test]
608    fn eq_ignores_build_metadata() {
609        let a = parse("1.0.0+build1").unwrap();
610        let b = parse("1.0.0+build2").unwrap();
611        assert_eq!(a, b);
612    }
613
614    #[test]
615    fn eq_respects_prerelease() {
616        let a = parse("1.0.0-alpha").unwrap();
617        let b = parse("1.0.0-beta").unwrap();
618        assert_ne!(a, b);
619    }
620
621    #[test]
622    fn precedence_spec_examples() {
623        // 1.0.0-alpha < 2.0.0 < 2.1.0 < 2.1.1
624        let versions: Vec<Version> = vec!["1.0.0-alpha", "2.0.0", "2.1.0", "2.1.1"]
625            .into_iter()
626            .map(|s| parse(s).unwrap())
627            .collect();
628        for i in 0..versions.len() - 1 {
629            assert!(versions[i] < versions[i + 1], "{} < {}", versions[i], versions[i + 1]);
630        }
631    }
632
633    #[test]
634    fn precedence_prerelease_vs_release() {
635        assert!(parse("1.0.0-alpha").unwrap() < parse("1.0.0").unwrap());
636    }
637
638    #[test]
639    fn precedence_complex_prerelease() {
640        let spec_order = [
641            "1.0.0-alpha",
642            "1.0.0-alpha.1",
643            "1.0.0-alpha.beta",
644            "1.0.0-beta",
645            "1.0.0-beta.2",
646            "1.0.0-beta.11",
647            "1.0.0-rc.1",
648            "1.0.0",
649        ];
650        let versions: Vec<Version> = spec_order.iter().map(|s| parse(s).unwrap()).collect();
651        for i in 0..versions.len() - 1 {
652            assert!(
653                versions[i] < versions[i + 1],
654                "expected {} < {}",
655                spec_order[i],
656                spec_order[i + 1]
657            );
658        }
659    }
660
661    #[test]
662    fn precedence_build_ignored() {
663        assert_eq!(parse("1.0.0+1").unwrap().cmp(&parse("1.0.0+2").unwrap()), Ordering::Equal);
664    }
665
666    #[test]
667    fn precedence_numeric_before_alpha() {
668        let a = parse("1.0.0-1").unwrap();
669        let b = parse("1.0.0-alpha").unwrap();
670        assert!(a < b);
671    }
672
673    // ===== Error precision =====
674
675    #[test]
676    fn error_leading_non_digit_major() {
677        assert_eq!(parse("a.0.0"), Err(ParseError::InvalidCharacter));
678    }
679
680    #[test]
681    fn error_leading_non_digit_minor() {
682        assert_eq!(parse("1.a.0"), Err(ParseError::InvalidCharacter));
683    }
684
685    #[test]
686    fn error_leading_non_digit_patch() {
687        assert_eq!(parse("1.0.a"), Err(ParseError::InvalidCharacter));
688    }
689
690    // ===== Empty identifier edge cases =====
691
692    #[test]
693    fn error_empty_identifier_middle_prerelease() {
694        assert_eq!(parse("1.0.0-alpha..1"), Err(ParseError::EmptyIdentifier));
695    }
696
697    #[test]
698    fn error_empty_identifier_middle_build() {
699        assert_eq!(parse("1.0.0+a..b"), Err(ParseError::EmptyIdentifier));
700    }
701
702    #[test]
703    fn error_trailing_dot_build() {
704        assert_eq!(parse("1.0.0+build."), Err(ParseError::EmptyIdentifier));
705    }
706
707    #[test]
708    fn parse_prerelease_double_dash() {
709        // first `-` is the version/prerelease separator, second starts the identifier
710        let v = parse("1.0.0--alpha").unwrap();
711        assert_eq!(v.pre_release.as_ref().unwrap().identifiers(), &[Identifier::Alpha("-alpha")]);
712    }
713
714    // ===== Trailing / invalid characters =====
715
716    #[test]
717    fn error_trailing_dot_after_patch() {
718        assert_eq!(parse("1.2.3."), Err(ParseError::TrailingData));
719    }
720
721    #[test]
722    fn error_dot_only_prerelease() {
723        assert_eq!(parse("1.0.0-."), Err(ParseError::EmptyIdentifier));
724    }
725
726    #[test]
727    fn error_trailing_plus_with_prerelease() {
728        assert_eq!(parse("1.0.0-alpha+"), Err(ParseError::EmptyIdentifier));
729    }
730
731    #[test]
732    fn error_space_in_prerelease() {
733        assert_eq!(parse("1.0.0-alpha beta"), Err(ParseError::InvalidCharacter));
734    }
735
736    #[test]
737    fn error_unicode_in_prerelease() {
738        assert_eq!(parse("1.0.0-α"), Err(ParseError::InvalidCharacter));
739    }
740
741    #[test]
742    fn error_space_in_build() {
743        assert_eq!(parse("1.0.0+build extra"), Err(ParseError::InvalidCharacter));
744    }
745
746    #[test]
747    fn error_double_plus_build() {
748        assert_eq!(parse("1.0.0+build+extra"), Err(ParseError::InvalidCharacter));
749    }
750
751    #[test]
752    fn error_unicode_in_build() {
753        assert_eq!(parse("1.0.0+é"), Err(ParseError::InvalidCharacter));
754    }
755
756    #[test]
757    fn error_prerelease_numeric_overflow() {
758        assert_eq!(parse("1.0.0-18446744073709551616"), Err(ParseError::InvalidNumber));
759    }
760
761    // ===== Valid edge case parsing =====
762
763    #[test]
764    fn parse_prerelease_alpha_starting_with_zero() {
765        let v = parse("1.0.0-0abc").unwrap();
766        assert_eq!(v.pre_release.as_ref().unwrap().identifiers(), &[Identifier::Alpha("0abc")]);
767    }
768
769    #[test]
770    fn parse_prerelease_hyphens_only() {
771        let v = parse("1.0.0-----").unwrap();
772        assert_eq!(v.pre_release.as_ref().unwrap().identifiers(), &[Identifier::Alpha("----")]);
773    }
774
775    #[test]
776    fn parse_prerelease_single_zero() {
777        let v = parse("1.0.0-0").unwrap();
778        assert_eq!(v.pre_release.as_ref().unwrap().identifiers(), &[Identifier::Numeric(0)]);
779    }
780
781    #[test]
782    fn parse_prerelease_with_plus_split() {
783        let v = parse("1.0.0-alpha+beta").unwrap();
784        assert_eq!(v.pre_release.as_ref().unwrap().identifiers(), &[Identifier::Alpha("alpha")]);
785        assert_eq!(v.build.unwrap().as_str(), "beta");
786    }
787
788    #[test]
789    fn parse_u64_max_major() {
790        let v = parse("18446744073709551615.0.0").unwrap();
791        assert_eq!(v.major, u64::MAX);
792    }
793
794    #[test]
795    fn parse_build_with_hyphen() {
796        let v = parse("1.0.0+build-id").unwrap();
797        assert_eq!(v.build.unwrap().as_str(), "build-id");
798    }
799
800    // ===== Precedence / ordering =====
801
802    #[test]
803    fn precedence_prerelease_length_tiebreak() {
804        assert!(parse("1.0.0-1").unwrap() < parse("1.0.0-1.0").unwrap());
805    }
806
807    #[test]
808    fn precedence_prerelease_length_tiebreak_alpha() {
809        assert!(parse("1.0.0-alpha").unwrap() < parse("1.0.0-alpha.0").unwrap());
810    }
811
812    #[test]
813    fn precedence_prerelease_zero_length() {
814        assert!(parse("1.0.0-0").unwrap() < parse("1.0.0-0.0").unwrap());
815    }
816
817    #[test]
818    fn precedence_alpha_case_ascii() {
819        assert!(parse("1.0.0-A").unwrap() < parse("1.0.0-a").unwrap());
820    }
821
822    #[test]
823    fn precedence_hyphen_in_alpha() {
824        assert!(parse("1.0.0--a").unwrap() < parse("1.0.0-a").unwrap());
825    }
826
827    // ===== Display / round-trip =====
828
829    #[test]
830    fn display_roundtrip() {
831        let inputs = [
832            "1.2.3",
833            "0.0.0",
834            "1.0.0-alpha",
835            "1.0.0-alpha.1",
836            "1.0.0-0.3.7",
837            "1.0.0-x-y-z.--",
838            "1.0.0+001",
839            "1.0.0+21AF26D3----117B344092BD",
840            "1.0.0+20130313144700",
841            "1.0.0-beta+exp.sha.5114f85",
842            "1.0.0-rc.2+sha.abc123",
843            "1.0.0----",
844            "1.0.0-0abc",
845            "1.0.0+build-id",
846        ];
847        for input in &inputs {
848            let v = parse(input).unwrap();
849            assert_eq!(v.to_string(), *input, "round-trip failed for: {input}");
850        }
851    }
852
853    #[test]
854    fn display_direct_construction() {
855        let v = Version {
856            major: 2,
857            minor: 5,
858            patch: 1,
859            pre_release: Some(PreRelease {
860                identifiers: vec![Identifier::Alpha("rc"), Identifier::Numeric(3)],
861            }),
862            build: Some(BuildMetadata {
863                raw: "sha.abc",
864            }),
865        };
866        assert_eq!(v.to_string(), "2.5.1-rc.3+sha.abc");
867    }
868
869    // ===== Direct construction equality/ordering =====
870
871    #[test]
872    fn direct_version_eq() {
873        let a = Version {
874            major: 1,
875            minor: 2,
876            patch: 3,
877            pre_release: None,
878            build: None,
879        };
880        let b = Version {
881            major: 1,
882            minor: 2,
883            patch: 3,
884            pre_release: None,
885            build: Some(BuildMetadata {
886                raw: "x",
887            }),
888        };
889        assert_eq!(a, b);
890    }
891
892    #[test]
893    fn direct_version_ord() {
894        let a = Version {
895            major: 1,
896            minor: 0,
897            patch: 0,
898            pre_release: None,
899            build: None,
900        };
901        let b = Version {
902            major: 1,
903            minor: 0,
904            patch: 0,
905            pre_release: Some(PreRelease {
906                identifiers: vec![Identifier::Alpha("alpha")],
907            }),
908            build: None,
909        };
910        assert!(b < a);
911    }
912
913    // ===== Identifier ordering unit tests =====
914
915    #[test]
916    fn identifier_cmp_numeric_vs_alpha() {
917        assert!(Identifier::Numeric(1) < Identifier::Alpha("a"));
918        assert!(Identifier::Alpha("a") > Identifier::Numeric(1));
919    }
920
921    #[test]
922    fn identifier_cmp_numeric_values() {
923        assert!(Identifier::Numeric(1) < Identifier::Numeric(2));
924        assert!(Identifier::Numeric(5) == Identifier::Numeric(5));
925    }
926
927    #[test]
928    fn identifier_cmp_alpha_values() {
929        assert!(Identifier::Alpha("alpha") < Identifier::Alpha("beta"));
930        assert!(Identifier::Alpha("a") < Identifier::Alpha("b"));
931        assert!(Identifier::Alpha("--") < Identifier::Alpha("a"));
932    }
933
934    // ===== PreRelease ordering unit tests =====
935
936    #[test]
937    fn prerelease_cmp_length() {
938        let a = PreRelease {
939            identifiers: vec![Identifier::Numeric(1)],
940        };
941        let b = PreRelease {
942            identifiers: vec![Identifier::Numeric(1), Identifier::Numeric(0)],
943        };
944        assert!(a < b);
945
946        let c = PreRelease {
947            identifiers: vec![Identifier::Alpha("alpha")],
948        };
949        let d = PreRelease {
950            identifiers: vec![Identifier::Alpha("alpha"), Identifier::Numeric(1)],
951        };
952        assert!(c < d);
953    }
954
955    // ===== Serde tests =====
956
957    #[cfg(feature = "serde")]
958    #[test]
959    fn serde_roundtrip() {
960        use serde_json;
961        let v = parse("1.2.3-alpha+build").unwrap();
962        let json = serde_json::to_string(&v).unwrap();
963        let v2: Version = serde_json::from_str(&json).unwrap();
964        assert_eq!(v, v2);
965    }
966
967    #[cfg(feature = "serde")]
968    #[test]
969    fn serde_deserialize_invalid_errors() {
970        use serde_json;
971        let result: Result<Version, _> = serde_json::from_str("\"01.2.3\"");
972        assert!(result.is_err());
973    }
974}