Skip to main content

synapse_primitives/
semver.rs

1//! Decimal semver packing for efficient version representation
2//!
3//! This module provides utilities to pack semantic versions (major.minor.patch)
4//! into a single u32 integer, enabling efficient storage and comparison.
5//!
6//! # Format
7//!
8//! `version = major * 1_000_000 + minor * 1_000 + patch`
9//!
10//! # Limits
11//!
12//! - Major: 0-4294
13//! - Minor: 0-999
14//! - Patch: 0-999
15//!
16//! # Examples
17//!
18//! ```
19//! use synapse_primitives::semver::{PackedVersion, pack_version};
20//!
21//! let version = pack_version(2, 3, 1).unwrap();
22//! assert_eq!(version.as_u32(), 2_003_001);
23//!
24//! // Versions are comparable as integers
25//! assert!(pack_version(2, 3, 1).unwrap() < pack_version(2, 4, 0).unwrap());
26//! assert!(pack_version(2, 10, 0).unwrap() > pack_version(2, 9, 999).unwrap());
27//! ```
28
29use std::fmt;
30use thiserror::Error;
31
32/// Packed semantic version represented as a u32
33///
34/// Format: `major * 1_000_000 + minor * 1_000 + patch`
35#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
36pub struct PackedVersion(u32);
37
38/// Error type for version packing operations
39#[derive(Debug, Error, PartialEq)]
40pub enum VersionError {
41    #[error("Major version {0} exceeds maximum of 4294")]
42    MajorTooLarge(u32),
43
44    #[error("Minor version {0} exceeds maximum of 999")]
45    MinorTooLarge(u32),
46
47    #[error("Patch version {0} exceeds maximum of 999")]
48    PatchTooLarge(u32),
49
50    #[error("Invalid version string format: {0}")]
51    InvalidFormat(String),
52
53    #[error("Failed to parse version component: {0}")]
54    ParseError(String),
55}
56
57impl PackedVersion {
58    /// Maximum allowed major version (leaves room for minor/patch)
59    /// u32::MAX = 4,294,967,295
60    /// With max minor (999,999), max major = 4,293
61    pub const MAX_MAJOR: u32 = 4293;
62
63    /// Maximum allowed minor version
64    pub const MAX_MINOR: u32 = 999;
65
66    /// Maximum allowed patch version
67    pub const MAX_PATCH: u32 = 999;
68
69    /// Create a new packed version from components
70    ///
71    /// Returns an error if any component exceeds its maximum value.
72    pub fn new(major: u32, minor: u32, patch: u32) -> Result<Self, VersionError> {
73        if major > Self::MAX_MAJOR {
74            return Err(VersionError::MajorTooLarge(major));
75        }
76        if minor > Self::MAX_MINOR {
77            return Err(VersionError::MinorTooLarge(minor));
78        }
79        if patch > Self::MAX_PATCH {
80            return Err(VersionError::PatchTooLarge(patch));
81        }
82
83        // Use checked arithmetic to avoid overflow panics
84        let major_part = major
85            .checked_mul(1_000_000)
86            .ok_or(VersionError::MajorTooLarge(major))?;
87        let minor_part = minor
88            .checked_mul(1_000)
89            .ok_or(VersionError::MinorTooLarge(minor))?;
90        let value = major_part
91            .checked_add(minor_part)
92            .and_then(|v| v.checked_add(patch))
93            .ok_or(VersionError::MajorTooLarge(major))?;
94
95        Ok(Self(value))
96    }
97
98    /// Create from a u32 directly (unsafe - no validation)
99    ///
100    /// Use this when you've already validated the packed format or received
101    /// it from a trusted source (e.g., wire protocol).
102    pub const fn from_raw(value: u32) -> Self {
103        Self(value)
104    }
105
106    /// Parse a version string in "major.minor.patch" format
107    ///
108    /// # Examples
109    ///
110    /// ```
111    /// use synapse_primitives::semver::PackedVersion;
112    ///
113    /// let version = PackedVersion::parse("2.3.1").unwrap();
114    /// assert_eq!(version.as_u32(), 2_003_001);
115    /// ```
116    pub fn parse(s: &str) -> Result<Self, VersionError> {
117        let parts: Vec<&str> = s.split('.').collect();
118
119        if parts.len() != 3 {
120            return Err(VersionError::InvalidFormat(format!(
121                "Expected format 'major.minor.patch', got '{}'",
122                s
123            )));
124        }
125
126        let major = parts[0]
127            .parse::<u32>()
128            .map_err(|e| VersionError::ParseError(format!("major: {}", e)))?;
129
130        let minor = parts[1]
131            .parse::<u32>()
132            .map_err(|e| VersionError::ParseError(format!("minor: {}", e)))?;
133
134        let patch = parts[2]
135            .parse::<u32>()
136            .map_err(|e| VersionError::ParseError(format!("patch: {}", e)))?;
137
138        Self::new(major, minor, patch)
139    }
140
141    /// Get the raw u32 value
142    pub const fn as_u32(&self) -> u32 {
143        self.0
144    }
145
146    /// Extract major version component
147    pub const fn major(&self) -> u32 {
148        self.0 / 1_000_000
149    }
150
151    /// Extract minor version component
152    pub const fn minor(&self) -> u32 {
153        (self.0 / 1_000) % 1_000
154    }
155
156    /// Extract patch version component
157    pub const fn patch(&self) -> u32 {
158        self.0 % 1_000
159    }
160
161    /// Check if this version is compatible with another (same major version)
162    pub fn is_compatible_with(&self, other: &Self) -> bool {
163        self.major() == other.major()
164    }
165
166    /// Check if this version is a breaking change from another (major version increased)
167    pub fn is_breaking_change_from(&self, other: &Self) -> bool {
168        self.major() > other.major()
169    }
170}
171
172/// Pack a semantic version into u32
173///
174/// This is a convenience function that wraps PackedVersion::new().
175/// Returns Err if any component exceeds its maximum.
176pub fn pack_version(major: u32, minor: u32, patch: u32) -> Result<PackedVersion, VersionError> {
177    PackedVersion::new(major, minor, patch)
178}
179
180/// Pack a version without validation (const fn)
181///
182/// # Safety
183///
184/// Caller must ensure components are within valid ranges:
185/// - major <= 4294
186/// - minor <= 999
187/// - patch <= 999
188pub const fn pack_version_unchecked(major: u32, minor: u32, patch: u32) -> PackedVersion {
189    PackedVersion(major * 1_000_000 + minor * 1_000 + patch)
190}
191
192impl fmt::Display for PackedVersion {
193    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
194        write!(f, "{}.{}.{}", self.major(), self.minor(), self.patch())
195    }
196}
197
198impl From<PackedVersion> for u32 {
199    fn from(v: PackedVersion) -> u32 {
200        v.0
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207
208    #[test]
209    fn test_pack_version() {
210        let v = pack_version(2, 3, 1).unwrap();
211        assert_eq!(v.as_u32(), 2_003_001);
212        assert_eq!(v.major(), 2);
213        assert_eq!(v.minor(), 3);
214        assert_eq!(v.patch(), 1);
215    }
216
217    #[test]
218    fn test_version_comparison() {
219        assert!(pack_version(2, 3, 1).unwrap() < pack_version(2, 4, 0).unwrap());
220        assert!(pack_version(2, 3, 99).unwrap() < pack_version(2, 4, 0).unwrap());
221        assert!(pack_version(2, 10, 0).unwrap() > pack_version(2, 9, 999).unwrap());
222        assert!(pack_version(3, 0, 0).unwrap() > pack_version(2, 999, 999).unwrap());
223    }
224
225    #[test]
226    fn test_parse_version() {
227        let v = PackedVersion::parse("2.3.1").unwrap();
228        assert_eq!(v.as_u32(), 2_003_001);
229
230        let v = PackedVersion::parse("10.999.999").unwrap();
231        assert_eq!(v.major(), 10);
232        assert_eq!(v.minor(), 999);
233        assert_eq!(v.patch(), 999);
234    }
235
236    #[test]
237    fn test_parse_errors() {
238        assert!(PackedVersion::parse("1.2").is_err());
239        assert!(PackedVersion::parse("1.2.3.4").is_err());
240        assert!(PackedVersion::parse("a.b.c").is_err());
241        assert!(PackedVersion::parse("1.1000.1").is_err()); // minor too large
242    }
243
244    #[test]
245    fn test_limits() {
246        // Valid edge cases
247        assert!(pack_version(4293, 999, 999).is_ok());
248        assert!(pack_version(0, 0, 0).is_ok());
249
250        // Invalid cases
251        assert_eq!(
252            pack_version(4294, 0, 0),
253            Err(VersionError::MajorTooLarge(4294))
254        );
255        assert_eq!(
256            pack_version(0, 1000, 0),
257            Err(VersionError::MinorTooLarge(1000))
258        );
259        assert_eq!(
260            pack_version(0, 0, 1000),
261            Err(VersionError::PatchTooLarge(1000))
262        );
263    }
264
265    #[test]
266    fn test_display() {
267        let v = pack_version(2, 3, 1).unwrap();
268        assert_eq!(v.to_string(), "2.3.1");
269    }
270
271    #[test]
272    fn test_compatibility() {
273        let v1 = pack_version(2, 3, 1).unwrap();
274        let v2 = pack_version(2, 5, 0).unwrap();
275        let v3 = pack_version(3, 0, 0).unwrap();
276
277        assert!(v1.is_compatible_with(&v2));
278        assert!(!v1.is_compatible_with(&v3));
279
280        assert!(!v1.is_breaking_change_from(&v2));
281        assert!(v3.is_breaking_change_from(&v1));
282    }
283
284    #[test]
285    fn test_const_packing() {
286        const VERSION: PackedVersion = pack_version_unchecked(1, 0, 0);
287        assert_eq!(VERSION.as_u32(), 1_000_000);
288    }
289}