feature_check/
expr.rs

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