synapse-primitives 0.0.2

Core types and ID hashing for Synapse RPC framework
Documentation
//! Decimal semver packing for efficient version representation
//!
//! This module provides utilities to pack semantic versions (major.minor.patch)
//! into a single u32 integer, enabling efficient storage and comparison.
//!
//! # Format
//!
//! `version = major * 1_000_000 + minor * 1_000 + patch`
//!
//! # Limits
//!
//! - Major: 0-4294
//! - Minor: 0-999
//! - Patch: 0-999
//!
//! # Examples
//!
//! ```
//! use synapse_primitives::semver::{PackedVersion, pack_version};
//!
//! let version = pack_version(2, 3, 1).unwrap();
//! assert_eq!(version.as_u32(), 2_003_001);
//!
//! // Versions are comparable as integers
//! assert!(pack_version(2, 3, 1).unwrap() < pack_version(2, 4, 0).unwrap());
//! assert!(pack_version(2, 10, 0).unwrap() > pack_version(2, 9, 999).unwrap());
//! ```

use std::fmt;
use thiserror::Error;

/// Packed semantic version represented as a u32
///
/// Format: `major * 1_000_000 + minor * 1_000 + patch`
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct PackedVersion(u32);

/// Error type for version packing operations
#[derive(Debug, Error, PartialEq)]
pub enum VersionError {
    #[error("Major version {0} exceeds maximum of 4294")]
    MajorTooLarge(u32),

    #[error("Minor version {0} exceeds maximum of 999")]
    MinorTooLarge(u32),

    #[error("Patch version {0} exceeds maximum of 999")]
    PatchTooLarge(u32),

    #[error("Invalid version string format: {0}")]
    InvalidFormat(String),

    #[error("Failed to parse version component: {0}")]
    ParseError(String),
}

impl PackedVersion {
    /// Maximum allowed major version (leaves room for minor/patch)
    /// u32::MAX = 4,294,967,295
    /// With max minor (999,999), max major = 4,293
    pub const MAX_MAJOR: u32 = 4293;

    /// Maximum allowed minor version
    pub const MAX_MINOR: u32 = 999;

    /// Maximum allowed patch version
    pub const MAX_PATCH: u32 = 999;

    /// Create a new packed version from components
    ///
    /// Returns an error if any component exceeds its maximum value.
    pub fn new(major: u32, minor: u32, patch: u32) -> Result<Self, VersionError> {
        if major > Self::MAX_MAJOR {
            return Err(VersionError::MajorTooLarge(major));
        }
        if minor > Self::MAX_MINOR {
            return Err(VersionError::MinorTooLarge(minor));
        }
        if patch > Self::MAX_PATCH {
            return Err(VersionError::PatchTooLarge(patch));
        }

        // Use checked arithmetic to avoid overflow panics
        let major_part = major
            .checked_mul(1_000_000)
            .ok_or(VersionError::MajorTooLarge(major))?;
        let minor_part = minor
            .checked_mul(1_000)
            .ok_or(VersionError::MinorTooLarge(minor))?;
        let value = major_part
            .checked_add(minor_part)
            .and_then(|v| v.checked_add(patch))
            .ok_or(VersionError::MajorTooLarge(major))?;

        Ok(Self(value))
    }

    /// Create from a u32 directly (unsafe - no validation)
    ///
    /// Use this when you've already validated the packed format or received
    /// it from a trusted source (e.g., wire protocol).
    pub const fn from_raw(value: u32) -> Self {
        Self(value)
    }

    /// Parse a version string in "major.minor.patch" format
    ///
    /// # Examples
    ///
    /// ```
    /// use synapse_primitives::semver::PackedVersion;
    ///
    /// let version = PackedVersion::parse("2.3.1").unwrap();
    /// assert_eq!(version.as_u32(), 2_003_001);
    /// ```
    pub fn parse(s: &str) -> Result<Self, VersionError> {
        let parts: Vec<&str> = s.split('.').collect();

        if parts.len() != 3 {
            return Err(VersionError::InvalidFormat(format!(
                "Expected format 'major.minor.patch', got '{}'",
                s
            )));
        }

        let major = parts[0]
            .parse::<u32>()
            .map_err(|e| VersionError::ParseError(format!("major: {}", e)))?;

        let minor = parts[1]
            .parse::<u32>()
            .map_err(|e| VersionError::ParseError(format!("minor: {}", e)))?;

        let patch = parts[2]
            .parse::<u32>()
            .map_err(|e| VersionError::ParseError(format!("patch: {}", e)))?;

        Self::new(major, minor, patch)
    }

