feature-check 1.0.1

Query a program for supported features
Documentation
/*
 * Copyright (c) 2021  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.

use std::cmp;
use std::collections::HashMap;
use std::error;
use std::fmt;
use std::str::FromStr;

use crate::version as fver;

quick_error! {
    #[derive(Debug)]
    enum ParseError {
        CannotCompare(left: String, right: String) {
            display("Cannot compare {} to {}", left, right)
        }
        InvalidComparisonOperator(op: String) {
            display("Invalid comparison operator '{}'", op)
        }
        SimpleExpected {
            display("Expected a 'feature-name op version' expression")
        }
        Uncomparable(left: String, right: String) {
            display("Don't know how to compare {} to anything, including {}", left, right)
        }
    }
}

const RE_VAR: &str = r"[A-Za-z0-9_-]+";
const RE_VALUE: &str = r"[A-Za-z0-9.]+";
const RE_OP: &str = r"(?: < | <= | = | >= | > | lt | le | eq | ge | gt )";

#[derive(Debug)]
enum BoolOpKind {
    LessThan,
    LessThanOrEqual,
    Equal,
    GreaterThanOrEqual,
    GreaterThan,
}

impl BoolOpKind {
    const LT: &'static str = "<";
    const LE: &'static str = "<=";
    const EQ: &'static str = "=";
    const GT: &'static str = ">";
    const GE: &'static str = ">=";

    const LT_S: &'static str = "lt";
    const LE_S: &'static str = "le";
    const EQ_S: &'static str = "eq";
    const GE_S: &'static str = "ge";
    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_string())),
        }
    }
}

/// The result of evaluating either a single term or the whole expression.
#[derive(Debug)]
pub enum CalcResult {
    /// No value, e.g. the queried feature is not present.
    Null,
    /// A boolean value, usually for the whole expression.
    Bool(bool),
    /// A feature's obtained version.
    Version(fver::Version),
}

/// An object that may be evaluated and provide a result.
pub trait Calculable: fmt::Debug {
    /// Get the value of the evaluated term or expression as applied to
    /// the list of features obtained for the program.
    fn get_value(
        &self,
        features: &HashMap<String, String>,
    ) -> Result<CalcResult, Box<dyn error::Error>>;
}

#[derive(Debug)]
struct BoolOp {
    op: BoolOpKind,
    left: Box<dyn Calculable>,
    right: Box<dyn Calculable>,
}

impl BoolOp {
    fn new(op: BoolOpKind, left: Box<dyn Calculable>, right: Box<dyn Calculable>) -> Self {
        Self { op, left, right }
    }

    fn boxed(op: BoolOpKind, left: Box<dyn Calculable>, right: Box<dyn Calculable>) -> Box<Self> {
        Box::new(Self::new(op, left, right))
    }
}

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

#[derive(Debug)]
struct FeatureOp {
    name: String,
}

impl FeatureOp {
    fn new(name: &str) -> Self {
        Self {
            name: name.to_string(),
        }
    }

    fn boxed(name: &str) -> Box<Self> {
        Box::new(Self::new(name))
    }
}

impl Calculable for FeatureOp {
    fn get_value(
        &self,
        features: &HashMap<String, String>,
    ) -> Result<CalcResult, Box<dyn error::Error>> {
        match features.get(&self.name) {
            Some(value) => Ok(CalcResult::Version(value.parse()?)),
            None => Ok(CalcResult::Null),
        }
    }
}

#[derive(Debug)]
struct VersionOp {
    value: String,
}

impl VersionOp {
    fn new(value: &str) -> Self {
        Self {
            value: value.to_string(),
        }
    }

    fn boxed(value: &str) -> Box<Self> {
        Box::new(Self::new(value))
    }
}

impl Calculable for VersionOp {
    fn get_value(
        &self,
        _features: &HashMap<String, String>,
    ) -> Result<CalcResult, Box<dyn error::Error>> {
        Ok(CalcResult::Version(self.value.parse()?))
    }
}

/// Parse a "feature op version" expression for later evaluation.
pub fn parse_simple(expr: &str) -> Result<Box<dyn Calculable>, Box<dyn error::Error>> {
    let re_simple = regex::Regex::new(&format!(
        r"(?x) ^ (?P<var> {} ) \s* (?P<op> {} ) \s* (?P<value> {} ) $",
        RE_VAR, RE_OP, RE_VALUE
    ))
    .unwrap();
    match re_simple.captures(expr) {
        Some(caps) => {
            let feature = &caps["var"];
            let op_name = &caps["op"];
            let value = &caps["value"];
            Ok(BoolOp::boxed(
                op_name.parse()?,
                FeatureOp::boxed(feature),
                VersionOp::boxed(value),
            ))
        }
        None => Err(Box::new(ParseError::SimpleExpected)),
    }
}