version-compare 0.0.5

A Rust library to easily compare version numbers, and test them against various comparison operators.
//! Version module, which provides the `Version` struct as parsed version representation.
//!
//! Version numbers in the form of a string are parsed to a `Version` first, before any comparison
//! is made. This struct provides many methods and features for easy comparison, probing and other
//! things.

use std::cmp::Ordering;
use std::iter::Peekable;
use std::slice::Iter;

use comp_op::CompOp;
use version_manifest::VersionManifest;
use version_part::VersionPart;

/// Version struct, which is a representation for a parsed version string.
///
/// A version in string format can be parsed using methods like `Version::from("1.2.3");`.
/// These methods return a `Result` holding the parsed version or an error on failure.
///
/// The original version string is stored in the struct, and can be accessed using the
/// `version.as_str()` method. Note, that when the version wasn't parsed from a string
/// representation, the returned value is generated.
///
/// The struct provides many methods for comparison and probing.
pub struct Version<'a> {
    version: &'a str,
    parts: Vec<VersionPart<'a>>,
    manifest: Option<&'a VersionManifest>,
}

impl<'a> Version<'a> {

    /// Create a `Version` instance from a version string.
    ///
    /// The version string should be passed to the `version` parameter.
    ///
    /// # Examples
    ///
    /// ```
    /// use version_compare::{CompOp, Version};
    ///
    /// let ver = Version::from("1.2.3").unwrap();
    ///
    /// assert_eq!(ver.compare(&Version::from("1.2.3").unwrap()), CompOp::Eq);
    /// ```
    pub fn from(version: &'a str) -> Option<Self> {
        // Split the version string
        let parts = Self::split_version_str(version, None);

        // Return nothing if the parts are none
        if parts.is_none() {
            return None;
        }

        // Create and return the object
        Some(Version {
            version: version,
            parts: parts.unwrap(),
            manifest: None,
        })
    }

    /// Create a `Version` instance from a version string with the given `manifest`.
    ///
    /// The version string should be passed to the `version` parameter.
    ///
    /// # Examples
    ///
    /// ```
    /// use version_compare::{CompOp, Version, VersionManifest};
    ///
    /// let manifest = VersionManifest::new();
    /// let ver = Version::from_manifest("1.2.3", &manifest).unwrap();
    ///
    /// assert_eq!(ver.compare(&Version::from("1.2.3").unwrap()), CompOp::Eq);
    /// ```
    pub fn from_manifest(version: &'a str, manifest: &'a VersionManifest) -> Option<Self> {
        // Split the version string
        let parts = Self::split_version_str(version, Some(&manifest));

        // Return nothing if the parts are none
        if parts.is_none() {
            return None;
        }

        // Create and return the object
        Some(Version {
            version: version,
            parts: parts.unwrap(),
            manifest: Some(&manifest),
        })
    }

    /// Get the version manifest, if available.
    ///
    /// # Examples
    ///
    /// ```
    /// use version_compare::Version;
    ///
    /// let version = Version::from("1.2.3").unwrap();
    ///
    /// if version.has_manifest() {
    ///     println!(
    ///         "Maximum version part depth is {} for this version",
    ///         version.manifest().unwrap().max_depth_number()
    ///     );
    /// } else {
    ///     println!("Version has no manifest");
    /// }
    /// ```
    pub fn manifest(&self) -> Option<&VersionManifest> {
        self.manifest
    }

    /// Check whether this version has a manifest.
    ///
    /// # Examples
    ///
    /// ```
    /// use version_compare::Version;
    ///
    /// let version = Version::from("1.2.3").unwrap();
    ///
    /// if version.has_manifest() {
    ///     println!("This version does have a manifest");
    /// } else {
    ///     println!("This version does not have a manifest");
    /// }
    /// ```
    pub fn has_manifest(&self) -> bool {
        self.manifest().is_some()
    }

    /// Set the version manifest.
    ///
    /// # Examples
    ///
    /// ```
    /// use version_compare::{Version, VersionManifest};
    ///
    /// let manifest = VersionManifest::new();
    /// let mut version = Version::from("1.2.3").unwrap();
    ///
    /// version.set_manifest(Some(&manifest));
    /// ```
    pub fn set_manifest(&mut self, manifest: Option<&'a VersionManifest>) {
        self.manifest = manifest;

        // TODO: Re-parse the version string, because the manifest might have changed.
    }

