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