Skip to main content

torsh_package/
version.rs

1//! Package version handling and compatibility
2
3use serde::{Deserialize, Serialize};
4use std::fmt;
5
6/// Package version with semantic versioning
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct PackageVersion {
9    /// Major version number (breaking changes)
10    pub major: u32,
11    /// Minor version number (backward-compatible additions)
12    pub minor: u32,
13    /// Patch version number (backward-compatible bug fixes)
14    pub patch: u32,
15    /// Pre-release version identifier (e.g., "alpha.1", "beta.2")
16    pub pre_release: Option<String>,
17    /// Build metadata (ignored in version precedence)
18    pub build_metadata: Option<String>,
19}
20
21impl PackageVersion {
22    /// Create a new version
23    pub fn new(major: u32, minor: u32, patch: u32) -> Self {
24        Self {
25            major,
26            minor,
27            patch,
28            pre_release: None,
29            build_metadata: None,
30        }
31    }
32
33    /// Parse version from string
34    pub fn parse(version: &str) -> Result<Self, String> {
35        let semver = semver::Version::parse(version)
36            .map_err(|e| format!("Invalid version format: {}", e))?;
37
38        Ok(Self {
39            major: semver.major as u32,
40            minor: semver.minor as u32,
41            patch: semver.patch as u32,
42            pre_release: if semver.pre.is_empty() {
43                None
44            } else {
45                Some(semver.pre.to_string())
46            },
47            build_metadata: if semver.build.is_empty() {
48                None
49            } else {
50                Some(semver.build.to_string())
51            },
52        })
53    }
54
55    /// Check if this version is compatible with a requirement
56    pub fn is_compatible_with(&self, requirement: &VersionRequirement) -> bool {
57        requirement.matches(self)
58    }
59
60    /// Get the next major version
61    pub fn next_major(&self) -> Self {
62        Self::new(self.major + 1, 0, 0)
63    }
64
65    /// Get the next minor version
66    pub fn next_minor(&self) -> Self {
67        Self::new(self.major, self.minor + 1, 0)
68    }
69
70    /// Get the next patch version
71    pub fn next_patch(&self) -> Self {
72        Self::new(self.major, self.minor, self.patch + 1)
73    }
74
75    /// Check if this is a pre-release version
76    pub fn is_pre_release(&self) -> bool {
77        self.pre_release.is_some()
78    }
79}
80
81impl PartialEq for PackageVersion {
82    fn eq(&self, other: &Self) -> bool {
83        // According to semantic versioning, build metadata should be ignored in comparisons
84        self.major == other.major
85            && self.minor == other.minor
86            && self.patch == other.patch
87            && self.pre_release == other.pre_release
88        // Note: build_metadata is intentionally ignored
89    }
90}
91
92impl Eq for PackageVersion {}
93
94impl std::cmp::PartialOrd for PackageVersion {
95    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
96        Some(self.cmp(other))
97    }
98}
99
100impl std::cmp::Ord for PackageVersion {
101    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
102        use std::cmp::Ordering;
103
104        // First compare major.minor.patch
105        match (self.major, self.minor, self.patch).cmp(&(other.major, other.minor, other.patch)) {
106            Ordering::Equal => {
107                // When major.minor.patch are equal, handle pre-release semantics
108                match (&self.pre_release, &other.pre_release) {
109                    (None, None) => Ordering::Equal,
110                    (None, Some(_)) => Ordering::Greater, // Release > pre-release
111                    (Some(_), None) => Ordering::Less,    // Pre-release < release
112                    (Some(a), Some(b)) => a.cmp(b),       // Compare pre-release strings
113                }
114                // Note: build metadata is ignored in version precedence per semver
115            }
116            other_ordering => other_ordering,
117        }
118    }
119}
120
121impl fmt::Display for PackageVersion {
122    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)?;
124
125        if let Some(pre) = &self.pre_release {
126            write!(f, "-{}", pre)?;
127        }
128
129        if let Some(build) = &self.build_metadata {
130            write!(f, "+{}", build)?;
131        }
132
133        Ok(())
134    }
135}
136
137impl From<semver::Version> for PackageVersion {
138    fn from(v: semver::Version) -> Self {
139        Self {
140            major: v.major as u32,
141            minor: v.minor as u32,
142            patch: v.patch as u32,
143            pre_release: if v.pre.is_empty() {
144                None
145            } else {
146                Some(v.pre.to_string())
147            },
148            build_metadata: if v.build.is_empty() {
149                None
150            } else {
151                Some(v.build.to_string())
152            },
153        }
154    }
155}
156
157/// Version requirement specification
158#[derive(Debug, Clone, Serialize, Deserialize)]
159pub struct VersionRequirement {
160    /// Comparison operator to use
161    pub comparator: VersionComparator,
162    /// Version to compare against
163    pub version: PackageVersion,
164}
165
166/// Version comparison operators
167#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
168pub enum VersionComparator {
169    /// Exact match
170    Exact,
171    /// Greater than
172    GreaterThan,
173    /// Greater than or equal
174    GreaterThanOrEqual,
175    /// Less than
176    LessThan,
177    /// Less than or equal
178    LessThanOrEqual,
179    /// Compatible (same major version, >= minor/patch)
180    Compatible,
181}
182
183impl VersionRequirement {
184    /// Create a new version requirement
185    pub fn new(comparator: VersionComparator, version: PackageVersion) -> Self {
186        Self {
187            comparator,
188            version,
189        }
190    }
191
192    /// Parse requirement from string
193    pub fn parse(req: &str) -> Result<Self, String> {
194        let req = req.trim();
195
196        if req.is_empty() {
197            return Err("Empty version requirement".to_string());
198        }
199
200        // Handle different formats
201        if let Some(version_str) = req.strip_prefix("^") {
202            // Compatible version (^1.2.3)
203            let version = PackageVersion::parse(version_str)?;
204            Ok(Self::new(VersionComparator::Compatible, version))
205        } else if let Some(version_str) = req.strip_prefix(">=") {
206            let version = PackageVersion::parse(version_str)?;
207            Ok(Self::new(VersionComparator::GreaterThanOrEqual, version))
208        } else if let Some(version_str) = req.strip_prefix(">") {
209            let version = PackageVersion::parse(version_str)?;
210            Ok(Self::new(VersionComparator::GreaterThan, version))
211        } else if let Some(version_str) = req.strip_prefix("<=") {
212            let version = PackageVersion::parse(version_str)?;
213            Ok(Self::new(VersionComparator::LessThanOrEqual, version))
214        } else if let Some(version_str) = req.strip_prefix("<") {
215            let version = PackageVersion::parse(version_str)?;
216            Ok(Self::new(VersionComparator::LessThan, version))
217        } else if let Some(version_str) = req.strip_prefix("=") {
218            let version = PackageVersion::parse(version_str)?;
219            Ok(Self::new(VersionComparator::Exact, version))
220        } else {
221            // Default to exact match
222            let version = PackageVersion::parse(req)?;
223            Ok(Self::new(VersionComparator::Exact, version))
224        }
225    }
226
227    /// Check if a version matches this requirement
228    pub fn matches(&self, version: &PackageVersion) -> bool {
229        match self.comparator {
230            VersionComparator::Exact => version == &self.version,
231            VersionComparator::GreaterThan => version > &self.version,
232            VersionComparator::GreaterThanOrEqual => version >= &self.version,
233            VersionComparator::LessThan => version < &self.version,
234            VersionComparator::LessThanOrEqual => version <= &self.version,
235            VersionComparator::Compatible => {
236                version.major == self.version.major
237                    && (version.minor > self.version.minor
238                        || (version.minor == self.version.minor
239                            && version.patch >= self.version.patch))
240            }
241        }
242    }
243}
244
245impl fmt::Display for VersionRequirement {
246    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
247        let prefix = match self.comparator {
248            VersionComparator::Exact => "=",
249            VersionComparator::GreaterThan => ">",
250            VersionComparator::GreaterThanOrEqual => ">=",
251            VersionComparator::LessThan => "<",
252            VersionComparator::LessThanOrEqual => "<=",
253            VersionComparator::Compatible => "^",
254        };
255
256        write!(f, "{}{}", prefix, self.version)
257    }
258}
259
260/// Version compatibility checker
261pub struct CompatibilityChecker {
262    requirements: Vec<VersionRequirement>,
263}
264
265impl CompatibilityChecker {
266    /// Create a new compatibility checker
267    pub fn new() -> Self {
268        Self {
269            requirements: Vec::new(),
270        }
271    }
272
273    /// Add a requirement
274    pub fn add_requirement(&mut self, requirement: VersionRequirement) {
275        self.requirements.push(requirement);
276    }
277
278    /// Check if a version satisfies all requirements
279    pub fn check(&self, version: &PackageVersion) -> bool {
280        self.requirements.iter().all(|req| req.matches(version))
281    }
282
283    /// Get all requirements
284    pub fn requirements(&self) -> &[VersionRequirement] {
285        &self.requirements
286    }
287}
288
289impl Default for CompatibilityChecker {
290    fn default() -> Self {
291        Self::new()
292    }
293}
294
295#[cfg(test)]
296mod tests {
297    use super::*;
298
299    #[test]
300    fn test_version_parsing() {
301        let v1 = PackageVersion::parse("1.2.3").expect("Failed to parse version in test");
302        assert_eq!(v1.major, 1);
303        assert_eq!(v1.minor, 2);
304        assert_eq!(v1.patch, 3);
305
306        let v2 = PackageVersion::parse("2.0.0-alpha.1").expect("Failed to parse version in test");
307        assert_eq!(v2.major, 2);
308        assert_eq!(v2.minor, 0);
309        assert_eq!(v2.patch, 0);
310        assert_eq!(v2.pre_release.as_deref(), Some("alpha.1"));
311    }
312
313    #[test]
314    fn test_version_comparison() {
315        let v1 = PackageVersion::new(1, 2, 3);
316        let v2 = PackageVersion::new(1, 2, 4);
317        let v3 = PackageVersion::new(1, 3, 0);
318        let v4 = PackageVersion::new(2, 0, 0);
319
320        assert!(v1 < v2);
321        assert!(v2 < v3);
322        assert!(v3 < v4);
323    }
324
325    #[test]
326    fn test_version_requirement() {
327        let req = VersionRequirement::parse("^1.2.3").expect("Failed to parse version in test");
328
329        assert!(req.matches(&PackageVersion::new(1, 2, 3)));
330        assert!(req.matches(&PackageVersion::new(1, 2, 4)));
331        assert!(req.matches(&PackageVersion::new(1, 3, 0)));
332        assert!(!req.matches(&PackageVersion::new(1, 2, 2)));
333        assert!(!req.matches(&PackageVersion::new(2, 0, 0)));
334
335        let req2 = VersionRequirement::parse(">=1.2.0").expect("Failed to parse version in test");
336        assert!(req2.matches(&PackageVersion::new(1, 2, 0)));
337        assert!(req2.matches(&PackageVersion::new(1, 3, 0)));
338        assert!(req2.matches(&PackageVersion::new(2, 0, 0)));
339        assert!(!req2.matches(&PackageVersion::new(1, 1, 9)));
340    }
341
342    #[test]
343    fn test_compatibility_checker() {
344        let mut checker = CompatibilityChecker::new();
345        checker.add_requirement(
346            VersionRequirement::parse(">=1.2.0").expect("Failed to parse version in test"),
347        );
348        checker.add_requirement(
349            VersionRequirement::parse("<2.0.0").expect("Failed to parse version in test"),
350        );
351
352        assert!(checker.check(&PackageVersion::new(1, 2, 0)));
353        assert!(checker.check(&PackageVersion::new(1, 5, 3)));
354        assert!(!checker.check(&PackageVersion::new(1, 1, 9)));
355        assert!(!checker.check(&PackageVersion::new(2, 0, 0)));
356    }
357}