    /// Split the given version string, in it's version parts.
    /// TODO: Move this method to some sort of helper class, maybe as part of `VersionPart`.
    fn split_version_str(version: &'a str, manifest: Option<&'a VersionManifest>) -> Option<Vec<VersionPart<'a>>> {
        // Split the version string, and create a vector to put the parts in
        let split = version.split('.');
        let mut parts = Vec::new();

        // Get the manifest to follow
        let mut used_manifest = &VersionManifest::new();
        if manifest.is_some() {
            used_manifest = manifest.unwrap();
        }

        // Flag to determine whether this version number contains any number part
        let mut has_number = false;

        // Loop over the parts, and parse them
        for part in split {
            // We may not go over the maximum depth
            if used_manifest.max_depth().is_some() && parts.len() >= used_manifest.max_depth_number() {
                break;
            }

            // Skip empty parts
            if part.is_empty() {
                continue;
            }

            // Try to parse the value as an number
            match part.parse::<i32>() {
                Ok(number) => {
                    // Push the number part to the vector, and set the has number flag
                    parts.push(VersionPart::Number(number));
                    has_number = true;
                },
                Err(_) => {
                    // Ignore text parts if specified
                    if used_manifest.ignore_text() {
                        continue;
                    }

                    // Push the text part to the vector
                    parts.push(VersionPart::Text(part))
                },
            }
        }

        // The version must contain a number part, if any part was parsed
        if !has_number && !parts.is_empty() {
            return None
        }

