waxpkg 0.15.9

Fast Homebrew-compatible package manager
use std::cmp::Ordering;

pub const WAX_VERSION: &str = env!("CARGO_PKG_VERSION");

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BrewVersion {
    pub base: String,
    pub revision: u32,
}

impl BrewVersion {
    pub fn parse(version: &str) -> Self {
        if let Some(idx) = version.rfind('_') {
            let (base, rev_str) = version.split_at(idx);
            let rev_str = &rev_str[1..];
            if let Ok(revision) = rev_str.parse::<u32>() {
                return BrewVersion {
                    base: base.to_string(),
                    revision,
                };
            }
        }
        BrewVersion {
            base: version.to_string(),
            revision: 0,
        }
    }

    fn parse_semver_parts(s: &str) -> Vec<u64> {
        s.split('.')
            .filter_map(|part| {
                let numeric: String = part.chars().take_while(|c| c.is_ascii_digit()).collect();
                numeric.parse().ok()
            })
            .collect()
    }
}

impl Ord for BrewVersion {
    fn cmp(&self, other: &Self) -> Ordering {
        let self_parts = Self::parse_semver_parts(&self.base);
        let other_parts = Self::parse_semver_parts(&other.base);

        let max_len = self_parts.len().max(other_parts.len());
        for i in 0..max_len {
            let a = self_parts.get(i).copied().unwrap_or(0);
            let b = other_parts.get(i).copied().unwrap_or(0);
            match a.cmp(&b) {
                Ordering::Equal => continue,
                ord => return ord,
            }
        }

        if self_parts.len() != other_parts.len() || self.base != other.base {
            return self.base.cmp(&other.base);
        }

        self.revision.cmp(&other.revision)
    }
}

impl PartialOrd for BrewVersion {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

pub fn is_same_or_newer(installed: &str, latest: &str) -> bool {
    let installed_v = BrewVersion::parse(installed);
    let latest_v = BrewVersion::parse(latest);
    installed_v >= latest_v
}

pub fn sort_versions(versions: &mut [String]) {
    versions.sort_by(|a, b| {
        let va = BrewVersion::parse(a);
        let vb = BrewVersion::parse(b);
        va.cmp(&vb)
    });
}

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

    #[test]
    fn test_parse_simple_version() {
        let v = BrewVersion::parse("2.52.0");
        assert_eq!(v.base, "2.52.0");
        assert_eq!(v.revision, 0);
    }

    #[test]
    fn test_parse_revision() {
        let v = BrewVersion::parse("2.52.0_1");
        assert_eq!(v.base, "2.52.0");
        assert_eq!(v.revision, 1);
    }

    #[test]
    fn test_revision_is_newer() {
        let v1 = BrewVersion::parse("2.52.0");
        let v2 = BrewVersion::parse("2.52.0_1");
        assert!(v2 > v1, "2.52.0_1 should be newer than 2.52.0");
    }

    #[test]
    fn test_higher_revision_is_newer() {
        let v1 = BrewVersion::parse("2.52.0_1");
        let v2 = BrewVersion::parse("2.52.0_2");
        assert!(v2 > v1, "2.52.0_2 should be newer than 2.52.0_1");
    }

    #[test]
    fn test_semver_comparison() {
        let v1 = BrewVersion::parse("2.51.0");
        let v2 = BrewVersion::parse("2.52.0");
        assert!(v2 > v1);
    }

    #[test]
    fn test_is_same_or_newer() {
        assert!(is_same_or_newer("2.52.0_1", "2.52.0"));
        assert!(is_same_or_newer("2.52.0", "2.52.0"));
        assert!(!is_same_or_newer("2.51.0", "2.52.0"));
    }

    #[test]
    fn test_sort_versions() {
        let mut versions = vec![
            "2.52.0".to_string(),
            "2.52.0_1".to_string(),
            "2.51.0".to_string(),
            "2.52.0_2".to_string(),
        ];
        sort_versions(&mut versions);
        assert_eq!(versions, vec!["2.51.0", "2.52.0", "2.52.0_1", "2.52.0_2"]);
    }
}