    /// Get the raw u32 value
    pub const fn as_u32(&self) -> u32 {
        self.0
    }

    /// Extract major version component
    pub const fn major(&self) -> u32 {
        self.0 / 1_000_000
    }

    /// Extract minor version component
    pub const fn minor(&self) -> u32 {
        (self.0 / 1_000) % 1_000
    }

    /// Extract patch version component
    pub const fn patch(&self) -> u32 {
        self.0 % 1_000
    }

    /// Check if this version is compatible with another (same major version)
    pub fn is_compatible_with(&self, other: &Self) -> bool {
        self.major() == other.major()
    }

    /// Check if this version is a breaking change from another (major version increased)
    pub fn is_breaking_change_from(&self, other: &Self) -> bool {
        self.major() > other.major()
    }
}

/// Pack a semantic version into u32
///
/// This is a convenience function that wraps PackedVersion::new().
/// Returns Err if any component exceeds its maximum.
pub fn pack_version(major: u32, minor: u32, patch: u32) -> Result<PackedVersion, VersionError> {
    PackedVersion::new(major, minor, patch)
}

/// Pack a version without validation (const fn)
///
/// # Safety
///
/// Caller must ensure components are within valid ranges:
/// - major <= 4294
/// - minor <= 999
/// - patch <= 999
pub const fn pack_version_unchecked(major: u32, minor: u32, patch: u32) -> PackedVersion {
    PackedVersion(major * 1_000_000 + minor * 1_000 + patch)
}

impl fmt::Display for PackedVersion {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}.{}.{}", self.major(), self.minor(), self.patch())
    }
}

impl From<PackedVersion> for u32 {
    fn from(v: PackedVersion) -> u32 {
        v.0
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_pack_version() {
        let v = pack_version(2, 3, 1).unwrap();
        assert_eq!(v.as_u32(), 2_003_001);
        assert_eq!(v.major(), 2);
        assert_eq!(v.minor(), 3);
        assert_eq!(v.patch(), 1);
    }

    #[test]
    fn test_version_comparison() {
        assert!(pack_version(2, 3, 1).unwrap() < pack_version(2, 4, 0).unwrap());
        assert!(pack_version(2, 3, 99).unwrap() < pack_version(2, 4, 0).unwrap());
        assert!(pack_version(2, 10, 0).unwrap() > pack_version(2, 9, 999).unwrap());
        assert!(pack_version(3, 0, 0).unwrap() > pack_version(2, 999, 999).unwrap());
    }

    #[test]
    fn test_parse_version() {
        let v = PackedVersion::parse("2.3.1").unwrap();
        assert_eq!(v.as_u32(), 2_003_001);

        let v = PackedVersion::parse("10.999.999").unwrap();
        assert_eq!(v.major(), 10);
        assert_eq!(v.minor(), 999);
        assert_eq!(v.patch(), 999);
    }

    #[test]
    fn test_parse_errors() {
        assert!(PackedVersion::parse("1.2").is_err());
        assert!(PackedVersion::parse("1.2.3.4").is_err());
        assert!(PackedVersion::parse("a.b.c").is_err());
        assert!(PackedVersion::parse("1.1000.1").is_err()); // minor too large
    }

    #[test]
    fn test_limits() {
        // Valid edge cases
        assert!(pack_version(4293, 999, 999).is_ok());
        assert!(pack_version(0, 0, 0).is_ok());

        // Invalid cases
        assert_eq!(
            pack_version(4294, 0, 0),
            Err(VersionError::MajorTooLarge(4294))
        );
        assert_eq!(
            pack_version(0, 1000, 0),
            Err(VersionError::MinorTooLarge(1000))
        );
        assert_eq!(
            pack_version(0, 0, 1000),
            Err(VersionError::PatchTooLarge(1000))
        );
    }

    #[test]
    fn test_display() {
        let v = pack_version(2, 3, 1).unwrap();
        assert_eq!(v.to_string(), "2.3.1");
    }

    #[test]
    fn test_compatibility() {
        let v1 = pack_version(2, 3, 1).unwrap();
        let v2 = pack_version(2, 5, 0).unwrap();
        let v3 = pack_version(3, 0, 0).unwrap();

        assert!(v1.is_compatible_with(&v2));
        assert!(!v1.is_compatible_with(&v3));

        assert!(!v1.is_breaking_change_from(&v2));
        assert!(v3.is_breaking_change_from(&v1));
    }

    #[test]
    fn test_const_packing() {
        const VERSION: PackedVersion = pack_version_unchecked(1, 0, 0);
        assert_eq!(VERSION.as_u32(), 1_000_000);
    }
}