Skip to main content

semver/
semver.rs

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