iggy_common 0.10.0

Iggy is the persistent message streaming platform written in Rust, supporting QUIC, TCP and HTTP transport protocols, capable of processing millions of messages per second.
Documentation
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

use crate::IggyError;
use std::borrow::Cow;
use std::fmt::Display;
use std::str::FromStr;

#[derive(Debug, Clone)]
pub struct SemanticVersion {
    pub major: u32,
    pub minor: u32,
    pub patch: u32,
    pub prerelease: Option<Cow<'static, str>>,
}

/// Parses a string slice to u32 at compile time.
///
/// # Panics
/// - If the string is empty
/// - If the string contains non-digit characters
const fn const_parse_u32_range(bytes: &[u8], start: usize, end: usize) -> u32 {
    if start >= end {
        panic!("Cannot parse empty range as u32");
    }

    let mut result = 0u32;
    let mut i = start;

    if bytes.is_empty() {
        panic!("Can not parse empty string as u32");
    }

    while i < end {
        let byte = bytes[i];

        if !byte.is_ascii_digit() {
            panic!("Invalid digit in version number");
        }

        // ASCII '0' - '9' to 0-9
        let digit = bytes[i] - b'0';
        result = result * 10 + digit as u32;
        i += 1;
    }

    result
}

/// Find pos of a byte in a range, return `end` if not found any
const fn find_byte_in_range(bytes: &[u8], target: u8, start: usize, end: usize) -> usize {
    let mut i = start;

    while i < end {
        if bytes[i] == target {
            return i;
        }
        i += 1;
    }

    end
}

/// Find pos of a byte in a slice, return bytes.len() if not found any
const fn find_byte_pos_or_len(bytes: &[u8], target: u8) -> usize {
    let mut i = 0;

    while i < bytes.len() {
        if bytes[i] == target {
            return i;
        }
        i += 1;
    }

    bytes.len()
}

/// Extract substring in const context
const fn const_str_slice(s: &str, start: usize, end: usize) -> &str {
    let bytes = s.as_bytes();

    if start > end {
        panic!("Start index must be less than or equal to end index");
    }
    if end > bytes.len() {
        panic!("End index out of bounds");
    }

    // SAFETY: Creating a slice within the bound of original byte slice.
    let slice = unsafe { core::slice::from_raw_parts(bytes.as_ptr().add(start), end - start) };

    match core::str::from_utf8(slice) {
        Ok(substr) => substr,
        Err(_) => panic!("Invalid UTF-8 in version string"),
    }
}

impl FromStr for SemanticVersion {
    type Err = IggyError;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        // Split on '+' to separate build metadata (which we ignore)
        let version_core = s.split('+').next().unwrap();

        // Split on '-' to separate prerelease identifier
        let mut parts = version_core.split('-');
        let version_numbers = parts.next().unwrap();
        let prerelease = parts.next().map(|s| Cow::Owned(s.to_string()));

        // Parse major.minor.patch
        let mut version = version_numbers.split('.');
        let major = version
            .next()
            .ok_or(IggyError::InvalidVersion(s.to_string()))?
            .parse::<u32>()
            .map_err(|_| IggyError::InvalidNumberValue)?;
        let minor = version
            .next()
            .ok_or(IggyError::InvalidVersion(s.to_string()))?
            .parse::<u32>()
            .map_err(|_| IggyError::InvalidNumberValue)?;
        let patch = version
            .next()
            .ok_or(IggyError::InvalidVersion(s.to_string()))?
            .parse::<u32>()
            .map_err(|_| IggyError::InvalidNumberValue)?;

        Ok(SemanticVersion {
            major,
            minor,
            patch,
            prerelease,
        })
    }
}

