scirs2_core/versioning/
semantic.rs

1//! # Semantic Versioning Implementation
2//!
3//! Comprehensive semantic versioning implementation with `SciRS2`-specific
4//! extensions for scientific computing environments. Provides `SemVer` 2.0.0
5//! compliance with additional features for research and enterprise use.
6
7use crate::error::CoreError;
8use std::cmp::Ordering;
9use std::fmt;
10use std::str::FromStr;
11
12#[cfg(feature = "serialization")]
13use serde::{Deserialize, Serialize};
14
15/// Semantic version representation following `SemVer` 2.0.0
16#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
17#[derive(Debug, Clone, PartialEq, Eq, Hash)]
18pub struct Version {
19    /// Major version (breaking changes)
20    major: u64,
21    /// Minor version (backward compatible features)
22    minor: u64,
23    /// Patch version (backward compatible bug fixes)
24    patch: u64,
25    /// Pre-release identifier (alpha, beta, rc, etc.)
26    prerelease: Option<String>,
27    /// Build metadata
28    buildmetadata: Option<String>,
29}
30
31impl Version {
32    /// Create a new version
33    pub fn new(major: u64, minor: u64, patch: u64) -> Self {
34        Self {
35            major,
36            minor,
37            patch,
38            prerelease: None,
39            buildmetadata: None,
40        }
41    }
42
43    /// Parse a simple version string (major.minor.patch format only)
44    pub fn parse_simple(versionstr: &str) -> Result<Self, String> {
45        let parts: Vec<&str> = versionstr.split('.').collect();
46        if parts.len() != 3 {
47            return Err(format!("Invalid version format: {versionstr}"));
48        }
49
50        Ok(Self {
51            major: parts[0]
52                .parse()
53                .map_err(|e| format!("Invalid major version: {e}"))?,
54            minor: parts[1]
55                .parse()
56                .map_err(|e| format!("Invalid minor version: {e}"))?,
57            patch: parts[2]
58                .parse()
59                .map_err(|e| format!("Invalid patch version: {e}"))?,
60            prerelease: None,
61            buildmetadata: None,
62        })
63    }
64
65    /// Create a version with pre-release
66    pub fn release(major: u64, minor: u64, patch: u64, prerelease: String) -> Self {
67        Self {
68            major,
69            minor,
70            patch,
71            prerelease: Some(prerelease),
72            buildmetadata: None,
73        }
74    }
75
76    /// Parse a version string (supports pre-release and build metadata)
77    pub fn parse(versionstr: &str) -> Result<Self, CoreError> {
78        let version = versionstr.trim();
79
80        // Remove 'v' prefix if present
81        let version = if version.starts_with('v') || version.starts_with('V') {
82            &version[1..]
83        } else {
84            version
85        };
86
87        // Split on '+' to separate build metadata
88        let (version_part, buildmetadata) = if let Some(plus_pos) = version.find('+') {
89            (
90                &version[..plus_pos],
91                Some(version[plus_pos + 1..].to_string()),
92            )
93        } else {
94            (version, None)
95        };
96
97        // Split on '-' to separate pre-release
98        let (core_version, prerelease) = if let Some(dash_pos) = version_part.find('-') {
99            (
100                &version_part[..dash_pos],
101                Some(version_part[dash_pos + 1..].to_string()),
102            )
103        } else {
104            (version_part, None)
105        };
106
107        // Parse major.minor.patch
108        let parts: Vec<&str> = core_version.split('.').collect();
109        if parts.len() != 3 {
110            return Err(CoreError::ComputationError(
111                crate::error::ErrorContext::new(format!("Invalid version format: {version}")),
112            ));
113        }
114
115        let major = parts[0].parse::<u64>().map_err(|_| {
116            CoreError::ComputationError(crate::error::ErrorContext::new(format!(
117                "Invalid major version: {}",
118                parts[0]
119            )))
120        })?;
121        let minor = parts[1].parse::<u64>().map_err(|_| {
122            CoreError::ComputationError(crate::error::ErrorContext::new(format!(
123                "Invalid minor version: {}",
124                parts[1]
125            )))
126        })?;
127        let patch = parts[2].parse::<u64>().map_err(|_| {
128            CoreError::ComputationError(crate::error::ErrorContext::new(format!(
129                "Invalid patch version: {}",
130                parts[2]
131            )))
132        })?;
133
134        Ok(Self {
135            major,
136            minor,
137            patch,
138            prerelease,
139            buildmetadata,
140        })
141    }
142
143    /// Get major version
144    pub fn major(&self) -> u64 {
145        self.major
146    }
147
148    /// Get minor version
149    pub fn minor(&self) -> u64 {
150        self.minor
151    }
152
153    /// Get patch version
154    pub fn patch(&self) -> u64 {
155        self.patch
156    }
157
158    /// Get pre-release identifier
159    pub fn prerelease(&self) -> Option<&str> {
160        self.prerelease.as_deref()
161    }
162
163    /// Get build metadata
164    pub fn buildmetadata(&self) -> Option<&str> {
165        self.buildmetadata.as_deref()
166    }
167
168    /// Check if this is a pre-release version
169    pub fn is_prerelease(&self) -> bool {
170        self.prerelease.is_some()
171    }
172
173    /// Check if this is a stable release
174    pub fn is_stable(&self) -> bool {
175        !self.is_prerelease()
176    }
177
178    /// Increment major version (resets minor and patch to 0)
179    pub fn increment_major(&mut self) {
180        self.major += 1;
181        self.minor = 0;
182        self.patch = 0;
183        self.prerelease = None;
184        self.buildmetadata = None;
185    }
186
187    /// Increment minor version (resets patch to 0)
188    pub fn increment_minor(&mut self) {
189        self.minor += 1;
190        self.patch = 0;
191        self.prerelease = None;
192        self.buildmetadata = None;
193    }
194
195    /// Increment patch version
196    pub fn increment_patch(&mut self) {
197        self.patch += 1;
198        self.prerelease = None;
199        self.buildmetadata = None;
200    }
201
202    /// Set pre-release identifier
203    pub fn release_2(&mut self, prerelease: Option<String>) {
204        self.prerelease = prerelease;
205    }
206
207    /// Set build metadata
208    pub fn metadata(&mut self, buildmetadata: Option<String>) {
209        self.buildmetadata = buildmetadata;
210    }
211
212    /// Check if this version is compatible with another version
213    pub fn is_compatible_with(&self, other: &Self) -> bool {
214        // Same major version means compatible (assuming proper `SemVer`)
215        self.major == other.major && self.major > 0
216    }
217
218    /// Check if this version has breaking changes compared to another
219    pub fn has_breakingchanges_from(&self, other: &Self) -> bool {
220        self.major > other.major
221    }
222
223    /// Get the core version without pre-release or build metadata
224    pub fn core_version(&self) -> Self {
225        Self::new(self.major, self.minor, self.patch)
226    }
227}
228
229impl fmt::Display for Version {
230    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
231        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)?;
232
233        if let Some(ref prerelease) = self.prerelease {
234            write!(f, "-{prerelease}")?;
235        }
236
237        if let Some(ref buildmetadata) = self.buildmetadata {
238            write!(f, "+{buildmetadata}")?;
239        }
240
241        Ok(())
242    }
243}
244
245impl FromStr for Version {
246    type Err = CoreError;
247
248    fn from_str(s: &str) -> Result<Self, Self::Err> {
249        Self::parse(s)
250    }
251}
252
253impl PartialOrd for Version {
254    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
255        Some(self.cmp(other))
256    }
257}
258
259impl Ord for Version {
260    fn cmp(&self, other: &Self) -> Ordering {
261        // Compare major, minor, patch
262        match self.major.cmp(&other.major) {
263            Ordering::Equal => {}
264            other => return other,
265        }
266
267        match self.minor.cmp(&other.minor) {
268            Ordering::Equal => {}
269            other => return other,
270        }
271
272        match self.patch.cmp(&other.patch) {
273            Ordering::Equal => {}
274            other => return other,
275        }
276
277        // Compare pre-release
278        match (&self.prerelease, &other.prerelease) {
279            (None, None) => Ordering::Equal,
280            (Some(_), None) => Ordering::Less, // Pre-release < release
281            (None, Some(_)) => Ordering::Greater, // Release > pre-release
282            (Some(a), Some(b)) => compare_prerelease(a, b),
283        }
284    }
285}
286
287/// Compare pre-release versions according to `SemVer` rules
288#[allow(dead_code)]
289fn compare_prerelease(a: &str, b: &str) -> Ordering {
290    let a_parts: Vec<&str> = a.split('.').collect();
291    let b_parts: Vec<&str> = b.split('.').collect();
292
293    for (a_part, b_part) in a_parts.iter().zip(b_parts.iter()) {
294        // Try to parse as numbers first
295        let a_num = a_part.parse::<u64>();
296        let b_num = b_part.parse::<u64>();
297
298        match (a_num, b_num) {
299            (Ok(a_n), Ok(b_n)) => match a_n.cmp(&b_n) {
300                Ordering::Equal => {}
301                other => return other,
302            },
303            (Ok(_), Err(_)) => return Ordering::Less, // Numeric < alphanumeric
304            (Err(_), Ok(_)) => return Ordering::Greater, // Alphanumeric > numeric
305            (Err(_), Err(_)) => match a_part.cmp(b_part) {
306                Ordering::Equal => {}
307                other => return other,
308            },
309        }
310    }
311
312    // If all compared parts are equal, the longer one is greater
313    a_parts.len().cmp(&b_parts.len())
314}
315
316/// Version constraint for specifying version requirements
317#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
318#[derive(Debug, Clone, PartialEq, Eq)]
319pub enum VersionConstraint {
320    /// Exact version match
321    Exact(Version),
322    /// Greater than
323    GreaterThan(Version),
324    /// Greater than or equal
325    GreaterThanOrEqual(Version),
326    /// Less than
327    LessThan(Version),
328    /// Less than or equal
329    LessThanOrEqual(Version),
330    /// Compatible with (same major version, >= minor.patch)
331    Compatible(Version),
332    /// Tilde range (~1.2.3 means >=1.2.3 and <1.3.0)
333    Tilde(Version),
334    /// Caret range (^1.2.3 means >=1.2.3 and <2.0.0)
335    Caret(Version),
336    /// Wildcard (*) - any version
337    Any,
338    /// Combined constraints (AND)
339    And(Vec<VersionConstraint>),
340    /// Alternative constraints (OR)
341    Or(Vec<VersionConstraint>),
342}
343
344impl VersionConstraint {
345    /// Parse a version constraint string
346    pub fn constraint(constraintstr: &str) -> Result<Self, CoreError> {
347        let constraint = constraintstr.trim();
348
349        if constraint == "*" {
350            return Ok(Self::Any);
351        }
352
353        if let Some(stripped) = constraint.strip_prefix(">=") {
354            let version = Version::parse(stripped)?;
355            return Ok(Self::GreaterThanOrEqual(version));
356        }
357
358        if let Some(stripped) = constraint.strip_prefix("<=") {
359            let version = Version::parse(stripped)?;
360            return Ok(Self::LessThanOrEqual(version));
361        }
362
363        if let Some(stripped) = constraint.strip_prefix('>') {
364            let version = Version::parse(stripped)?;
365            return Ok(Self::GreaterThan(version));
366        }
367
368        if let Some(stripped) = constraint.strip_prefix('<') {
369            let version = Version::parse(stripped)?;
370            return Ok(Self::LessThan(version));
371        }
372
373        if let Some(stripped) = constraint.strip_prefix('~') {
374            let version = Version::parse(stripped)?;
375            return Ok(Self::Tilde(version));
376        }
377
378        if let Some(stripped) = constraint.strip_prefix('^') {
379            let version = Version::parse(stripped)?;
380            return Ok(Self::Caret(version));
381        }
382
383        if let Some(stripped) = constraint.strip_prefix('=') {
384            let version = Version::parse(stripped)?;
385            return Ok(Self::Exact(version));
386        }
387
388        // Default to exact match
389        let version = Version::parse(constraint)?;
390        Ok(Self::Exact(version))
391    }
392
393    /// Check if a version matches this constraint
394    pub fn matches(&self, version: &Version) -> bool {
395        match self {
396            Self::Exact(v) => version == v,
397            Self::GreaterThan(v) => version > v,
398            Self::GreaterThanOrEqual(v) => version >= v,
399            Self::LessThan(v) => version < v,
400            Self::LessThanOrEqual(v) => version <= v,
401            Self::Compatible(v) => version.major == v.major && version >= v,
402            Self::Tilde(v) => version.major == v.major && version.minor == v.minor && version >= v,
403            Self::Caret(v) => {
404                if v.major() > 0 {
405                    version.major == v.major && version >= v
406                } else if v.minor() > 0 {
407                    version.major == 0 && version.minor == v.minor && version >= v
408                } else {
409                    version.major == 0
410                        && version.minor == 0
411                        && version.patch == v.patch
412                        && version >= v
413                }
414            }
415            Self::Any => true,
416            Self::And(constraints) => constraints.iter().all(|c| c.matches(version)),
417            Self::Or(constraints) => constraints.iter().any(|c| c.matches(version)),
418        }
419    }
420}
421
422impl fmt::Display for VersionConstraint {
423    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
424        match self {
425            Self::Exact(v) => write!(f, "={v}"),
426            Self::GreaterThan(v) => write!(f, ">{v}"),
427            Self::GreaterThanOrEqual(v) => write!(f, ">={v}"),
428            Self::LessThan(v) => write!(f, "<{v}"),
429            Self::LessThanOrEqual(v) => write!(f, "<={v}"),
430            Self::Compatible(v) => write!(f, "~{v}"),
431            Self::Tilde(v) => write!(f, "~{v}"),
432            Self::Caret(v) => write!(f, "^{v}"),
433            Self::Any => write!(f, "*"),
434            Self::And(constraints) => {
435                let constraintstrs: Vec<String> =
436                    constraints.iter().map(|c| c.to_string()).collect();
437                let joined = constraintstrs.join(" && ");
438                write!(f, "{joined}")
439            }
440            Self::Or(constraints) => {
441                let constraintstrs: Vec<String> =
442                    constraints.iter().map(|c| c.to_string()).collect();
443                let joined = constraintstrs.join(" || ");
444                write!(f, "{joined}")
445            }
446        }
447    }
448}
449
450/// Version range specification
451#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
452#[derive(Debug, Clone, PartialEq, Eq)]
453pub struct VersionRange {
454    /// Minimum version (inclusive)
455    pub min: Option<Version>,
456    /// Maximum version (exclusive)
457    pub max: Option<Version>,
458    /// Include pre-release versions
459    pub include_prerelease: bool,
460}
461
462impl VersionRange {
463    /// Create a new version range
464    pub fn new(min: Option<Version>, max: Option<Version>) -> Self {
465        Self {
466            min,
467            max,
468            include_prerelease: false,
469        }
470    }
471
472    /// Create a range that includes pre-release versions
473    pub fn with_prerelease(mut self) -> Self {
474        self.include_prerelease = true;
475        self
476    }
477
478    /// Check if a version is within this range
479    pub fn contains(&self, version: &Version) -> bool {
480        // Check pre-release inclusion
481        if version.is_prerelease() && !self.include_prerelease {
482            return false;
483        }
484
485        // Check minimum bound
486        if let Some(ref min) = self.min {
487            if version < min {
488                return false;
489            }
490        }
491
492        // Check maximum bound
493        if let Some(ref max) = self.max {
494            if version >= max {
495                return false;
496            }
497        }
498
499        true
500    }
501
502    /// Get all versions in a list that fall within this range
503    pub fn filterversions<'a>(&self, versions: &'a [Version]) -> Vec<&'a Version> {
504        versions.iter().filter(|v| self.contains(v)).collect()
505    }
506}
507
508/// Version builder for convenient version construction
509pub struct VersionBuilder {
510    major: u64,
511    minor: u64,
512    patch: u64,
513    prerelease: Option<String>,
514    buildmetadata: Option<String>,
515}
516
517impl VersionBuilder {
518    /// Create a new version builder
519    pub fn new(major: u64, minor: u64, patch: u64) -> Self {
520        Self {
521            major,
522            minor,
523            patch,
524            prerelease: None,
525            buildmetadata: None,
526        }
527    }
528
529    /// Set pre-release identifier
530    pub fn release(mut self, prerelease: &str) -> Self {
531        self.prerelease = Some(prerelease.to_string());
532        self
533    }
534
535    /// Set build metadata
536    pub fn metadata(mut self, buildmetadata: &str) -> Self {
537        self.buildmetadata = Some(buildmetadata.to_string());
538        self
539    }
540
541    /// Build the version
542    pub fn build(self) -> Version {
543        Version {
544            major: self.major,
545            minor: self.minor,
546            patch: self.patch,
547            prerelease: self.prerelease,
548            buildmetadata: self.buildmetadata,
549        }
550    }
551}
552
553#[cfg(test)]
554mod tests {
555    use super::*;
556
557    #[test]
558    fn test_version_creation() {
559        let version = Version::new(1, 2, 3);
560        assert_eq!(version.major(), 1);
561        assert_eq!(version.minor(), 2);
562        assert_eq!(version.patch(), 3);
563        assert!(!version.is_prerelease());
564        assert!(version.is_stable());
565    }
566
567    #[test]
568    fn test_version_parsing() {
569        let version = Version::parse("1.2.3").unwrap();
570        assert_eq!(version.to_string(), "1.2.3");
571
572        let version = Version::parse("1.2.3-alpha.1").unwrap();
573        assert_eq!(version.to_string(), "1.2.3-alpha.1");
574        assert_eq!(version.prerelease(), Some("alpha.1"));
575        assert!(version.is_prerelease());
576
577        let version = Version::parse("1.2.3+build.123").unwrap();
578        assert_eq!(version.buildmetadata(), Some("build.123"));
579
580        let version = Version::parse("v1.2.3").unwrap();
581        assert_eq!(version.to_string(), "1.2.3");
582    }
583
584    #[test]
585    fn test_version_comparison() {
586        let v1_0_0 = Version::parse("1.0.0").unwrap();
587        let v1_0_1 = Version::parse("1.0.1").unwrap();
588        let v1_1_0 = Version::parse("1.1.0").unwrap();
589        let v2_0_0 = Version::parse("2.0.0").unwrap();
590        let v1_0_0_alpha = Version::parse("1.0.0-alpha").unwrap();
591
592        assert!(v1_0_0 < v1_0_1);
593        assert!(v1_0_1 < v1_1_0);
594        assert!(v1_1_0 < v2_0_0);
595        assert!(v1_0_0_alpha < v1_0_0);
596    }
597
598    #[test]
599    fn test_version_increments() {
600        let mut version = Version::parse("1.2.3-alpha+build").unwrap();
601
602        version.increment_patch();
603        assert_eq!(version.to_string(), "1.2.4");
604
605        version.increment_minor();
606        assert_eq!(version.to_string(), "1.3.0");
607
608        version.increment_major();
609        assert_eq!(version.to_string(), "2.0.0");
610    }
611
612    #[test]
613    fn test_version_constraints() {
614        let constraint = VersionConstraint::constraint(">=1.2.0").unwrap();
615        let version = Version::parse("1.2.3").unwrap();
616        assert!(constraint.matches(&version));
617
618        let constraint = VersionConstraint::constraint("^1.2.0").unwrap();
619        let version = Version::parse("1.5.0").unwrap();
620        assert!(constraint.matches(&version));
621        let version = Version::parse("2.0.0").unwrap();
622        assert!(!constraint.matches(&version));
623
624        let constraint = VersionConstraint::constraint("~1.2.0").unwrap();
625        let version = Version::parse("1.2.5").unwrap();
626        assert!(constraint.matches(&version));
627        let version = Version::parse("1.3.0").unwrap();
628        assert!(!constraint.matches(&version));
629    }
630
631    #[test]
632    fn test_version_range() {
633        let min = Version::parse("1.0.0").unwrap();
634        let max = Version::parse("2.0.0").unwrap();
635        let range = VersionRange::new(Some(min), Some(max));
636
637        let version = Version::parse("1.5.0").unwrap();
638        assert!(range.contains(&version));
639
640        let version = Version::parse("0.9.0").unwrap();
641        assert!(!range.contains(&version));
642
643        let version = Version::parse("2.0.0").unwrap();
644        assert!(!range.contains(&version));
645
646        let version = Version::parse("1.5.0-alpha").unwrap();
647        assert!(!range.contains(&version));
648
649        let range = range.with_prerelease();
650        assert!(range.contains(&version));
651    }
652
653    #[test]
654    fn test_version_builder() {
655        let version = VersionBuilder::new(1, 2, 3)
656            .release("alpha.1")
657            .metadata("build.123")
658            .build();
659
660        assert_eq!(version.to_string(), "1.2.3-alpha.1+build.123");
661    }
662
663    #[test]
664    fn test_compatibility() {
665        let v1_0_0 = Version::parse("1.0.0").unwrap();
666        let v1_2_0 = Version::parse("1.2.0").unwrap();
667        let v2_0_0 = Version::parse("2.0.0").unwrap();
668
669        assert!(v1_0_0.is_compatible_with(&v1_2_0));
670        assert!(v1_2_0.is_compatible_with(&v1_0_0));
671        assert!(!v1_0_0.is_compatible_with(&v2_0_0));
672        assert!(v2_0_0.has_breakingchanges_from(&v1_0_0)); // Major version change indicates breaking changes
673    }
674
675    #[test]
676    fn test_prerelease_comparison() {
677        let versions = vec![
678            Version::parse("1.0.0-alpha").unwrap(),
679            Version::parse("1.0.0-alpha.1").unwrap(),
680            Version::parse("1.0.0-alpha.beta").unwrap(),
681            Version::parse("1.0.0-beta").unwrap(),
682            Version::parse("1.0.0-beta.2").unwrap(),
683            Version::parse("1.0.0-beta.11").unwrap(),
684            Version::parse("1.0.0-rc.1").unwrap(),
685            Version::parse("1.0.0").unwrap(),
686        ];
687
688        let mut sortedversions = versions.clone();
689        sortedversions.sort();
690
691        assert_eq!(sortedversions, versions);
692    }
693}