        // Return the list of parts
        Some(parts)
    }

    /// Get the original version string.
    ///
    /// # Examples
    ///
    /// ```
    /// use version_compare::Version;
    ///
    /// let ver = Version::from("1.2.3").unwrap();
    ///
    /// assert_eq!(ver.as_str(), "1.2.3");
    /// ```
    pub fn as_str(&self) -> &str {
        &self.version
    }

    /// Get a specific version part by it's `index`.
    /// An error is returned if the given index is out of bound.
    ///
    /// # Examples
    ///
    /// ```
    /// use version_compare::{Version, VersionPart};
    ///
    /// let ver = Version::from("1.2.3").unwrap();
    ///
    /// assert_eq!(ver.part(0), Ok(&VersionPart::Number(1)));
    /// assert_eq!(ver.part(1), Ok(&VersionPart::Number(2)));
    /// assert_eq!(ver.part(2), Ok(&VersionPart::Number(3)));
    /// ```
    pub fn part(&self, index: usize) -> Result<&VersionPart<'a>, ()> {
        // Make sure the index is in-bound
        if index >= self.parts.len() {
            return Err(());
        }

        // Return the requested part
        Ok(&self.parts[index])
    }

    /// Get a vector of all version parts.
    ///
    /// # Examples
    ///
    /// ```
    /// use version_compare::{Version, VersionPart};
    ///
    /// let ver = Version::from("1.2.3").unwrap();
    ///
    /// assert_eq!(ver.parts(), &vec![
    ///     VersionPart::Number(1),
    ///     VersionPart::Number(2),
    ///     VersionPart::Number(3)
    /// ]);
    /// ```
    pub fn parts(&self) -> &Vec<VersionPart<'a>> {
        &self.parts
    }

    /// Get the number of parts in this version string.
    ///
    /// # Examples
    ///
    /// ```
    /// use version_compare::Version;
    ///
    /// let ver_a = Version::from("1.2.3").unwrap();
    /// let ver_b = Version::from("1.2.3.4").unwrap();
    ///
    /// assert_eq!(ver_a.part_count(), 3);
    /// assert_eq!(ver_b.part_count(), 4);
    /// ```
    pub fn part_count(&self) -> usize {
        self.parts.len()
    }

    /// Compare this version to the given `other` version.
    ///
    /// This method returns one of the following comparison operators:
    ///
    /// * `Lt`
    /// * `Eq`
    /// * `Gt`
    ///
    /// Other comparison operators can be used when comparing, but aren't returned by this method.
    ///
    /// # Examples:
    ///
    /// ```
    /// use version_compare::{CompOp, Version};
    ///
    /// assert_eq!(Version::from("1.2").unwrap().compare(&Version::from("1.3.2").unwrap()), CompOp::Lt);
    /// assert_eq!(Version::from("1.9").unwrap().compare(&Version::from("1.9").unwrap()), CompOp::Eq);
    /// assert_eq!(Version::from("0.3.0.0").unwrap().compare(&Version::from("0.3").unwrap()), CompOp::Eq);
    /// assert_eq!(Version::from("2").unwrap().compare(&Version::from("1.7.3").unwrap()), CompOp::Gt);
    /// ```
    pub fn compare(&self, other: &Version) -> CompOp {
        // Compare the versions with their peekable iterators
        Self::compare_iter(
            self.parts.iter().peekable(),
            other.parts.iter().peekable(),
        )
    }

    /// Compare this version to the given `other` version,
    /// and check whether the given comparison operator is valid.
    ///
    /// All comparison operators can be used.
    ///
    /// # Examples:
    ///
    /// ```
    /// use version_compare::{CompOp, Version};
    ///
    /// assert!(Version::from("1.2").unwrap().compare_to(&Version::from("1.3.2").unwrap(), &CompOp::Lt));
    /// assert!(Version::from("1.2").unwrap().compare_to(&Version::from("1.3.2").unwrap(), &CompOp::Le));
    /// assert!(Version::from("1.2").unwrap().compare_to(&Version::from("1.2").unwrap(), &CompOp::Eq));
    /// assert!(Version::from("1.2").unwrap().compare_to(&Version::from("1.2").unwrap(), &CompOp::Le));
    /// ```
    pub fn compare_to(&self, other: &Version, operator: &CompOp) -> bool {
        // Get the comparison result
        let result = self.compare(&other);

        // Match the result against the given operator
        match result {
            CompOp::Eq =>
                match operator {
                    &CompOp::Eq | &CompOp::Le | &CompOp::Ge => true,
                    _ => false,
                },
            CompOp::Lt =>
                match operator {
                    &CompOp::Ne | &CompOp::Lt | &CompOp::Le => true,
                    _ => false,
                },
            CompOp::Gt =>
                match operator {
                    &CompOp::Ne | &CompOp::Gt | &CompOp::Ge => true,
                    _ => false,
                },

            _ => unreachable!(),
        }
    }

    /// Compare two version numbers based on the iterators of their version parts.
    ///
    /// This method returns one of the following comparison operators:
    ///
    /// * `Lt`
    /// * `Eq`
    /// * `Gt`
    ///
    /// Other comparison operators can be used when comparing, but aren't returned by this method.
    fn compare_iter(mut iter: Peekable<Iter<VersionPart<'a>>>, mut other_iter: Peekable<Iter<VersionPart<'a>>>) -> CompOp {
        // Iterate through the parts of this version
        let mut other_part: Option<&VersionPart>;

        // Iterate over the iterator, without consuming it
        loop {
            match iter.next() {
                Some(part) => {
                    // Skip this part if it's non-numeric
                    match part {
                        &VersionPart::Number(_) => {},
                        _ => continue,
                    }

                    // Get the next numerical part for the other version
                    loop {
                        // Get the next other part
                        other_part = other_iter.next();

                        // Make sure it's a number or none
                        match other_part {
                            Some(val) =>
                                match val {
                                    &VersionPart::Number(_) => break,
                                    _ => {},
                                },
                            None => break,
                        }
                    }

                    // If there are no parts left in the other version, try to determine the result
                    if other_part.is_none() {
                        // In the main version: if the current part is zero, continue to the next one
                        match part {
                            &VersionPart::Number(num) =>
                                if num == 0 {
                                    continue;
                                },
                            _ => {},
                        }

                        // The main version is greater
                        return CompOp::Gt;
                    }

                    // Match both part as numbers to destruct their numerical values
                    match part {
                        &VersionPart::Number(num) =>
                            match other_part.unwrap() {
                                &VersionPart::Number(other_num) => {
                                    // Compare the numbers
                                    match num {
                                        n if n < other_num => return CompOp::Lt,
                                        n if n > other_num => return CompOp::Gt,
                                        _ => continue,
                                    }
                                },

                                _ => unreachable!(),
                            },

                        _ => unreachable!(),
                    }
                },
                None => break,
            }
        }

        // Check whether we should iterate over the other iterator, if it has any items left
        match other_iter.peek() {
            // Compare based on the other iterator
            Some(_) => Self::compare_iter(other_iter, iter).as_flipped(),

            // Nothing more to iterate over, the versions should be equal
            None => CompOp::Eq,
        }
    }
}

