terraform-version 0.4.0

Parser and match calculator for terraform version constraint syntax
Documentation
//!`terraform-version` is a short parser and match calculator for terraform version constraint syntax.
//!
//! It follows the [terraform semantic constraints](https://developer.hashicorp.com/terraform/language/expressions/version-constraints).
//!
//! *Compiler support: requires rustc 1.67+*
//!
//!
//! ## Example
//!
//! ```rust
//! # use terraform_version::{Version, VersionRequirement, NumericIdentifiers};
//!     let version_req = VersionRequirement::parse("< 5.4.3, >= 1.2.3").unwrap();
//!
//!     let version = Version::parse("1.2.3").unwrap();
//!     assert!(version.matches(&version_req));
//!
//!     let version = Version::parse("5.4.4").unwrap();
//!     assert!(!version.matches(&version_req));
//!
//!
//!     let version_req = VersionRequirement::parse("= 1.2.3-beta").unwrap();
//!
//!     let version = Version::parse("1.2.3-beta").unwrap();
//!     assert!(version.matches(&version_req));
//!
//!     let version = Version {
//!         numeric_identifiers: NumericIdentifiers::new(vec![1, 2, 3]),
//!         suffix: None
//!     };
//!     assert!(!version.matches(&version_req));
//!
//! ```
//!
//! ## License
//!
//! `terraform-version` is provided under the MIT license. See [LICENSE](./LICENSE).

use std::{cmp, fmt};

pub use error::Error;
use error::Result;

mod error;

#[derive(Clone, PartialEq, Eq, Debug)]
pub struct Version {
    /// Series of numbers, usually representing major, minor and patch (when semantic versioning is respected).
    pub numeric_identifiers: NumericIdentifiers,
    /// Equivalent of prerelease and/or build metadata in semantic versioning syntax.
    pub suffix: Option<String>,
}

impl Version {
    /// Try to create `Version` from given string.
    /// If the string contains a `-` character, it is split in two, the first part will be considered as the numeric identifier and the 2nd one as suffix.
    pub fn parse(text: &str) -> Result<Self> {
        let vec: Vec<&str> = text.splitn(2, '-').collect();

        if vec == [""] {
            return Err(Error::NoVersion);
        }

        let numeric_identifiers = NumericIdentifiers::parse(vec.first().ok_or(Error::NoVersion)?)?;

        let suffix = vec.get(1).map(|pr| pr.to_string());

        Ok(Self {
            numeric_identifiers,
            suffix,
        })
    }

    /// Evaluate whether `self` satisfies the given `VersionRequirement`.
    pub fn matches(&self, vr: &VersionRequirement) -> bool {
        vr.comparators.iter().all(|c| self.matches_comparators(c))
    }

    fn matches_comparators(&self, cmp: &Comparator) -> bool {
        let useful_len = cmp.version.numeric_identifiers.0.len();
        let useful_lhs = &self.numeric_identifiers.0[..useful_len];
        let useful_rhs = cmp.version.numeric_identifiers.0.as_slice();

        match cmp.operator.unwrap_or(Operator::Exact) {
            Operator::Exact => self == &cmp.version,
            Operator::Different => self != &cmp.version,
            Operator::Greater => useful_lhs > useful_rhs,
            Operator::GreaterEq => useful_lhs >= useful_rhs,
            Operator::Less => useful_lhs < useful_rhs,
            Operator::LessEq => useful_lhs <= useful_rhs,
            Operator::RightMost => {
                let prefix_len = useful_rhs.len() - 1;
                let prefix_lhs = &useful_lhs[..prefix_len];
                let prefix_rhs = &useful_rhs[..prefix_len];

                (useful_lhs >= useful_rhs) && (prefix_lhs == prefix_rhs)
            }
        }
    }
}

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

impl cmp::Ord for Version {
    fn cmp(&self, other: &Self) -> cmp::Ordering {
        self.numeric_identifiers.cmp(&other.numeric_identifiers)
    }
}

impl fmt::Display for Version {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let pr = match &self.suffix {
            None => "".to_string(),
            Some(suffix) => format!("-{}", suffix),
        };
        write!(f, "{}{pr}", self.numeric_identifiers)
    }
}

/// Version requirement describing the intersection of some version comparators, such as `>= 1.2.3, != 1.3.0`.
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct VersionRequirement {
    pub comparators: Vec<Comparator>,
}

