feature-check 2.0.0

Query a program for supported features
Documentation
/*
 * Copyright (c) 2021, 2022  Peter Pentchev <roam@ringlet.net>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */
//! Check whether the program's features satisfy the specified condition.

#![allow(clippy::pub_use)]

use std::cmp::Ordering;
use std::collections::HashMap;
use std::str::FromStr;

use crate::defs::{Mode, ParseError};
use crate::version::Version;

pub mod parser;

pub use crate::defs::{CalcResult, Calculable};

/// The type of a boolean comparison operation requested.
#[derive(Debug)]
enum BoolOpKind {
    /// The first version string sorts before the second one.
    LessThan,
    /// The first version string sorts before the second one or is the same.
    LessThanOrEqual,
    /// The version strings are the same.
    Equal,
    /// The first version string sorts after the second one or is the same.
    GreaterThanOrEqual,
    /// The first version string sorts after the second one.
    GreaterThan,
}

impl BoolOpKind {
    /// The symbol representing the "is less than" comparison operation.
    const LT: &'static str = "<";
    /// The symbol representing the "is less than or equal to" comparison operation.
    const LE: &'static str = "<=";
    /// The symbol representing the "is equal to" comparison operation.
    const EQ: &'static str = "=";
    /// The symbol representing the "is greater than or equal to" comparison operation.
    const GT: &'static str = ">";
    /// The symbol representing the "is greater than" comparison operation.
    const GE: &'static str = ">=";

    /// The string representing the "is less than" comparison operation.
    const LT_S: &'static str = "lt";
    /// The string representing the "is less than or equal to" comparison operation.
    const LE_S: &'static str = "le";
    /// The string representing the "is equal to" comparison operation.
    const EQ_S: &'static str = "eq";
    /// The string representing the "is greater than or equal to" comparison operation.
    const GE_S: &'static str = "ge";
    /// The string representing the "is greater than" comparison operation.
    const GT_S: &'static str = "gt";
}

impl FromStr for BoolOpKind {
    type Err = ParseError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            Self::LT | Self::LT_S => Ok(Self::LessThan),
            Self::LE | Self::LE_S => Ok(Self::LessThanOrEqual),
            Self::EQ | Self::EQ_S => Ok(Self::Equal),
            Self::GE | Self::GE_S => Ok(Self::GreaterThanOrEqual),
            Self::GT | Self::GT_S => Ok(Self::GreaterThan),
            other => Err(ParseError::InvalidComparisonOperator(other.to_owned())),
        }
    }
}

/// A boolean comparison operation with its arguments.
#[derive(Debug)]
struct BoolOp {
    /// The comparison type.
    op: BoolOpKind,
    /// The left (first) operand.
    left: Box<dyn Calculable + 'static>,
    /// The right (second) operand.
    right: Box<dyn Calculable + 'static>,
}

impl BoolOp {
    /// Construct a boolean operation object with the specified parameters.
    fn new(op: BoolOpKind, left: Box<dyn Calculable>, right: Box<dyn Calculable>) -> Self {
        Self { op, left, right }
    }
}

impl Calculable for BoolOp {
    fn get_value(&self, features: &HashMap<String, Version>) -> Result<CalcResult, ParseError> {
        let left = self.left.get_value(features)?;
        let right = self.right.get_value(features)?;
        if let CalcResult::Version(ver_left) = left {
            if let CalcResult::Version(ver_right) = right {
                let ncomp = ver_left.cmp(&ver_right);
                match self.op {
                    BoolOpKind::LessThan => Ok(CalcResult::Bool(ncomp == Ordering::Less)),
                    BoolOpKind::LessThanOrEqual => Ok(CalcResult::Bool(ncomp != Ordering::Greater)),
                    BoolOpKind::Equal => Ok(CalcResult::Bool(ncomp == Ordering::Equal)),
                    BoolOpKind::GreaterThanOrEqual => Ok(CalcResult::Bool(ncomp != Ordering::Less)),
                    BoolOpKind::GreaterThan => Ok(CalcResult::Bool(ncomp == Ordering::Greater)),
                }
            } else {
                Err(ParseError::CannotCompare(
                    format!("{:?}", ver_left),
                    format!("{:?}", right),
                ))
            }
        } else {
            Err(ParseError::Uncomparable(
                format!("{:?}", left),
                format!("{:?}", right),
            ))
        }
    }
}

/// A feature name as a term in an expression.
#[derive(Debug)]
struct FeatureOp {
    /// The name of the queried feature.
    name: String,
}