/// Implement the partial ordering trait for the version struct, to easily allow version comparison.
impl<'a> PartialOrd for Version<'a> {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.compare(other).ord().unwrap())
    }
}

/// Implement the partial equality trait for the version struct, to easily allow version comparison.
impl<'a> PartialEq for Version<'a> {
    fn eq(&self, other: &Self) -> bool {
        self.compare_to(other, &CompOp::Eq)
    }
}

#[cfg(test)]
mod tests {
    use std::cmp;

    use comp_op::CompOp;
    use test::test_version::{TEST_VERSIONS, TEST_VERSIONS_ERROR};
    use test::test_version_set::TEST_VERSION_SETS;
    use version_manifest::VersionManifest;
    use version_part::VersionPart;

    use super::Version;

    #[test]
    // TODO: This doesn't really test whether this method fully works
    fn from() {
        // Test whether parsing works for each test version
        for version in TEST_VERSIONS {
            assert!(Version::from(&version.0).is_some());
        }

        // Test whether parsing works for each test invalid version
        for version in TEST_VERSIONS_ERROR {
            assert!(Version::from(&version.0).is_none());
        }
    }

    #[test]
    // TODO: This doesn't really test whether this method fully works
    fn from_manifest() {
        // Create a manifest
        let manifest = VersionManifest::new();

        // Test whether parsing works for each test version
        for version in TEST_VERSIONS {
            assert_eq!(Version::from_manifest(&version.0, &manifest).unwrap().manifest, Some(&manifest));
        }

        // Test whether parsing works for each test invalid version
        for version in TEST_VERSIONS_ERROR {
            assert!(Version::from_manifest(&version.0, &manifest).is_none());
        }
    }

    #[test]
    fn manifest() {
        let manifest = VersionManifest::new();
        let mut version = Version::from("1.2.3").unwrap();

        version.manifest = Some(&manifest);
        assert_eq!(version.manifest(), Some(&manifest));

        version.manifest = None;
        assert_eq!(version.manifest(), None);
    }

    #[test]
    fn has_manifest() {
        let manifest = VersionManifest::new();
        let mut version = Version::from("1.2.3").unwrap();

        version.manifest = Some(&manifest);
        assert!(version.has_manifest());

        version.manifest = None;
        assert!(!version.has_manifest());
    }

    #[test]
    fn set_manifest() {
        let manifest = VersionManifest::new();
        let mut version = Version::from("1.2.3").unwrap();

        version.set_manifest(Some(&manifest));
        assert_eq!(version.manifest, Some(&manifest));

        version.set_manifest(None);
        assert_eq!(version.manifest, None);
    }

    #[test]
    fn as_str() {
        // Test for each test version
        for version in TEST_VERSIONS {
            // The input version string must be the same as the returned string
            assert_eq!(Version::from(&version.0).unwrap().as_str(), version.0);
        }
    }

    #[test]
    fn part() {
        // Test for each test version
        for version in TEST_VERSIONS {
            // Create a version object
            let ver = Version::from(&version.0).unwrap();

            // Loop through each part
            for i in 0..version.1 {
                assert_eq!(ver.part(i), Ok(&ver.parts[i]));
            }

            // A value outside the range must return an error
            assert!(ver.part(version.1).is_err());
        }
    }

    #[test]
    fn parts() {
        // Test for each test version
        for version in TEST_VERSIONS {
            // The number of parts must match
            assert_eq!(Version::from(&version.0).unwrap().parts().len(), version.1);
        }
    }

