newton-core 0.4.16

newton protocol core sdk
//! Protocol version compatibility module
//!
//! This module defines protocol version constants and provides utilities
//! for checking version compatibility between deployed contracts and
//! the Rust-side protocol infrastructure (gateway, operators, CLI).
//!
//! Version constants are automatically injected at compile time from Cargo.toml
//! via the build script. To update the protocol version, modify the workspace
//! version in the root Cargo.toml.

/// Current protocol version following SemVer 2.0.0
/// Automatically set from CARGO_PKG_VERSION at build time
pub const PROTOCOL_VERSION: &str = env!("PROTOCOL_VERSION");

/// Minimum compatible factory version (MAJOR.MINOR.0)
/// Derived from CARGO_PKG_VERSION at build time by zeroing the patch component.
/// Used for both policy and policy data factory version checks.
pub const MIN_COMPATIBLE_VERSION: &str = env!("MIN_COMPATIBLE_VERSION");

/// Errors that can occur during version parsing and validation
#[derive(Debug, thiserror::Error)]
pub enum VersionError {
    /// Version string does not match expected format (MAJOR.MINOR.PATCH)
    #[error("Invalid version format: {0}")]
    InvalidFormat(String),
    /// Failed to parse a version component as a number
    #[error("Version component parse error: {0}")]
    ParseError(String),
}

/// Parse a semantic version string into (major, minor, patch) components
///
/// # Arguments
/// * `version` - Version string in format "MAJOR.MINOR.PATCH" (e.g., "1.0.0")
///
/// # Returns
/// * `Ok((major, minor, patch))` - Parsed version components
/// * `Err(VersionError)` - If version format is invalid
///
/// # Example
/// ```
/// use newton_core::version::parse_version;
/// let (major, minor, patch) = parse_version("1.2.3").unwrap();
/// assert_eq!((major, minor, patch), (1, 2, 3));
/// ```
pub fn parse_version(version: &str) -> Result<(u32, u32, u32), VersionError> {
    let parts: Vec<&str> = version.split('.').collect();

    if parts.len() != 3 {
        return Err(VersionError::InvalidFormat(format!(
            "Expected format 'MAJOR.MINOR.PATCH', got '{}'",
            version
        )));
    }

    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)))?;

    Ok((major, minor, patch))
}

/// Check if an actual version is compatible with a minimum required version
///
/// Compatibility rules (lenient SemVer):
/// - Major versions must match exactly
/// - Minor version must be >= minimum
/// - Patch version is ignored (patch bumps are backward-compatible bug fixes)
///
/// # Arguments
/// * `actual` - The actual version string (e.g., "1.2.3")
/// * `minimum` - The minimum required version string (e.g., "1.0.0")
///
/// # Returns
/// * `Ok(true)` - If actual version is compatible with minimum
/// * `Ok(false)` - If actual version is incompatible
/// * `Err(VersionError)` - If either version string is invalid
///
/// # Example
/// ```
/// use newton_core::version::is_compatible;
/// assert!(is_compatible("1.2.3", "1.0.0").unwrap()); // true: same major, newer minor
/// assert!(is_compatible("1.0.0", "1.0.5").unwrap()); // true: patch ignored
/// assert!(!is_compatible("2.0.0", "1.0.0").unwrap()); // false: different major
/// assert!(!is_compatible("1.0.0", "1.1.0").unwrap()); // false: older minor
/// ```
pub fn is_compatible(actual: &str, minimum: &str) -> Result<bool, VersionError> {
    let (actual_major, actual_minor, _) = parse_version(actual)?;
    let (min_major, min_minor, _) = parse_version(minimum)?;

    // Major version must match
    if actual_major != min_major {
        return Ok(false);
    }

    // Minor version must be >= minimum; patch is ignored
    Ok(actual_minor >= min_minor)
}

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

    #[test]
    fn test_version_constants_injected() {
        // Verify that version constants are properly injected at compile time
        assert!(!PROTOCOL_VERSION.is_empty(), "PROTOCOL_VERSION should not be empty");
        assert!(
            !MIN_COMPATIBLE_VERSION.is_empty(),
            "MIN_COMPATIBLE_VERSION should not be empty"
        );

        // Verify they are valid semver
        assert!(
            parse_version(PROTOCOL_VERSION).is_ok(),
            "PROTOCOL_VERSION should be valid semver"
        );
        assert!(
            parse_version(MIN_COMPATIBLE_VERSION).is_ok(),
            "MIN_COMPATIBLE_VERSION should be valid semver"
        );
    }

    #[test]
    fn test_parse_version() {
        assert_eq!(parse_version("1.0.0").unwrap(), (1, 0, 0));
        assert_eq!(parse_version("2.5.10").unwrap(), (2, 5, 10));

        assert!(parse_version("1.0").is_err());
        assert!(parse_version("1.0.0.0").is_err());
        assert!(parse_version("abc").is_err());
    }

    #[test]
    fn test_is_compatible() {
        // Same major, newer minor
        assert!(is_compatible("1.2.0", "1.0.0").unwrap());
        assert!(is_compatible("1.5.0", "1.2.0").unwrap());

        // Same major/minor, newer patch
        assert!(is_compatible("1.0.5", "1.0.3").unwrap());
        assert!(is_compatible("1.2.10", "1.2.5").unwrap());

        // Exact match
        assert!(is_compatible("1.0.0", "1.0.0").unwrap());
        assert!(is_compatible("2.5.3", "2.5.3").unwrap());

        // Different major version
        assert!(!is_compatible("2.0.0", "1.0.0").unwrap());
        assert!(!is_compatible("1.0.0", "2.0.0").unwrap());

        // Older minor version
        assert!(!is_compatible("1.0.0", "1.1.0").unwrap());
        assert!(!is_compatible("1.2.0", "1.5.0").unwrap());

        // Same minor, older patch — compatible (patch ignored)
        assert!(is_compatible("1.0.3", "1.0.5").unwrap());
        assert!(is_compatible("2.5.0", "2.5.1").unwrap());

        // Patch version fully ignored in both directions
        assert!(is_compatible("1.0.0", "1.0.99").unwrap());
        assert!(is_compatible("1.0.99", "1.0.0").unwrap());
    }
}