impl SemanticVersion {
    pub const fn parse_const(s: &'static str) -> Self {
        let bytes = s.as_bytes();

        // Split on '+' to ignore build metadata
        let core_end = find_byte_pos_or_len(bytes, b'+');

        // Split on '-' to separate prerelease
        let dash_pos = find_byte_in_range(bytes, b'-', 0, core_end);
        let version_end = if dash_pos < core_end {
            dash_pos
        } else {
            core_end
        };

        // Split version number on '.'
        let first_dot = find_byte_in_range(bytes, b'.', 0, version_end);
        let second_dot = find_byte_in_range(bytes, b'.', first_dot + 1, version_end);

        // Parse major.minor.patch
        let major = const_parse_u32_range(bytes, 0, first_dot);
        let minor = const_parse_u32_range(bytes, first_dot + 1, second_dot);
        let patch = const_parse_u32_range(bytes, second_dot + 1, version_end);

        // Extract prerelease if it present
        let prerelease = if dash_pos < core_end {
            Some(Cow::Borrowed(const_str_slice(s, dash_pos + 1, core_end)))
        } else {
            None
        };

        Self {
            major,
            minor,
            patch,
            prerelease,
        }
    }

    #[must_use]
    pub fn is_equal_to(&self, other: &SemanticVersion) -> bool {
        self.major == other.major && self.minor == other.minor && self.patch == other.patch
    }

    pub fn is_greater_than(&self, other: &SemanticVersion) -> bool {
        if self.major > other.major {
            return true;
        }
        if self.major < other.major {
            return false;
        }

        if self.minor > other.minor {
            return true;
        }
        if self.minor < other.minor {
            return false;
        }

        if self.patch > other.patch {
            return true;
        }
        if self.patch < other.patch {
            return false;
        }

        false
    }

    pub fn get_numeric_version(&self) -> Result<u32, IggyError> {
        let major = self.major;
        let minor = format!("{:03}", self.minor);
        let patch = format!("{:03}", self.patch);
        if let Ok(version) = format!("{major}{minor}{patch}").parse::<u32>() {
            return Ok(version);
        }

        Err(IggyError::InvalidVersion(self.to_string()))
    }
}