    #[test]
    fn parts_max_depth() {
        // Create a manifest
        let mut manifest = VersionManifest::new();

        // Loop through a range of numbers
        for depth in 0..5 {
            // Set the maximum depth
            manifest.set_max_depth_number(depth);

            // Test for each test version with the manifest
            for version in TEST_VERSIONS {
                // Create a version object, and count it's parts
                let ver = Version::from_manifest(&version.0, &manifest);

                // Some versions might be none, because not all of the start with a number when the
                // maximum depth is 1. A version string with only text isn't allowed,
                // resulting in none.
                if ver.is_none() {
                    continue;
                }

                // Get the part count
                let count = ver.unwrap().parts().len();

                // The number of parts must match
                if depth == 0 {
                    assert_eq!(count, version.1);
                } else {
                    assert_eq!(count, cmp::min(version.1, depth));
                }
            }
        }
    }

    #[test]
    fn parts_ignore_text() {
        // Create a manifest
        let mut manifest = VersionManifest::new();

        // Try this for true and false
        for ignore in vec![true, false] {
            // Set to ignore text
            manifest.set_ignore_text(ignore);

            // Keep track whether any version passed with text
            let mut had_text = false;

            // Test each test version
            for version in TEST_VERSIONS {
                // Create a version instance, and get it's parts
                let ver = Version::from_manifest(&version.0, &manifest).unwrap();

                // Loop through all version parts
                for part in ver.parts() {
                    match part {
                        &VersionPart::Text(_) => {
                            // Set the flag
                            had_text = true;

                            // Break the loop if we already reached text when not ignored
                            if !ignore {
                                break;
                            }                        },
                        _ => {},
                    }
                }
            }

            // Assert had text
            assert_eq!(had_text, !ignore);
        }
    }

    #[test]
    fn part_count() {
        // Test for each test version
        for version in TEST_VERSIONS {
            // The number of parts must match the metadata
            assert_eq!(Version::from(&version.0).unwrap().part_count(), version.1);
        }
    }

    #[test]
    fn compare() {
        // Compare each version in the version set
        for entry in TEST_VERSION_SETS {
            // Get both versions
            let version_a = Version::from(&entry.0).unwrap();
            let version_b = Version::from(&entry.1).unwrap();

            // Compare them
            assert_eq!(
                version_a.compare(&version_b),
                entry.2.clone()
            );
        }
    }

    #[test]
    fn compare_to() {
        // Compare each version in the version set
        for entry in TEST_VERSION_SETS {
            // Get both versions
            let version_a = Version::from(&entry.0).unwrap();
            let version_b = Version::from(&entry.1).unwrap();

            // Test
            assert!(version_a.compare_to(&version_b, &entry.2));

            // Make sure the inverse operator is not correct
            assert_eq!(version_a.compare_to(&version_b, &entry.2.invert()), false);
        }

        // Assert an exceptional case, compare to not equal
        assert!(
            Version::from("1.2").unwrap().compare_to(
                &Version::from("1.2.3").unwrap(),
                &CompOp::Ne,
            ));
    }

    #[test]
    fn partial_cmp() {
        // Compare each version in the version set
        for entry in TEST_VERSION_SETS {
            // Get both versions
            let version_a = Version::from(&entry.0).unwrap();
            let version_b = Version::from(&entry.1).unwrap();

            // Compare and assert
            match entry.2 {
                CompOp::Eq => assert!(version_a == version_b),
                CompOp::Lt => assert!(version_a < version_b),
                CompOp::Gt => assert!(version_a > version_b),
                _ => unreachable!(),
            }
        }
    }

    #[test]
    fn partial_eq() {
        // Compare each version in the version set
        for entry in TEST_VERSION_SETS {
            // Skip entries that are less or equal, or greater or equal
            match entry.2 {
                CompOp::Le | CompOp::Ge => continue,
                _ => {}
            }

            // Get both versions
            let version_a = Version::from(&entry.0).unwrap();
            let version_b = Version::from(&entry.1).unwrap();

            // Determine what the result should be
            let result = match entry.2 {
                CompOp::Eq => true,
                _ => false,
            };

            // Test
            assert_eq!(version_a == version_b, result);
        }

        // Assert an exceptional case, compare to not equal
        assert!(Version::from("1.2").unwrap() != Version::from("1.2.3").unwrap());
    }
}