impl FeatureOp {
    /// Construct a feature object with the specified name.
    fn new(name: &str) -> Self {
        Self {
            name: name.to_owned(),
        }
    }
}

impl Calculable for FeatureOp {
    fn get_value(&self, features: &HashMap<String, Version>) -> Result<CalcResult, ParseError> {
        Ok(features
            .get(&self.name)
            .map_or(CalcResult::Null, |value| CalcResult::Version(value.clone())))
    }
}

/// A version string used as a term in an expression.
#[derive(Debug)]
struct VersionOp {
    /// The parsed version string.
    value: Version,
}

impl VersionOp {
    /// Construct a version object for the specified [`Version`].
    const fn from_version(version: Version) -> Self {
        Self { value: version }
    }
}

impl Calculable for VersionOp {
    fn get_value(&self, _features: &HashMap<String, Version>) -> Result<CalcResult, ParseError> {
        Ok(CalcResult::Version(self.value.clone()))
    }
}

/// Parse a "feature" or "feature op version" expression for later evaluation.
///
/// Returns either [`Mode::Single`] or [`Mode::Simple`].
///
/// # Errors
///
/// Will return an error if the expression is neither a single feature name nor in
/// the "var op value" format or if an unrecognized comparison operator is specified.
#[inline]
pub fn parse(expr: &str) -> Result<Mode, ParseError> {
    parser::parse_expr(expr)
}

#[cfg(test)]
mod tests {
    #![allow(clippy::panic)]
    #![allow(clippy::panic_in_result_fn)]
    #![allow(clippy::use_debug)]
    #![allow(clippy::wildcard_enum_match_arm)]

    use std::collections::HashMap;
    use std::error::Error;

    use crate::defs::{CalcResult, Mode};

    #[test]
    fn test_parse_mode_simple_sign_no_space() -> Result<(), Box<dyn Error>> {
        let mode = super::parse("hello<3.1")?;
        let res = match mode {
            Mode::Simple(res) => res,
            other => panic!("{:?}", other),
        };
        match res.get_value(&HashMap::from([("hello".to_owned(), "2".parse()?)]))? {
            CalcResult::Bool(true) => (),
            other => panic!("{:?}", other),
        };
        match res.get_value(&HashMap::from([("hello".to_owned(), "4".parse()?)]))? {
            CalcResult::Bool(false) => (),
            other => panic!("{:?}", other),
        };
        res.get_value(&HashMap::new()).unwrap_err();
        Ok(())
    }

    #[test]
    fn test_parse_mode_simple_sign_space() -> Result<(), Box<dyn Error>> {
        let mode = super::parse("hello < 3.1")?;
        let res = match mode {
            Mode::Simple(res) => res,
            other => panic!("{:?}", other),
        };
        match res.get_value(&HashMap::from([("hello".to_owned(), "2".parse()?)]))? {
            CalcResult::Bool(true) => (),
            other => panic!("{:?}", other),
        };
        match res.get_value(&HashMap::from([("hello".to_owned(), "4".parse()?)]))? {
            CalcResult::Bool(false) => (),
            other => panic!("{:?}", other),
        };
        res.get_value(&HashMap::new()).unwrap_err();
        Ok(())
    }

    #[test]
    fn test_parse_mode_simple_word() -> Result<(), Box<dyn Error>> {
        let mode = super::parse("hello lt 3.1")?;
        let res = match mode {
            Mode::Simple(res) => res,
            other => panic!("{:?}", other),
        };
        match res.get_value(&HashMap::from([("hello".to_owned(), "2".parse()?)]))? {
            CalcResult::Bool(true) => (),
            other => panic!("{:?}", other),
        };
        match res.get_value(&HashMap::from([("hello".to_owned(), "4".parse()?)]))? {
            CalcResult::Bool(false) => (),
            other => panic!("{:?}", other),
        };
        res.get_value(&HashMap::new()).unwrap_err();
        Ok(())
    }

    #[test]
    fn test_parse_mode_single() -> Result<(), Box<dyn Error>> {
        let mode = super::parse("hello")?;
        let res = match mode {
            Mode::Single(res) => res,
            other => panic!("{:?}", other),
        };
        match res.get_value(&HashMap::from([("hello".to_owned(), "2".parse()?)]))? {
            CalcResult::Version(ver) => assert_eq!(ver.as_ref(), "2"),
            other => panic!("{:?}", other),
        };
        match res.get_value(&HashMap::new())? {
            CalcResult::Null => (),
            other => panic!("{:?}", other),
        };
        Ok(())
    }
}