From 96ca4b3d9ba76dd0212aa530ae7d72e4a63177f4 Mon Sep 17 00:00:00 2001 From: Wisaroot Lertthaweedech Date: Wed, 13 Aug 2025 23:11:31 +0700 Subject: [PATCH] feat: add comprehensive SemVer implementation with full test coverage --- src/version/mod.rs | 2 + src/version/semver/core.rs | 316 +++++++++++++++++ src/version/semver/display.rs | 365 ++++++++++++++++++++ src/version/semver/mod.rs | 6 + src/version/semver/ordering.rs | 461 +++++++++++++++++++++++++ src/version/semver/parser.rs | 598 +++++++++++++++++++++++++++++++++ 6 files changed, 1748 insertions(+) create mode 100644 src/version/semver/core.rs create mode 100644 src/version/semver/display.rs create mode 100644 src/version/semver/mod.rs create mode 100644 src/version/semver/ordering.rs create mode 100644 src/version/semver/parser.rs diff --git a/src/version/mod.rs b/src/version/mod.rs index 346fca3..27df236 100644 --- a/src/version/mod.rs +++ b/src/version/mod.rs @@ -1,3 +1,5 @@ pub mod pep440; +pub mod semver; pub use pep440::{PEP440Version, PreReleaseLabel}; +pub use semver::{BuildMetadata, PreReleaseIdentifier, SemVerVersion}; diff --git a/src/version/semver/core.rs b/src/version/semver/core.rs new file mode 100644 index 0000000..b920db2 --- /dev/null +++ b/src/version/semver/core.rs @@ -0,0 +1,316 @@ +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PreReleaseIdentifier { + String(String), + Integer(u64), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum BuildMetadata { + String(String), + Integer(u64), +} + +#[derive(Debug, Clone)] +pub struct SemVerVersion { + pub major: u64, + pub minor: u64, + pub patch: u64, + pub pre_release: Option>, + pub build_metadata: Option>, +} + +impl SemVerVersion { + pub fn new(major: u64, minor: u64, patch: u64) -> Self { + Self { + major, + minor, + patch, + pre_release: None, + build_metadata: None, + } + } + + pub fn with_pre_release(mut self, pre_release: Vec) -> Self { + self.pre_release = Some(pre_release); + self + } + + pub fn with_build_metadata(mut self, build_metadata: Vec) -> Self { + self.build_metadata = Some(build_metadata); + self + } + + pub fn is_pre_release(&self) -> bool { + self.pre_release.is_some() + } + + pub fn is_stable(&self) -> bool { + !self.is_pre_release() + } +} + +impl Default for SemVerVersion { + fn default() -> Self { + Self::new(0, 0, 0) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rstest::rstest; + + mod construction { + use super::*; + + #[test] + fn test_new() { + let version = SemVerVersion::new(1, 2, 3); + assert_eq!(version.major, 1); + assert_eq!(version.minor, 2); + assert_eq!(version.patch, 3); + assert!(version.pre_release.is_none()); + assert!(version.build_metadata.is_none()); + } + + #[test] + fn test_default() { + let version = SemVerVersion::default(); + assert_eq!(version.major, 0); + assert_eq!(version.minor, 0); + assert_eq!(version.patch, 0); + assert!(version.pre_release.is_none()); + assert!(version.build_metadata.is_none()); + } + + #[test] + fn test_with_pre_release() { + let pre_release = vec![ + PreReleaseIdentifier::String("alpha".to_string()), + PreReleaseIdentifier::Integer(1), + ]; + let version = SemVerVersion::new(1, 0, 0).with_pre_release(pre_release.clone()); + assert_eq!(version.pre_release, Some(pre_release)); + } + + #[test] + fn test_with_build_metadata() { + let build_metadata = vec![ + BuildMetadata::String("build".to_string()), + BuildMetadata::Integer(123), + ]; + let version = SemVerVersion::new(1, 0, 0).with_build_metadata(build_metadata.clone()); + assert_eq!(version.build_metadata, Some(build_metadata)); + } + + #[test] + fn test_method_chaining() { + let pre_release = vec![PreReleaseIdentifier::String("alpha".to_string())]; + let build_metadata = vec![BuildMetadata::String("build".to_string())]; + + let version = SemVerVersion::new(1, 2, 3) + .with_pre_release(pre_release.clone()) + .with_build_metadata(build_metadata.clone()); + + assert_eq!(version.major, 1); + assert_eq!(version.minor, 2); + assert_eq!(version.patch, 3); + assert_eq!(version.pre_release, Some(pre_release)); + assert_eq!(version.build_metadata, Some(build_metadata)); + } + } + + mod properties { + use super::*; + + #[test] + fn test_is_stable() { + let stable = SemVerVersion::new(1, 0, 0); + assert!(stable.is_stable()); + assert!(!stable.is_pre_release()); + } + + #[test] + fn test_is_pre_release() { + let pre_release = SemVerVersion::new(1, 0, 0) + .with_pre_release(vec![PreReleaseIdentifier::String("alpha".to_string())]); + assert!(pre_release.is_pre_release()); + assert!(!pre_release.is_stable()); + } + + #[test] + fn test_build_metadata_does_not_affect_stability() { + let version = SemVerVersion::new(1, 0, 0) + .with_build_metadata(vec![BuildMetadata::String("build".to_string())]); + assert!(version.is_stable()); + assert!(!version.is_pre_release()); + } + } + + mod edge_cases { + use super::*; + + #[test] + fn test_zero_version() { + let version = SemVerVersion::new(0, 0, 0); + assert_eq!(version.major, 0); + assert_eq!(version.minor, 0); + assert_eq!(version.patch, 0); + } + + #[test] + fn test_max_values() { + let version = SemVerVersion::new(u64::MAX, u64::MAX, u64::MAX); + assert_eq!(version.major, u64::MAX); + assert_eq!(version.minor, u64::MAX); + assert_eq!(version.patch, u64::MAX); + } + + #[test] + fn test_empty_pre_release() { + let version = SemVerVersion::new(1, 0, 0).with_pre_release(vec![]); + assert_eq!(version.pre_release, Some(vec![])); + assert!(version.is_pre_release()); + } + + #[test] + fn test_empty_build_metadata() { + let version = SemVerVersion::new(1, 0, 0).with_build_metadata(vec![]); + assert_eq!(version.build_metadata, Some(vec![])); + } + + #[test] + fn test_overwrite_pre_release() { + let first = vec![PreReleaseIdentifier::String("alpha".to_string())]; + let second = vec![PreReleaseIdentifier::String("beta".to_string())]; + + let version = SemVerVersion::new(1, 0, 0) + .with_pre_release(first) + .with_pre_release(second.clone()); + + assert_eq!(version.pre_release, Some(second)); + } + + #[test] + fn test_overwrite_build_metadata() { + let first = vec![BuildMetadata::String("build1".to_string())]; + let second = vec![BuildMetadata::String("build2".to_string())]; + + let version = SemVerVersion::new(1, 0, 0) + .with_build_metadata(first) + .with_build_metadata(second.clone()); + + assert_eq!(version.build_metadata, Some(second)); + } + } + + mod identifiers { + use super::*; + + #[rstest] + #[case("alpha")] + #[case("beta")] + #[case("rc")] + #[case("x")] + #[case("")] + fn test_pre_release_string_identifier(#[case] value: &str) { + let identifier = PreReleaseIdentifier::String(value.to_string()); + match identifier { + PreReleaseIdentifier::String(s) => assert_eq!(s, value), + _ => panic!("Expected string identifier"), + } + } + + #[rstest] + #[case(0)] + #[case(1)] + #[case(123)] + #[case(u64::MAX)] + fn test_pre_release_integer_identifier(#[case] value: u64) { + let identifier = PreReleaseIdentifier::Integer(value); + match identifier { + PreReleaseIdentifier::Integer(n) => assert_eq!(n, value), + _ => panic!("Expected integer identifier"), + } + } + + #[rstest] + #[case("build")] + #[case("commit")] + #[case("sha")] + #[case("")] + fn test_build_metadata_string(#[case] value: &str) { + let metadata = BuildMetadata::String(value.to_string()); + match metadata { + BuildMetadata::String(s) => assert_eq!(s, value), + _ => panic!("Expected string metadata"), + } + } + + #[rstest] + #[case(0)] + #[case(1)] + #[case(20240101)] + #[case(u64::MAX)] + fn test_build_metadata_integer(#[case] value: u64) { + let metadata = BuildMetadata::Integer(value); + match metadata { + BuildMetadata::Integer(n) => assert_eq!(n, value), + _ => panic!("Expected integer metadata"), + } + } + } + + mod complex_versions { + use super::*; + + #[test] + fn test_complex_pre_release() { + let pre_release = vec![ + PreReleaseIdentifier::String("alpha".to_string()), + PreReleaseIdentifier::Integer(1), + PreReleaseIdentifier::String("build".to_string()), + PreReleaseIdentifier::Integer(456), + ]; + + let version = SemVerVersion::new(2, 0, 0).with_pre_release(pre_release.clone()); + assert_eq!(version.pre_release, Some(pre_release)); + } + + #[test] + fn test_complex_build_metadata() { + let build_metadata = vec![ + BuildMetadata::String("commit".to_string()), + BuildMetadata::String("abc123".to_string()), + BuildMetadata::Integer(20240101), + ]; + + let version = SemVerVersion::new(1, 5, 0).with_build_metadata(build_metadata.clone()); + assert_eq!(version.build_metadata, Some(build_metadata)); + } + + #[test] + fn test_full_version() { + let pre_release = vec![ + PreReleaseIdentifier::String("rc".to_string()), + PreReleaseIdentifier::Integer(2), + ]; + let build_metadata = vec![ + BuildMetadata::String("build".to_string()), + BuildMetadata::Integer(789), + ]; + + let version = SemVerVersion::new(3, 1, 4) + .with_pre_release(pre_release.clone()) + .with_build_metadata(build_metadata.clone()); + + assert_eq!(version.major, 3); + assert_eq!(version.minor, 1); + assert_eq!(version.patch, 4); + assert_eq!(version.pre_release, Some(pre_release)); + assert_eq!(version.build_metadata, Some(build_metadata)); + assert!(version.is_pre_release()); + assert!(!version.is_stable()); + } + } +} diff --git a/src/version/semver/display.rs b/src/version/semver/display.rs new file mode 100644 index 0000000..33f5cef --- /dev/null +++ b/src/version/semver/display.rs @@ -0,0 +1,365 @@ +use super::core::{BuildMetadata, PreReleaseIdentifier, SemVerVersion}; +use std::fmt; + +impl fmt::Display for SemVerVersion { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}.{}.{}", self.major, self.minor, self.patch)?; + + if let Some(ref pre_release) = self.pre_release + && !pre_release.is_empty() + { + write!(f, "-{}", format_identifiers(pre_release))?; + } + + if let Some(ref build_metadata) = self.build_metadata + && !build_metadata.is_empty() + { + write!(f, "+{}", format_build_metadata(build_metadata))?; + } + + Ok(()) + } +} + +impl fmt::Display for PreReleaseIdentifier { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + PreReleaseIdentifier::String(s) => write!(f, "{s}"), + PreReleaseIdentifier::Integer(n) => write!(f, "{n}"), + } + } +} + +impl fmt::Display for BuildMetadata { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + BuildMetadata::String(s) => write!(f, "{s}"), + BuildMetadata::Integer(n) => write!(f, "{n}"), + } + } +} + +fn format_identifiers(identifiers: &[PreReleaseIdentifier]) -> String { + identifiers + .iter() + .map(|id| id.to_string()) + .collect::>() + .join(".") +} + +fn format_build_metadata(metadata: &[BuildMetadata]) -> String { + metadata + .iter() + .map(|meta| meta.to_string()) + .collect::>() + .join(".") +} + +#[cfg(test)] +mod tests { + use super::*; + use rstest::rstest; + + mod basic_display { + use super::*; + + #[test] + fn test_simple_version() { + let version = SemVerVersion::new(1, 2, 3); + assert_eq!(version.to_string(), "1.2.3"); + } + + #[test] + fn test_zero_version() { + let version = SemVerVersion::new(0, 0, 0); + assert_eq!(version.to_string(), "0.0.0"); + } + + #[test] + fn test_large_numbers() { + let version = SemVerVersion::new(999, 888, 777); + assert_eq!(version.to_string(), "999.888.777"); + } + + #[test] + fn test_max_values() { + let version = SemVerVersion::new(u64::MAX, u64::MAX, u64::MAX); + let expected = format!("{}.{}.{}", u64::MAX, u64::MAX, u64::MAX); + assert_eq!(version.to_string(), expected); + } + } + + mod pre_release_display { + use super::*; + + #[test] + fn test_single_string_pre_release() { + let version = SemVerVersion::new(1, 0, 0) + .with_pre_release(vec![PreReleaseIdentifier::String("alpha".to_string())]); + assert_eq!(version.to_string(), "1.0.0-alpha"); + } + + #[test] + fn test_single_integer_pre_release() { + let version = SemVerVersion::new(1, 0, 0) + .with_pre_release(vec![PreReleaseIdentifier::Integer(1)]); + assert_eq!(version.to_string(), "1.0.0-1"); + } + + #[test] + fn test_mixed_pre_release() { + let version = SemVerVersion::new(1, 0, 0).with_pre_release(vec![ + PreReleaseIdentifier::String("alpha".to_string()), + PreReleaseIdentifier::Integer(1), + ]); + assert_eq!(version.to_string(), "1.0.0-alpha.1"); + } + + #[test] + fn test_complex_pre_release() { + let version = SemVerVersion::new(2, 1, 0).with_pre_release(vec![ + PreReleaseIdentifier::String("rc".to_string()), + PreReleaseIdentifier::Integer(2), + PreReleaseIdentifier::String("build".to_string()), + PreReleaseIdentifier::Integer(456), + ]); + assert_eq!(version.to_string(), "2.1.0-rc.2.build.456"); + } + + #[test] + fn test_empty_pre_release() { + let version = SemVerVersion::new(1, 0, 0).with_pre_release(vec![]); + assert_eq!(version.to_string(), "1.0.0"); + } + + #[rstest] + #[case("alpha")] + #[case("beta")] + #[case("rc")] + #[case("x")] + #[case("")] + #[case("0")] + #[case("123abc")] + fn test_various_string_pre_release(#[case] pre_release: &str) { + let version = SemVerVersion::new(1, 0, 0) + .with_pre_release(vec![PreReleaseIdentifier::String(pre_release.to_string())]); + assert_eq!(version.to_string(), format!("1.0.0-{pre_release}")); + } + + #[rstest] + #[case(0)] + #[case(1)] + #[case(999)] + #[case(u64::MAX)] + fn test_various_integer_pre_release(#[case] pre_release: u64) { + let version = SemVerVersion::new(1, 0, 0) + .with_pre_release(vec![PreReleaseIdentifier::Integer(pre_release)]); + assert_eq!(version.to_string(), format!("1.0.0-{pre_release}")); + } + } + + mod build_metadata_display { + use super::*; + + #[test] + fn test_single_string_build_metadata() { + let version = SemVerVersion::new(1, 0, 0) + .with_build_metadata(vec![BuildMetadata::String("build".to_string())]); + assert_eq!(version.to_string(), "1.0.0+build"); + } + + #[test] + fn test_single_integer_build_metadata() { + let version = + SemVerVersion::new(1, 0, 0).with_build_metadata(vec![BuildMetadata::Integer(123)]); + assert_eq!(version.to_string(), "1.0.0+123"); + } + + #[test] + fn test_mixed_build_metadata() { + let version = SemVerVersion::new(1, 0, 0).with_build_metadata(vec![ + BuildMetadata::String("commit".to_string()), + BuildMetadata::String("abc123".to_string()), + ]); + assert_eq!(version.to_string(), "1.0.0+commit.abc123"); + } + + #[test] + fn test_complex_build_metadata() { + let version = SemVerVersion::new(1, 5, 2).with_build_metadata(vec![ + BuildMetadata::String("build".to_string()), + BuildMetadata::Integer(789), + BuildMetadata::String("sha".to_string()), + BuildMetadata::String("def456".to_string()), + ]); + assert_eq!(version.to_string(), "1.5.2+build.789.sha.def456"); + } + + #[test] + fn test_empty_build_metadata() { + let version = SemVerVersion::new(1, 0, 0).with_build_metadata(vec![]); + assert_eq!(version.to_string(), "1.0.0"); + } + + #[rstest] + #[case("build")] + #[case("commit")] + #[case("sha")] + #[case("")] + #[case("abc123")] + #[case("20240101")] + fn test_various_string_build_metadata(#[case] metadata: &str) { + let version = SemVerVersion::new(1, 0, 0) + .with_build_metadata(vec![BuildMetadata::String(metadata.to_string())]); + assert_eq!(version.to_string(), format!("1.0.0+{metadata}")); + } + + #[rstest] + #[case(0)] + #[case(1)] + #[case(20240101)] + #[case(u64::MAX)] + fn test_various_integer_build_metadata(#[case] metadata: u64) { + let version = SemVerVersion::new(1, 0, 0) + .with_build_metadata(vec![BuildMetadata::Integer(metadata)]); + assert_eq!(version.to_string(), format!("1.0.0+{metadata}")); + } + } + + mod combined_display { + use super::*; + + #[test] + fn test_pre_release_and_build_metadata() { + let version = SemVerVersion::new(1, 2, 3) + .with_pre_release(vec![ + PreReleaseIdentifier::String("alpha".to_string()), + PreReleaseIdentifier::Integer(1), + ]) + .with_build_metadata(vec![ + BuildMetadata::String("build".to_string()), + BuildMetadata::Integer(456), + ]); + assert_eq!(version.to_string(), "1.2.3-alpha.1+build.456"); + } + + #[test] + fn test_complex_full_version() { + let version = SemVerVersion::new(10, 20, 30) + .with_pre_release(vec![ + PreReleaseIdentifier::String("rc".to_string()), + PreReleaseIdentifier::Integer(2), + PreReleaseIdentifier::String("hotfix".to_string()), + ]) + .with_build_metadata(vec![ + BuildMetadata::String("commit".to_string()), + BuildMetadata::String("abc123def".to_string()), + BuildMetadata::Integer(20240315), + ]); + assert_eq!( + version.to_string(), + "10.20.30-rc.2.hotfix+commit.abc123def.20240315" + ); + } + + #[test] + fn test_empty_pre_release_with_build_metadata() { + let version = SemVerVersion::new(1, 0, 0) + .with_pre_release(vec![]) + .with_build_metadata(vec![BuildMetadata::String("build".to_string())]); + assert_eq!(version.to_string(), "1.0.0+build"); + } + + #[test] + fn test_pre_release_with_empty_build_metadata() { + let version = SemVerVersion::new(1, 0, 0) + .with_pre_release(vec![PreReleaseIdentifier::String("alpha".to_string())]) + .with_build_metadata(vec![]); + assert_eq!(version.to_string(), "1.0.0-alpha"); + } + + #[test] + fn test_both_empty() { + let version = SemVerVersion::new(1, 0, 0) + .with_pre_release(vec![]) + .with_build_metadata(vec![]); + assert_eq!(version.to_string(), "1.0.0"); + } + } + + mod identifier_display { + use super::*; + + #[test] + fn test_pre_release_identifier_string() { + let identifier = PreReleaseIdentifier::String("alpha".to_string()); + assert_eq!(identifier.to_string(), "alpha"); + } + + #[test] + fn test_pre_release_identifier_integer() { + let identifier = PreReleaseIdentifier::Integer(123); + assert_eq!(identifier.to_string(), "123"); + } + + #[test] + fn test_build_metadata_string() { + let metadata = BuildMetadata::String("build".to_string()); + assert_eq!(metadata.to_string(), "build"); + } + + #[test] + fn test_build_metadata_integer() { + let metadata = BuildMetadata::Integer(456); + assert_eq!(metadata.to_string(), "456"); + } + } + + mod helper_functions { + use super::*; + + #[test] + fn test_format_identifiers_empty() { + let identifiers = vec![]; + assert_eq!(format_identifiers(&identifiers), ""); + } + + #[test] + fn test_format_identifiers_single() { + let identifiers = vec![PreReleaseIdentifier::String("alpha".to_string())]; + assert_eq!(format_identifiers(&identifiers), "alpha"); + } + + #[test] + fn test_format_identifiers_multiple() { + let identifiers = vec![ + PreReleaseIdentifier::String("alpha".to_string()), + PreReleaseIdentifier::Integer(1), + PreReleaseIdentifier::String("build".to_string()), + ]; + assert_eq!(format_identifiers(&identifiers), "alpha.1.build"); + } + + #[test] + fn test_format_build_metadata_empty() { + let metadata = vec![]; + assert_eq!(format_build_metadata(&metadata), ""); + } + + #[test] + fn test_format_build_metadata_single() { + let metadata = vec![BuildMetadata::String("build".to_string())]; + assert_eq!(format_build_metadata(&metadata), "build"); + } + + #[test] + fn test_format_build_metadata_multiple() { + let metadata = vec![ + BuildMetadata::String("commit".to_string()), + BuildMetadata::String("abc123".to_string()), + BuildMetadata::Integer(789), + ]; + assert_eq!(format_build_metadata(&metadata), "commit.abc123.789"); + } + } +} diff --git a/src/version/semver/mod.rs b/src/version/semver/mod.rs new file mode 100644 index 0000000..34d8139 --- /dev/null +++ b/src/version/semver/mod.rs @@ -0,0 +1,6 @@ +pub mod core; +mod display; +mod ordering; +mod parser; + +pub use core::{BuildMetadata, PreReleaseIdentifier, SemVerVersion}; diff --git a/src/version/semver/ordering.rs b/src/version/semver/ordering.rs new file mode 100644 index 0000000..bebdd64 --- /dev/null +++ b/src/version/semver/ordering.rs @@ -0,0 +1,461 @@ +use super::core::{PreReleaseIdentifier, SemVerVersion}; +use std::cmp::Ordering; + +impl PartialOrd for SemVerVersion { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for SemVerVersion { + fn cmp(&self, other: &Self) -> Ordering { + // Compare major.minor.patch first + self.major + .cmp(&other.major) + .then_with(|| self.minor.cmp(&other.minor)) + .then_with(|| self.patch.cmp(&other.patch)) + .then_with(|| { + // Pre-release versions have lower precedence than normal versions + match (&self.pre_release, &other.pre_release) { + (None, None) => Ordering::Equal, + (None, Some(_)) => Ordering::Greater, // stable > pre-release + (Some(_), None) => Ordering::Less, // pre-release < stable + (Some(self_pre), Some(other_pre)) => { + compare_pre_release_identifiers(self_pre, other_pre) + } + } + }) + // Build metadata MUST be ignored when determining version precedence + } +} + +impl PartialEq for SemVerVersion { + fn eq(&self, other: &Self) -> bool { + self.cmp(other) == Ordering::Equal + } +} + +impl Eq for SemVerVersion {} + +impl PartialOrd for PreReleaseIdentifier { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for PreReleaseIdentifier { + fn cmp(&self, other: &Self) -> Ordering { + match (self, other) { + (PreReleaseIdentifier::Integer(a), PreReleaseIdentifier::Integer(b)) => a.cmp(b), + (PreReleaseIdentifier::String(a), PreReleaseIdentifier::String(b)) => a.cmp(b), + (PreReleaseIdentifier::Integer(_), PreReleaseIdentifier::String(_)) => Ordering::Less, + (PreReleaseIdentifier::String(_), PreReleaseIdentifier::Integer(_)) => { + Ordering::Greater + } + } + } +} + +fn compare_pre_release_identifiers( + left: &[PreReleaseIdentifier], + right: &[PreReleaseIdentifier], +) -> Ordering { + let min_len = left.len().min(right.len()); + + for i in 0..min_len { + match left[i].cmp(&right[i]) { + Ordering::Equal => continue, + other => return other, + } + } + + // If all compared identifiers are equal, the version with fewer identifiers has lower precedence + left.len().cmp(&right.len()) +} + +#[cfg(test)] +mod tests { + use super::super::core::BuildMetadata; + use super::*; + use rstest::rstest; + + mod basic_ordering { + use super::*; + + #[test] + fn test_major_version_ordering() { + let v1 = SemVerVersion::new(1, 0, 0); + let v2 = SemVerVersion::new(2, 0, 0); + assert!(v1 < v2); + assert!(v2 > v1); + } + + #[test] + fn test_minor_version_ordering() { + let v1 = SemVerVersion::new(1, 0, 0); + let v2 = SemVerVersion::new(1, 1, 0); + assert!(v1 < v2); + assert!(v2 > v1); + } + + #[test] + fn test_patch_version_ordering() { + let v1 = SemVerVersion::new(1, 0, 0); + let v2 = SemVerVersion::new(1, 0, 1); + assert!(v1 < v2); + assert!(v2 > v1); + } + + #[test] + fn test_equal_versions() { + let v1 = SemVerVersion::new(1, 2, 3); + let v2 = SemVerVersion::new(1, 2, 3); + assert_eq!(v1, v2); + assert!(v1 <= v2); + assert!(v1 >= v2); + } + + #[test] + fn test_reflexivity() { + let version = SemVerVersion::new(1, 2, 3); + assert_eq!(version.cmp(&version), Ordering::Equal); + } + } + + mod pre_release_ordering { + use super::*; + + #[test] + fn test_stable_vs_pre_release() { + let stable = SemVerVersion::new(1, 0, 0); + let pre_release = SemVerVersion::new(1, 0, 0) + .with_pre_release(vec![PreReleaseIdentifier::String("alpha".to_string())]); + + assert!(pre_release < stable); + assert!(stable > pre_release); + } + + #[test] + fn test_pre_release_string_ordering() { + let alpha = SemVerVersion::new(1, 0, 0) + .with_pre_release(vec![PreReleaseIdentifier::String("alpha".to_string())]); + let beta = SemVerVersion::new(1, 0, 0) + .with_pre_release(vec![PreReleaseIdentifier::String("beta".to_string())]); + + assert!(alpha < beta); + assert!(beta > alpha); + } + + #[test] + fn test_pre_release_integer_ordering() { + let v1 = SemVerVersion::new(1, 0, 0) + .with_pre_release(vec![PreReleaseIdentifier::Integer(1)]); + let v2 = SemVerVersion::new(1, 0, 0) + .with_pre_release(vec![PreReleaseIdentifier::Integer(2)]); + + assert!(v1 < v2); + assert!(v2 > v1); + } + + #[test] + fn test_pre_release_mixed_type_ordering() { + let integer = SemVerVersion::new(1, 0, 0) + .with_pre_release(vec![PreReleaseIdentifier::Integer(1)]); + let string = SemVerVersion::new(1, 0, 0) + .with_pre_release(vec![PreReleaseIdentifier::String("alpha".to_string())]); + + assert!(integer < string); // integers < strings + assert!(string > integer); + } + + #[test] + fn test_pre_release_length_ordering() { + let shorter = SemVerVersion::new(1, 0, 0) + .with_pre_release(vec![PreReleaseIdentifier::String("alpha".to_string())]); + let longer = SemVerVersion::new(1, 0, 0).with_pre_release(vec![ + PreReleaseIdentifier::String("alpha".to_string()), + PreReleaseIdentifier::Integer(1), + ]); + + assert!(shorter < longer); // fewer identifiers < more identifiers + assert!(longer > shorter); + } + + #[test] + fn test_complex_pre_release_ordering() { + let v1 = SemVerVersion::new(1, 0, 0).with_pre_release(vec![ + PreReleaseIdentifier::String("alpha".to_string()), + PreReleaseIdentifier::Integer(1), + ]); + let v2 = SemVerVersion::new(1, 0, 0).with_pre_release(vec![ + PreReleaseIdentifier::String("alpha".to_string()), + PreReleaseIdentifier::Integer(2), + ]); + + assert!(v1 < v2); + assert!(v2 > v1); + } + } + + mod build_metadata_ignored { + use super::*; + + #[test] + fn test_build_metadata_ignored_in_comparison() { + let v1 = SemVerVersion::new(1, 0, 0) + .with_build_metadata(vec![BuildMetadata::String("build1".to_string())]); + let v2 = SemVerVersion::new(1, 0, 0) + .with_build_metadata(vec![BuildMetadata::String("build2".to_string())]); + + assert_eq!(v1, v2); // build metadata is ignored + } + + #[test] + fn test_build_metadata_with_pre_release() { + let v1 = SemVerVersion::new(1, 0, 0) + .with_pre_release(vec![PreReleaseIdentifier::String("alpha".to_string())]) + .with_build_metadata(vec![BuildMetadata::String("build1".to_string())]); + let v2 = SemVerVersion::new(1, 0, 0) + .with_pre_release(vec![PreReleaseIdentifier::String("alpha".to_string())]) + .with_build_metadata(vec![BuildMetadata::String("build2".to_string())]); + + assert_eq!(v1, v2); // build metadata is ignored + } + + #[test] + fn test_no_build_metadata_vs_with_build_metadata() { + let v1 = SemVerVersion::new(1, 0, 0); + let v2 = SemVerVersion::new(1, 0, 0) + .with_build_metadata(vec![BuildMetadata::String("build".to_string())]); + + assert_eq!(v1, v2); // build metadata is ignored + } + } + + mod identifier_ordering { + use super::*; + + #[test] + fn test_integer_identifier_ordering() { + let id1 = PreReleaseIdentifier::Integer(1); + let id2 = PreReleaseIdentifier::Integer(2); + assert!(id1 < id2); + assert!(id2 > id1); + } + + #[test] + fn test_string_identifier_ordering() { + let id1 = PreReleaseIdentifier::String("alpha".to_string()); + let id2 = PreReleaseIdentifier::String("beta".to_string()); + assert!(id1 < id2); + assert!(id2 > id1); + } + + #[test] + fn test_mixed_identifier_ordering() { + let integer = PreReleaseIdentifier::Integer(999); + let string = PreReleaseIdentifier::String("a".to_string()); + assert!(integer < string); // integers always < strings + assert!(string > integer); + } + + #[test] + fn test_identifier_equality() { + let id1 = PreReleaseIdentifier::String("alpha".to_string()); + let id2 = PreReleaseIdentifier::String("alpha".to_string()); + assert_eq!(id1, id2); + + let id3 = PreReleaseIdentifier::Integer(42); + let id4 = PreReleaseIdentifier::Integer(42); + assert_eq!(id3, id4); + } + } + + mod semver_version_equality { + use super::*; + + #[rstest] + #[case("1.0.0", "1.0.0")] + #[case("1.2.3", "1.2.3")] + #[case("0.0.0", "0.0.0")] + #[case("1.0.0-alpha", "1.0.0-alpha")] + #[case("1.0.0-alpha.1", "1.0.0-alpha.1")] + #[case("1.0.0-alpha.beta", "1.0.0-alpha.beta")] + #[case("1.0.0+build", "1.0.0+build")] + #[case("1.0.0+build.1", "1.0.0+build.1")] + #[case("1.0.0-alpha+build", "1.0.0-alpha+build")] + #[case("1.0.0-alpha.1+build.1", "1.0.0-alpha.1+build.1")] + #[case("1.0.0+build1", "1.0.0+build2")] // build metadata ignored + #[case("1.0.0-alpha+build1", "1.0.0-alpha+build2")] // build metadata ignored + #[case("1.0.0-alpha+build1", "1.0.0-alpha+Build2")] // build metadata ignored + #[case("1.0.0-alpha+build1", "1.0.0-alpha+BUILD2")] // build metadata ignored + fn test_semver_version_equality(#[case] left: &str, #[case] right: &str) { + let left_version: SemVerVersion = left.parse().unwrap(); + let right_version: SemVerVersion = right.parse().unwrap(); + assert_eq!(left_version, right_version); + } + + // #[rstest] + // #[case("1.0.0-alpha", "1.0.0-ALPHA")] + // #[case("1.0.0-Alpha", "1.0.0-alpha")] + // fn test_case_sensitivity_inequality(#[case] left: &str, #[case] right: &str) { + // let left_version: SemVerVersion = left.parse().unwrap(); + // let right_version: SemVerVersion = right.parse().unwrap(); + // assert_ne!(left_version, right_version); // case sensitive + // } + } + + mod comprehensive_ordering { + use super::*; + + #[rstest] + #[case("1.0.0", "2.0.0")] + #[case("2.0.0", "2.1.0")] + #[case("2.1.0", "2.1.1")] + #[case("1.0.0-alpha", "1.0.0")] + #[case("1.0.0-alpha", "1.0.0-alpha.1")] + #[case("1.0.0-alpha.1", "1.0.0-alpha.beta")] + #[case("1.0.0-alpha.beta", "1.0.0-beta")] + #[case("1.0.0-beta", "1.0.0-beta.2")] + #[case("1.0.0-beta.2", "1.0.0-beta.11")] + #[case("1.0.0-beta.11", "1.0.0-rc.1")] + #[case("1.0.0-rc.1", "1.0.0")] + #[case("1.0.0-ALPHA", "1.0.0-alpha")] + #[case("1.0.0-Alpha", "1.0.0-alpha")] + fn test_semver_spec_examples(#[case] left: &str, #[case] right: &str) { + let left_version: SemVerVersion = left.parse().unwrap(); + let right_version: SemVerVersion = right.parse().unwrap(); + assert!(left_version < right_version, "{left} should be < {right}"); + assert!(left_version <= right_version, "{left} should be <= {right}"); + assert!(right_version > left_version, "{left} should be > {right}"); + assert!(right_version >= left_version, "{left} should be >= {right}"); + } + + #[test] + fn test_transitivity() { + let v1 = SemVerVersion::new(1, 0, 0) + .with_pre_release(vec![PreReleaseIdentifier::String("alpha".to_string())]); + let v2 = SemVerVersion::new(1, 0, 0) + .with_pre_release(vec![PreReleaseIdentifier::String("beta".to_string())]); + let v3 = SemVerVersion::new(1, 0, 0); + + assert!(v1 < v2); + assert!(v2 < v3); + assert!(v1 < v3); // transitivity + } + + #[test] + fn test_antisymmetry() { + let v1 = SemVerVersion::new(1, 0, 0); + let v2 = SemVerVersion::new(2, 0, 0); + + assert!(v1 < v2); + assert!(v2 >= v1); + } + } + + mod edge_cases { + use super::*; + + #[test] + fn test_zero_versions() { + let v1 = SemVerVersion::new(0, 0, 0); + let v2 = SemVerVersion::new(0, 0, 1); + assert!(v1 < v2); + } + + #[test] + fn test_max_values() { + let v1 = SemVerVersion::new(u64::MAX - 1, u64::MAX, u64::MAX); + let v2 = SemVerVersion::new(u64::MAX, 0, 0); + assert!(v1 < v2); + } + + #[test] + fn test_empty_pre_release() { + let v1 = SemVerVersion::new(1, 0, 0).with_pre_release(vec![]); + let v2 = SemVerVersion::new(1, 0, 0); + assert!(v1 < v2); // empty pre-release still makes it a pre-release + } + + #[test] + fn test_very_long_pre_release() { + let long_pre_release = (0..100).map(PreReleaseIdentifier::Integer).collect(); + let short_pre_release = vec![PreReleaseIdentifier::Integer(0)]; + + let v1 = SemVerVersion::new(1, 0, 0).with_pre_release(short_pre_release); + let v2 = SemVerVersion::new(1, 0, 0).with_pre_release(long_pre_release); + + assert!(v1 < v2); // fewer identifiers < more identifiers + } + + #[test] + fn test_numeric_string_comparison() { + let v1 = SemVerVersion::new(1, 0, 0) + .with_pre_release(vec![PreReleaseIdentifier::String("10".to_string())]); + let v2 = SemVerVersion::new(1, 0, 0) + .with_pre_release(vec![PreReleaseIdentifier::String("2".to_string())]); + + // String comparison: "10" < "2" lexicographically + assert!(v1 < v2); + } + + #[test] + fn test_integer_vs_numeric_string() { + let integer = SemVerVersion::new(1, 0, 0) + .with_pre_release(vec![PreReleaseIdentifier::Integer(10)]); + let string = SemVerVersion::new(1, 0, 0) + .with_pre_release(vec![PreReleaseIdentifier::String("2".to_string())]); + + // Integer < String regardless of numeric value + assert!(integer < string); + } + } + + mod helper_function_tests { + use super::*; + + #[test] + fn test_compare_pre_release_identifiers_empty() { + let left = vec![]; + let right = vec![]; + assert_eq!( + compare_pre_release_identifiers(&left, &right), + Ordering::Equal + ); + } + + #[test] + fn test_compare_pre_release_identifiers_different_lengths() { + let left = vec![PreReleaseIdentifier::String("alpha".to_string())]; + let right = vec![ + PreReleaseIdentifier::String("alpha".to_string()), + PreReleaseIdentifier::Integer(1), + ]; + assert_eq!( + compare_pre_release_identifiers(&left, &right), + Ordering::Less + ); + assert_eq!( + compare_pre_release_identifiers(&right, &left), + Ordering::Greater + ); + } + + #[test] + fn test_compare_pre_release_identifiers_same_prefix() { + let left = vec![ + PreReleaseIdentifier::String("alpha".to_string()), + PreReleaseIdentifier::Integer(1), + ]; + let right = vec![ + PreReleaseIdentifier::String("alpha".to_string()), + PreReleaseIdentifier::Integer(2), + ]; + assert_eq!( + compare_pre_release_identifiers(&left, &right), + Ordering::Less + ); + } + } +} diff --git a/src/version/semver/parser.rs b/src/version/semver/parser.rs new file mode 100644 index 0000000..115650e --- /dev/null +++ b/src/version/semver/parser.rs @@ -0,0 +1,598 @@ +use crate::error::ZervError; +use crate::version::semver::core::{BuildMetadata, PreReleaseIdentifier, SemVerVersion}; +use regex::Regex; +use std::str::FromStr; +use std::sync::LazyLock; + +static SEMVER_REGEX: LazyLock = LazyLock::new(|| { + Regex::new( + r"(?x) + ^v?(?P0|[1-9]\d*) # major version + \.(?P0|[1-9]\d*) # minor version + \.(?P0|[1-9]\d*) # patch version + (?: # optional prerelease + -(?P + (?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*) # prerelease identifier + (?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))* # additional identifiers + ) + )? + (?: # optional build metadata + \+(?P + [0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)* # build metadata + ) + )?$ + ", + ) + .unwrap() +}); + +fn parse_identifiers(input: &str) -> Vec { + if input.is_empty() { + return vec![PreReleaseIdentifier::String("".to_string())]; + } + input + .split('.') + .map(|part| { + if part.chars().all(|c| c.is_ascii_digit()) && (part == "0" || !part.starts_with('0')) { + PreReleaseIdentifier::Integer(part.parse().unwrap_or(0)) + } else { + PreReleaseIdentifier::String(part.to_string()) + } + }) + .collect() +} + +fn parse_build_metadata(input: &str) -> Vec { + if input.is_empty() { + return vec![BuildMetadata::String("".to_string())]; + } + input + .split('.') + .map(|part| { + if part.chars().all(|c| c.is_ascii_digit()) && (part == "0" || !part.starts_with('0')) { + BuildMetadata::Integer(part.parse().unwrap_or(0)) + } else { + BuildMetadata::String(part.to_string()) + } + }) + .collect() +} + +impl FromStr for SemVerVersion { + type Err = ZervError; + + fn from_str(s: &str) -> Result { + let captures = SEMVER_REGEX + .captures(s) + .ok_or_else(|| ZervError::InvalidVersion(format!("Invalid SemVer version: {s}")))?; + + let major = captures + .name("major") + .unwrap() + .as_str() + .parse() + .map_err(|_| ZervError::InvalidVersion("Invalid major version".to_string()))?; + + let minor = captures + .name("minor") + .unwrap() + .as_str() + .parse() + .map_err(|_| ZervError::InvalidVersion("Invalid minor version".to_string()))?; + + let patch = captures + .name("patch") + .unwrap() + .as_str() + .parse() + .map_err(|_| ZervError::InvalidVersion("Invalid patch version".to_string()))?; + + let mut version = SemVerVersion::new(major, minor, patch); + + if let Some(pre_release_match) = captures.name("prerelease") { + let pre_release = parse_identifiers(pre_release_match.as_str()); + version = version.with_pre_release(pre_release); + } + + if let Some(build_metadata_match) = captures.name("buildmetadata") { + let build_metadata = parse_build_metadata(build_metadata_match.as_str()); + version = version.with_build_metadata(build_metadata); + } + + Ok(version) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rstest::rstest; + + mod basic_parsing { + use super::*; + + #[rstest] + #[case("0.0.0", 0, 0, 0)] + #[case("1.0.0", 1, 0, 0)] + #[case("1.2.3", 1, 2, 3)] + #[case("10.20.30", 10, 20, 30)] + #[case("999.888.777", 999, 888, 777)] + fn test_parse_basic_versions( + #[case] input: &str, + #[case] major: u64, + #[case] minor: u64, + #[case] patch: u64, + ) { + let parsed: SemVerVersion = input.parse().unwrap(); + assert_eq!(parsed.major, major); + assert_eq!(parsed.minor, minor); + assert_eq!(parsed.patch, patch); + assert!(parsed.pre_release.is_none()); + assert!(parsed.build_metadata.is_none()); + } + + #[test] + fn test_parse_max_values() { + let input = format!("{}.{}.{}", u64::MAX, u64::MAX, u64::MAX); + let parsed: SemVerVersion = input.parse().unwrap(); + assert_eq!(parsed.major, u64::MAX); + assert_eq!(parsed.minor, u64::MAX); + assert_eq!(parsed.patch, u64::MAX); + } + } + + mod pre_release_parsing { + use super::*; + + #[test] + fn test_parse_single_string_pre_release() { + let parsed: SemVerVersion = "1.0.0-alpha".parse().unwrap(); + assert_eq!( + parsed.pre_release, + Some(vec![PreReleaseIdentifier::String("alpha".to_string())]) + ); + } + + #[test] + fn test_parse_single_integer_pre_release() { + let parsed: SemVerVersion = "1.0.0-1".parse().unwrap(); + assert_eq!( + parsed.pre_release, + Some(vec![PreReleaseIdentifier::Integer(1)]) + ); + } + + #[test] + fn test_parse_mixed_pre_release() { + let parsed: SemVerVersion = "1.0.0-alpha.1".parse().unwrap(); + assert_eq!( + parsed.pre_release, + Some(vec![ + PreReleaseIdentifier::String("alpha".to_string()), + PreReleaseIdentifier::Integer(1), + ]) + ); + } + + #[test] + fn test_parse_complex_pre_release() { + let parsed: SemVerVersion = "1.0.0-alpha.1.beta.2".parse().unwrap(); + assert_eq!( + parsed.pre_release, + Some(vec![ + PreReleaseIdentifier::String("alpha".to_string()), + PreReleaseIdentifier::Integer(1), + PreReleaseIdentifier::String("beta".to_string()), + PreReleaseIdentifier::Integer(2), + ]) + ); + } + + #[rstest] + #[case("1.0.0-alpha")] + #[case("1.0.0-beta")] + #[case("1.0.0-rc")] + #[case("1.0.0-x")] + #[case("1.0.0-a")] + #[case("1.0.0-alpha-beta")] + #[case("1.0.0-alpha123")] + #[case("1.0.0-123alpha")] + fn test_parse_various_string_pre_release(#[case] input: &str) { + let parsed: SemVerVersion = input.parse().unwrap(); + assert!(parsed.pre_release.is_some()); + assert!(parsed.is_pre_release()); + } + + #[rstest] + #[case("1.0.0-0")] + #[case("1.0.0-1")] + #[case("1.0.0-999")] + fn test_parse_various_integer_pre_release(#[case] input: &str) { + let parsed: SemVerVersion = input.parse().unwrap(); + assert!(parsed.pre_release.is_some()); + assert!(parsed.is_pre_release()); + } + + #[test] + fn test_parse_leading_zero_pre_release() { + // Leading zeros are invalid in SemVer according to the spec + let result: Result = "1.0.0-01".parse(); + assert!(result.is_err(), "Leading zeros should be invalid in SemVer"); + } + + #[test] + fn test_parse_zero_pre_release() { + // "0" is valid as integer + let parsed: SemVerVersion = "1.0.0-0".parse().unwrap(); + assert_eq!( + parsed.pre_release, + Some(vec![PreReleaseIdentifier::Integer(0)]) + ); + } + } + + mod build_metadata_parsing { + use super::*; + + #[test] + fn test_parse_single_string_build_metadata() { + let parsed: SemVerVersion = "1.0.0+build".parse().unwrap(); + assert_eq!( + parsed.build_metadata, + Some(vec![BuildMetadata::String("build".to_string())]) + ); + } + + #[test] + fn test_parse_single_integer_build_metadata() { + let parsed: SemVerVersion = "1.0.0+123".parse().unwrap(); + assert_eq!( + parsed.build_metadata, + Some(vec![BuildMetadata::Integer(123)]) + ); + } + + #[test] + fn test_parse_mixed_build_metadata() { + let parsed: SemVerVersion = "1.0.0+build.123".parse().unwrap(); + assert_eq!( + parsed.build_metadata, + Some(vec![ + BuildMetadata::String("build".to_string()), + BuildMetadata::Integer(123), + ]) + ); + } + + #[test] + fn test_parse_complex_build_metadata() { + let parsed: SemVerVersion = "1.0.0+commit.abc123.20240101".parse().unwrap(); + assert_eq!( + parsed.build_metadata, + Some(vec![ + BuildMetadata::String("commit".to_string()), + BuildMetadata::String("abc123".to_string()), + BuildMetadata::Integer(20240101), + ]) + ); + } + + #[test] + fn test_parse_leading_zero_build_metadata() { + // Leading zeros are treated as strings in build metadata + let parsed: SemVerVersion = "1.0.0+01".parse().unwrap(); + assert_eq!( + parsed.build_metadata, + Some(vec![BuildMetadata::String("01".to_string())]) + ); + } + + #[test] + fn test_parse_zero_build_metadata() { + // "0" is valid as integer + let parsed: SemVerVersion = "1.0.0+0".parse().unwrap(); + assert_eq!(parsed.build_metadata, Some(vec![BuildMetadata::Integer(0)])); + } + } + + mod combined_parsing { + use super::*; + + #[test] + fn test_parse_pre_release_and_build_metadata() { + let parsed: SemVerVersion = "1.2.3-alpha.1+build.456".parse().unwrap(); + + assert_eq!(parsed.major, 1); + assert_eq!(parsed.minor, 2); + assert_eq!(parsed.patch, 3); + assert_eq!( + parsed.pre_release, + Some(vec![ + PreReleaseIdentifier::String("alpha".to_string()), + PreReleaseIdentifier::Integer(1), + ]) + ); + assert_eq!( + parsed.build_metadata, + Some(vec![ + BuildMetadata::String("build".to_string()), + BuildMetadata::Integer(456), + ]) + ); + } + + #[test] + fn test_parse_complex_full_version() { + let parsed: SemVerVersion = "10.20.30-rc.2.hotfix+commit.abc123def.20240315" + .parse() + .unwrap(); + + assert_eq!(parsed.major, 10); + assert_eq!(parsed.minor, 20); + assert_eq!(parsed.patch, 30); + assert_eq!( + parsed.pre_release, + Some(vec![ + PreReleaseIdentifier::String("rc".to_string()), + PreReleaseIdentifier::Integer(2), + PreReleaseIdentifier::String("hotfix".to_string()), + ]) + ); + assert_eq!( + parsed.build_metadata, + Some(vec![ + BuildMetadata::String("commit".to_string()), + BuildMetadata::String("abc123def".to_string()), + BuildMetadata::Integer(20240315), + ]) + ); + } + } + + mod invalid_parsing { + use super::*; + + #[rstest] + // Invalid basic format + #[case("")] + #[case("1")] + #[case("1.2")] + #[case("1.2.3.4")] + #[case("1.2.3-")] + #[case("1.2.3+")] + #[case("1.2.3-+")] + // Leading zeros in version numbers + #[case("01.2.3")] + #[case("1.02.3")] + #[case("1.2.03")] + // Invalid characters + #[case("1.2.a")] + #[case("a.2.3")] + #[case("1.b.3")] + // Invalid pre-release + #[case("1.2.3-")] + #[case("1.2.3-.")] + #[case("1.2.3-..")] + #[case("1.2.3-.alpha")] + #[case("1.2.3-alpha.")] + // Invalid build metadata + #[case("1.2.3+")] + #[case("1.2.3+.")] + #[case("1.2.3+..")] + #[case("1.2.3+.build")] + #[case("1.2.3+build.")] + // Negative numbers + #[case("-1.2.3")] + #[case("1.-2.3")] + #[case("1.2.-3")] + // Spaces + #[case("1 .2.3")] + #[case("1. 2.3")] + #[case("1.2 .3")] + #[case("1.2. 3")] + #[case("1.2.3 -alpha")] + #[case("1.2.3- alpha")] + #[case("1.2.3 +build")] + #[case("1.2.3+ build")] + // Invalid separators + #[case("1,2,3")] + #[case("1:2:3")] + #[case("1;2;3")] + fn test_parse_invalid_versions(#[case] input: &str) { + let result: Result = input.parse(); + assert!( + result.is_err(), + "Expected '{input}' to be invalid but it parsed successfully" + ); + } + } + + mod helper_functions { + use super::*; + + #[test] + fn test_parse_identifiers_empty() { + let identifiers = parse_identifiers(""); + assert_eq!( + identifiers, + vec![PreReleaseIdentifier::String("".to_string())] + ); + } + + #[test] + fn test_parse_identifiers_single_string() { + let identifiers = parse_identifiers("alpha"); + assert_eq!( + identifiers, + vec![PreReleaseIdentifier::String("alpha".to_string())] + ); + } + + #[test] + fn test_parse_identifiers_single_integer() { + let identifiers = parse_identifiers("123"); + assert_eq!(identifiers, vec![PreReleaseIdentifier::Integer(123)]); + } + + #[test] + fn test_parse_identifiers_zero() { + let identifiers = parse_identifiers("0"); + assert_eq!(identifiers, vec![PreReleaseIdentifier::Integer(0)]); + } + + #[test] + fn test_parse_identifiers_leading_zero() { + let identifiers = parse_identifiers("01"); + assert_eq!( + identifiers, + vec![PreReleaseIdentifier::String("01".to_string())] + ); + } + + #[test] + fn test_parse_identifiers_mixed() { + let identifiers = parse_identifiers("alpha.1.beta.2"); + assert_eq!( + identifiers, + vec![ + PreReleaseIdentifier::String("alpha".to_string()), + PreReleaseIdentifier::Integer(1), + PreReleaseIdentifier::String("beta".to_string()), + PreReleaseIdentifier::Integer(2), + ] + ); + } + + #[test] + fn test_parse_build_metadata_empty() { + let metadata = parse_build_metadata(""); + assert_eq!(metadata, vec![BuildMetadata::String("".to_string())]); + } + + #[test] + fn test_parse_build_metadata_single_string() { + let metadata = parse_build_metadata("build"); + assert_eq!(metadata, vec![BuildMetadata::String("build".to_string())]); + } + + #[test] + fn test_parse_build_metadata_single_integer() { + let metadata = parse_build_metadata("123"); + assert_eq!(metadata, vec![BuildMetadata::Integer(123)]); + } + + #[test] + fn test_parse_build_metadata_zero() { + let metadata = parse_build_metadata("0"); + assert_eq!(metadata, vec![BuildMetadata::Integer(0)]); + } + + #[test] + fn test_parse_build_metadata_leading_zero() { + let metadata = parse_build_metadata("01"); + assert_eq!(metadata, vec![BuildMetadata::String("01".to_string())]); + } + + #[test] + fn test_parse_build_metadata_mixed() { + let metadata = parse_build_metadata("commit.abc123.20240101"); + assert_eq!( + metadata, + vec![ + BuildMetadata::String("commit".to_string()), + BuildMetadata::String("abc123".to_string()), + BuildMetadata::Integer(20240101), + ] + ); + } + } + + mod roundtrip_tests { + use super::*; + + #[rstest] + // Basic versions + #[case("0.0.0")] + #[case("1.0.0")] + #[case("1.2.3")] + #[case("10.20.30")] + // Pre-release versions + #[case("1.0.0-alpha")] + #[case("1.0.0-1")] + #[case("1.0.0-alpha.1")] + #[case("1.0.0-alpha.1.beta.2")] + #[case("1.0.0-0")] + // Build metadata versions + #[case("1.0.0+build")] + #[case("1.0.0+123")] + #[case("1.0.0+build.123")] + #[case("1.0.0+commit.abc123.20240101")] + #[case("1.0.0+0")] + // Combined versions + #[case("1.2.3-alpha.1+build.456")] + #[case("10.20.30-rc.2.hotfix+commit.abc123def.20240315")] + #[case("1.0.0-alpha+beta")] + #[case("1.0.0-0+0")] + fn test_roundtrip_parsing(#[case] input: &str) { + let parsed: SemVerVersion = input.parse().unwrap(); + let output = parsed.to_string(); + assert_eq!(input, output, "Roundtrip failed for: {input}"); + + // Also verify the reparsed version is equal + let reparsed: SemVerVersion = output.parse().unwrap(); + assert_eq!(parsed, reparsed, "Reparsed version should be equal"); + } + } + + mod edge_cases { + use super::*; + + #[test] + fn test_parse_very_large_numbers() { + let input = format!("{}.{}.{}", u64::MAX, u64::MAX, u64::MAX); + let parsed: SemVerVersion = input.parse().unwrap(); + assert_eq!(parsed.major, u64::MAX); + assert_eq!(parsed.minor, u64::MAX); + assert_eq!(parsed.patch, u64::MAX); + } + + #[test] + fn test_parse_long_pre_release() { + let long_pre_release = (0..100) + .map(|i| format!("part{i}")) + .collect::>() + .join("."); + let input = format!("1.0.0-{long_pre_release}"); + let parsed: SemVerVersion = input.parse().unwrap(); + assert!(parsed.pre_release.is_some()); + assert_eq!(parsed.pre_release.as_ref().unwrap().len(), 100); + } + + #[test] + fn test_parse_long_build_metadata() { + let long_build_metadata = (0..100) + .map(|i| format!("meta{i}")) + .collect::>() + .join("."); + let input = format!("1.0.0+{long_build_metadata}"); + let parsed: SemVerVersion = input.parse().unwrap(); + assert!(parsed.build_metadata.is_some()); + assert_eq!(parsed.build_metadata.as_ref().unwrap().len(), 100); + } + + #[test] + fn test_parse_alphanumeric_identifiers() { + let parsed: SemVerVersion = "1.0.0-alpha123beta456+build789meta012".parse().unwrap(); + assert_eq!( + parsed.pre_release, + Some(vec![PreReleaseIdentifier::String( + "alpha123beta456".to_string() + )]) + ); + assert_eq!( + parsed.build_metadata, + Some(vec![BuildMetadata::String("build789meta012".to_string())]) + ); + } + } +}