Skip to main content

semver/
semver.rs

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