feature_check/
expr.rs

1/*
2 * SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net>
3 * SPDX-License-Identifier: BSD-2-Clause
4 */
5//! Check whether the program's features satisfy the specified condition.
6
7#![allow(clippy::pub_use)]
8
9use std::cmp::Ordering;
10use std::collections::HashMap;
11use std::str::FromStr;
12
13use crate::defs::{Mode, ParseError};
14use crate::version::Version;
15
16pub mod parser;
17
18pub use crate::defs::{CalcResult, Calculable};
19
20/// The type of a boolean comparison operation requested.
21#[derive(Debug)]
22enum BoolOpKind {
23    /// The first version string sorts before the second one.
24    LessThan,
25    /// The first version string sorts before the second one or is the same.
26    LessThanOrEqual,
27    /// The version strings are the same.
28    Equal,
29    /// The first version string sorts after the second one or is the same.
30    GreaterThanOrEqual,
31    /// The first version string sorts after the second one.
32    GreaterThan,
33}
34
35impl BoolOpKind {
36    /// The symbol representing the "is less than" comparison operation.
37    const LT: &'static str = "<";
38    /// The symbol representing the "is less than or equal to" comparison operation.
39    const LE: &'static str = "<=";
40    /// The symbol representing the "is equal to" comparison operation.
41    const EQ: &'static str = "=";
42    /// The symbol representing the "is greater than or equal to" comparison operation.
43    const GT: &'static str = ">";
44    /// The symbol representing the "is greater than" comparison operation.
45    const GE: &'static str = ">=";
46
47    /// The string representing the "is less than" comparison operation.
48    const LT_S: &'static str = "lt";
49    /// The string representing the "is less than or equal to" comparison operation.
50    const LE_S: &'static str = "le";
51    /// The string representing the "is equal to" comparison operation.
52    const EQ_S: &'static str = "eq";
53    /// The string representing the "is greater than or equal to" comparison operation.
54    const GE_S: &'static str = "ge";
55    /// The string representing the "is greater than" comparison operation.
56    const GT_S: &'static str = "gt";
57}
58
59impl FromStr for BoolOpKind {
60    type Err = ParseError;
61
62    fn from_str(value: &str) -> Result<Self, Self::Err> {
63        match value {
64            Self::LT | Self::LT_S => Ok(Self::LessThan),
65            Self::LE | Self::LE_S => Ok(Self::LessThanOrEqual),
66            Self::EQ | Self::EQ_S => Ok(Self::Equal),
67            Self::GE | Self::GE_S => Ok(Self::GreaterThanOrEqual),
68            Self::GT | Self::GT_S => Ok(Self::GreaterThan),
69            other => Err(ParseError::InvalidComparisonOperator(other.to_owned())),
70        }
71    }
72}
73
74/// A boolean comparison operation with its arguments.
75#[derive(Debug)]
76struct BoolOp {
77    /// The comparison type.
78    op: BoolOpKind,
79    /// The left (first) operand.
80    left: Box<dyn Calculable + 'static>,
81    /// The right (second) operand.
82    right: Box<dyn Calculable + 'static>,
83}
84
85impl BoolOp {
86    /// Construct a boolean operation object with the specified parameters.
87    fn new(op: BoolOpKind, left: Box<dyn Calculable>, right: Box<dyn Calculable>) -> Self {
88        Self { op, left, right }
89    }
90}
91
92impl Calculable for BoolOp {
93    fn get_value(&self, features: &HashMap<String, Version>) -> Result<CalcResult, ParseError> {
94        let left = self.left.get_value(features)?;
95        let right = self.right.get_value(features)?;
96        if let CalcResult::Version(ver_left) = left {
97            if let CalcResult::Version(ver_right) = right {
98                let ncomp = ver_left.cmp(&ver_right);
99                match self.op {
100                    BoolOpKind::LessThan => Ok(CalcResult::Bool(ncomp == Ordering::Less)),
101                    BoolOpKind::LessThanOrEqual => Ok(CalcResult::Bool(ncomp != Ordering::Greater)),
102                    BoolOpKind::Equal => Ok(CalcResult::Bool(ncomp == Ordering::Equal)),
103                    BoolOpKind::GreaterThanOrEqual => Ok(CalcResult::Bool(ncomp != Ordering::Less)),
104                    BoolOpKind::GreaterThan => Ok(CalcResult::Bool(ncomp == Ordering::Greater)),
105                }
106            } else {
107                Err(ParseError::CannotCompare(
108                    format!("{ver_left:?}"),
109                    format!("{right:?}"),
110                ))
111            }
112        } else {
113            Err(ParseError::Uncomparable(
114                format!("{left:?}"),
115                format!("{right:?}"),
116            ))
117        }
118    }
119}
120
121/// A feature name as a term in an expression.
122#[derive(Debug)]
123struct FeatureOp {
124    /// The name of the queried feature.
125    name: String,
126}
127
128impl FeatureOp {
129    /// Construct a feature object with the specified name.
130    fn new(name: &str) -> Self {
131        Self {
132            name: name.to_owned(),
133        }
134    }
135}
136
137impl Calculable for FeatureOp {
138    fn get_value(&self, features: &HashMap<String, Version>) -> Result<CalcResult, ParseError> {
139        Ok(features
140            .get(&self.name)
141            .map_or(CalcResult::Null, |value| CalcResult::Version(value.clone())))
142    }
143}
144
145/// A version string used as a term in an expression.
146#[derive(Debug)]
147struct VersionOp {
148    /// The parsed version string.
149    value: Version,
150}
151
152impl VersionOp {
153    /// Construct a version object for the specified [`Version`].
154    const fn from_version(version: Version) -> Self {
155        Self { value: version }
156    }
157}
158
159impl Calculable for VersionOp {
160    fn get_value(&self, _features: &HashMap<String, Version>) -> Result<CalcResult, ParseError> {
161        Ok(CalcResult::Version(self.value.clone()))
162    }
163}
164
165/// Parse a "feature" or "feature op version" expression for later evaluation.
166///
167/// Returns either [`Mode::Single`] or [`Mode::Simple`].
168///
169/// # Errors
170///
171/// Will return an error if the expression is neither a single feature name nor in
172/// the "var op value" format or if an unrecognized comparison operator is specified.
173#[inline]
174pub fn parse(expr: &str) -> Result<Mode, ParseError> {
175    parser::parse_expr(expr)
176}
177
178#[cfg(test)]
179mod tests {
180    #![allow(clippy::panic)]
181    #![allow(clippy::panic_in_result_fn)]
182    #![allow(clippy::unwrap_used)]
183    #![allow(clippy::use_debug)]
184    #![allow(clippy::wildcard_enum_match_arm)]
185
186    use std::collections::HashMap;
187    use std::error::Error;
188
189    use crate::defs::{CalcResult, Mode};
190
191    #[test]
192    fn test_parse_mode_simple_sign_no_space() -> Result<(), Box<dyn Error>> {
193        let mode = super::parse("hello<3.1")?;
194        let res = match mode {
195            Mode::Simple(res) => res,
196            other => panic!("{other:?}"),
197        };
198        match res.get_value(&HashMap::from([("hello".to_owned(), "2".parse()?)]))? {
199            CalcResult::Bool(true) => (),
200            other => panic!("{other:?}"),
201        };
202        match res.get_value(&HashMap::from([("hello".to_owned(), "4".parse()?)]))? {
203            CalcResult::Bool(false) => (),
204            other => panic!("{other:?}"),
205        };
206        res.get_value(&HashMap::new()).unwrap_err();
207        Ok(())
208    }
209
210    #[test]
211    fn test_parse_mode_simple_sign_space() -> Result<(), Box<dyn Error>> {
212        let mode = super::parse("hello < 3.1")?;
213        let res = match mode {
214            Mode::Simple(res) => res,
215            other => panic!("{other:?}"),
216        };
217        match res.get_value(&HashMap::from([("hello".to_owned(), "2".parse()?)]))? {
218            CalcResult::Bool(true) => (),
219            other => panic!("{other:?}"),
220        };
221        match res.get_value(&HashMap::from([("hello".to_owned(), "4".parse()?)]))? {
222            CalcResult::Bool(false) => (),
223            other => panic!("{other:?}"),
224        };
225        res.get_value(&HashMap::new()).unwrap_err();
226        Ok(())
227    }
228
229    #[test]
230    fn test_parse_mode_simple_word() -> Result<(), Box<dyn Error>> {
231        let mode = super::parse("hello lt 3.1")?;
232        let res = match mode {
233            Mode::Simple(res) => res,
234            other => panic!("{other:?}"),
235        };
236        match res.get_value(&HashMap::from([("hello".to_owned(), "2".parse()?)]))? {
237            CalcResult::Bool(true) => (),
238            other => panic!("{other:?}"),
239        };
240        match res.get_value(&HashMap::from([("hello".to_owned(), "4".parse()?)]))? {
241            CalcResult::Bool(false) => (),
242            other => panic!("{other:?}"),
243        };
244        res.get_value(&HashMap::new()).unwrap_err();
245        Ok(())
246    }
247
248    #[test]
249    fn test_parse_mode_single() -> Result<(), Box<dyn Error>> {
250        let mode = super::parse("hello")?;
251        let res = match mode {
252            Mode::Single(res) => res,
253            other => panic!("{other:?}"),
254        };
255        match res.get_value(&HashMap::from([("hello".to_owned(), "2".parse()?)]))? {
256            CalcResult::Version(ver) => assert_eq!(ver.as_ref(), "2"),
257            other => panic!("{other:?}"),
258        };
259        match res.get_value(&HashMap::new())? {
260            CalcResult::Null => (),
261            other => panic!("{other:?}"),
262        };
263        Ok(())
264    }
265}