impl Display for SemanticVersion {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)?;
        if let Some(ref prerelease) = self.prerelease {
            write!(f, "-{prerelease}")?;
        }
        Ok(())
    }
}

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

    #[test]
    fn test_find_byte_pos_or_len() {
        let bytes = b"1.2.3-beta+build";
        assert_eq!(find_byte_pos_or_len(bytes, b'.'), 1);
        assert_eq!(find_byte_pos_or_len(bytes, b'-'), 5);
        assert_eq!(find_byte_pos_or_len(bytes, b'+'), 10);
        assert_eq!(find_byte_pos_or_len(bytes, b'y'), bytes.len()); // Not found
    }

    #[test]
    fn test_find_byte_in_range() {
        let bytes = b"1.2.3-beta";
        assert_eq!(find_byte_in_range(bytes, b'.', 0, 5), 1);
        assert_eq!(find_byte_in_range(bytes, b'.', 2, 5), 3);
        assert_eq!(find_byte_in_range(bytes, b'.', 4, 5), 5); // Not found in range
        assert_eq!(find_byte_in_range(bytes, b'-', 0, 10), 5);
    }

    #[test]
    fn test_const_parse_u32_range() {
        let bytes = b"123";
        assert_eq!(const_parse_u32_range(bytes, 0, 3), 123);

        let bytes = b"1.52.999";
        assert_eq!(const_parse_u32_range(bytes, 0, 1), 1);
        assert_eq!(const_parse_u32_range(bytes, 2, 4), 52);
        assert_eq!(const_parse_u32_range(bytes, 5, 8), 999);
    }

    #[test]
    #[should_panic(expected = "Cannot parse empty range as u32")]
    fn test_const_parse_u32_range_empty() {
        let bytes = b"123";
        const_parse_u32_range(bytes, 1, 1);
    }

    #[test]
    #[should_panic(expected = "Invalid digit in version number")]
    fn test_const_parse_u32_range_invalid() {
        let bytes = b"12a";
        const_parse_u32_range(bytes, 0, 3);
    }

    #[test]
    fn test_const_str_slice() {
        let s = "1.2.3-beta+build";
        assert_eq!(const_str_slice(s, 0, 5), "1.2.3");
        assert_eq!(const_str_slice(s, 6, 10), "beta");
        assert_eq!(const_str_slice(s, 11, 16), "build");
        assert_eq!(const_str_slice(s, 0, 0), "");
    }

    #[test]
    fn should_parse_semver_on_compile_time() {
        const SEMVER: SemanticVersion = SemanticVersion::parse_const("1.0.0-beta");
        assert_eq!(SEMVER.major, 1);
        assert_eq!(SEMVER.minor, 0);
        assert_eq!(SEMVER.patch, 0);
        assert_eq!(SEMVER.prerelease, Some(Cow::Borrowed("beta")));

        const SEMVER_1: SemanticVersion = SemanticVersion::parse_const("2.1.5-rc.1");
        assert_eq!(SEMVER_1.major, 2);
        assert_eq!(SEMVER_1.minor, 1);
        assert_eq!(SEMVER_1.patch, 5);
        assert_eq!(SEMVER_1.prerelease, Some(Cow::Borrowed("rc.1")));

        const SEMVER_2: SemanticVersion = SemanticVersion::parse_const("1.2.3+build.123");
        assert_eq!(SEMVER_2.major, 1);
        assert_eq!(SEMVER_2.minor, 2);
        assert_eq!(SEMVER_2.patch, 3);
        assert_eq!(SEMVER_2.prerelease, None);

        const SEMVER_3: SemanticVersion = SemanticVersion::parse_const("3.2.1-alpha.2+build.456");
        assert_eq!(SEMVER_3.major, 3);
        assert_eq!(SEMVER_3.minor, 2);
        assert_eq!(SEMVER_3.patch, 1);
        assert_eq!(SEMVER_3.prerelease, Some(Cow::Borrowed("alpha.2")));
    }

    #[test]
    fn should_parse_basic_semantic_version() {
        let version = "1.2.3".parse::<SemanticVersion>().unwrap();
        assert_eq!(version.major, 1);
        assert_eq!(version.minor, 2);
        assert_eq!(version.patch, 3);
        assert_eq!(version.prerelease, None);
        assert_eq!(version.to_string(), "1.2.3");
    }

    #[test]
    fn should_parse_semantic_version_with_prerelease() {
        let version = "0.6.0-rc1".parse::<SemanticVersion>().unwrap();
        assert_eq!(version.major, 0);
        assert_eq!(version.minor, 6);
        assert_eq!(version.patch, 0);
        assert_eq!(version.prerelease, Some(Cow::Borrowed("rc1")));
        assert_eq!(version.to_string(), "0.6.0-rc1");
    }

    #[test]
    fn should_parse_semantic_version_with_alpha() {
        let version = "2.0.0-alpha.1".parse::<SemanticVersion>().unwrap();
        assert_eq!(version.major, 2);
        assert_eq!(version.minor, 0);
        assert_eq!(version.patch, 0);
        assert_eq!(version.prerelease, Some(Cow::Borrowed("alpha.1")));
        assert_eq!(version.to_string(), "2.0.0-alpha.1");
    }

    #[test]
    fn should_parse_semantic_version_with_build_metadata() {
        let version = "1.0.0+20130313144700".parse::<SemanticVersion>().unwrap();
        assert_eq!(version.major, 1);
        assert_eq!(version.minor, 0);
        assert_eq!(version.patch, 0);
        assert_eq!(version.prerelease, None);
        // Build metadata is not included in the display
        assert_eq!(version.to_string(), "1.0.0");
    }

    #[test]
    fn should_parse_semantic_version_with_prerelease_and_build_metadata() {
        let version = "1.0.0-beta+exp.sha.5114f85"
            .parse::<SemanticVersion>()
            .unwrap();
        assert_eq!(version.major, 1);
        assert_eq!(version.minor, 0);
        assert_eq!(version.patch, 0);
        assert_eq!(version.prerelease, Some(Cow::Borrowed("beta")));
        assert_eq!(version.to_string(), "1.0.0-beta");
    }

    #[test]
    fn should_compare_versions_correctly() {
        let v1 = "1.0.0".parse::<SemanticVersion>().unwrap();
        let v2 = "1.0.0-rc1".parse::<SemanticVersion>().unwrap();
        let v3 = "2.0.0".parse::<SemanticVersion>().unwrap();

        assert!(v1.is_equal_to(&v2)); // Prerelease is ignored in comparison
        assert!(!v1.is_equal_to(&v3));
        assert!(v3.is_greater_than(&v1));
        assert!(!v1.is_greater_than(&v3));
    }
}