impl VersionRequirement {
    /// Try to create a `VersionRequirement` from given string.
    /// Each `Comparator` is separated  by a `,` character.
    pub fn parse(text: &str) -> Result<Self> {
        let vec: Vec<&str> = text.split(',').map(|s| s.trim()).collect();
        if vec == [""] {
            return Err(Error::NoVersionRequirement);
        }

        let comparators: Vec<Comparator> = vec
            .iter()
            .map(|comp| Comparator::parse(comp))
            .collect::<Result<Vec<Comparator>>>()?;

        if comparators.len() > 1
            && comparators
                .iter()
                .any(|c| c.operator == Some(Operator::Exact) || c.operator.is_none())
        {
            return Err(Error::NotAllowedOperatorWithMultipleComparators(
                text.to_string(),
            ));
        }

        Ok(Self { comparators })
    }

    /// Returns true if self has a single `Comparator` without `Operator`
    pub fn is_without_operator(&self) -> bool {
        match &self.comparators[..] {
            [item] => item.operator.is_none(),
            _ => false,
        }
    }
}

#[derive(Clone, PartialEq, Eq, Debug)]
pub struct Comparator {
    /// None is considered like "Exact" Operator for matching
    pub operator: Option<Operator>,
    pub version: Version,
}

impl Comparator {
    fn parse(text: &str) -> Result<Self> {
        let Some((operator, version)) = Comparator::split_and_parse_operator(text) else {
            return Err(Error::InvalidOperator(text.to_string()));
        };
        let version = Version::parse(version)?;

        match operator {
            Some(op)
                if version.suffix.is_some()
                    && op != Operator::Exact
                    && op != Operator::Different =>
            {
                return Err(Error::NotAllowedOperatorWithSuffix(op));
            }
            _ => {}
        }

        Ok(Self { operator, version })
    }

    #[allow(clippy::manual_map)]
    fn split_and_parse_operator(text: &str) -> Option<(Option<Operator>, &str)> {
        if let Some(rest) = text.strip_prefix("<=") {
            Some((Some(Operator::LessEq), rest.trim_start()))
        } else if let Some(rest) = text.strip_prefix(">=") {
            Some((Some(Operator::GreaterEq), rest.trim_start()))
        } else if let Some(rest) = text.strip_prefix("!=") {
            Some((Some(Operator::Different), rest.trim_start()))
        } else if let Some(rest) = text.strip_prefix("~>") {
            Some((Some(Operator::RightMost), rest.trim_start()))
        } else if let Some(rest) = text.strip_prefix('<') {
            Some((Some(Operator::Less), rest.trim_start()))
        } else if let Some(rest) = text.strip_prefix('>') {
            Some((Some(Operator::Greater), rest.trim_start()))
        } else if let Some(rest) = text.strip_prefix('=') {
            Some((Some(Operator::Exact), rest.trim_start()))
        } else if let Some(first) = text.trim_start().chars().next() {
            if first.is_ascii_digit() {
                Some((None, text.trim_start()))
            } else {
                None
            }
        } else {
            None
        }
    }
}

#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct NumericIdentifiers(Vec<u32>);

impl NumericIdentifiers {
    pub fn new(vec: Vec<u32>) -> NumericIdentifiers {
        NumericIdentifiers(vec)
    }

    pub fn parse(text: &str) -> Result<Self> {
        let nums = text
            .split('.')
            .map(|ni| {
                ni.parse::<u32>()
                    .map_err(|err| Error::ImpossibleNumericIdentifierParsing {
                        err,
                        text: text.into(),
                        ni: ni.into(),
                    })
            })
            .collect::<Result<Vec<_>>>()?;

        Ok(Self(nums))
    }
}

impl fmt::Display for NumericIdentifiers {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        self.0
            .iter()
            .map(|integer| integer.to_string())
            .collect::<Vec<String>>()
            .join(".")
            .fmt(f)
    }
}

#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum Operator {
    /// operator `=` Allows only one exact version number. Cannot be combined with other conditions.
    Exact,
    /// operator `!=` : Excludes an exact version number.
    Different,
    /// operator `>` : Comparisons against a specified version, allowing versions for which the comparison is true. "Greater" requests newer versions.
    Greater,
    /// operator `>=` : Comparisons against a specified version, allowing versions for which the comparison is true.
    GreaterEq,
    /// operator `<` : Comparisons against a specified version, allowing versions for which the comparison is true. "Less" requests older versions.
    Less,
    /// operator `<=` : Comparisons against a specified version, allowing versions for which the comparison is true.
    LessEq,
    /// operator `~>` : Allows only the rightmost version component to increment. For example, to allow new patch releases within a specific minor release, use the full version number: ~> 1.0.4 will allow installation of 1.0.5 and 1.0.10 but not 1.1.0.
    RightMost,
}

impl fmt::Display for Operator {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        fmt::Debug::fmt(self, f)
    }
}