1#![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#[derive(Debug)]
20enum BoolOpKind {
21 LessThan,
23 LessThanOrEqual,
25 Equal,
27 GreaterThanOrEqual,
29 GreaterThan,
31}
32
33impl BoolOpKind {
34 const LT: &'static str = "<";
36 const LE: &'static str = "<=";
38 const EQ: &'static str = "=";
40 const GT: &'static str = ">";
42 const GE: &'static str = ">=";
44
45 const LT_S: &'static str = "lt";
47 const LE_S: &'static str = "le";
49 const EQ_S: &'static str = "eq";
51 const GE_S: &'static str = "ge";
53 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#[derive(Debug)]
74struct BoolOp {
75 op: BoolOpKind,
77 left: Box<dyn Calculable + 'static>,
79 right: Box<dyn Calculable + 'static>,
81}
82
83impl BoolOp {
84 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#[derive(Debug)]
121struct FeatureOp {
122 name: String,
124}
125
126impl FeatureOp {
127 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#[derive(Debug)]
145struct VersionOp {
146 value: Version,
148}
149
150impl VersionOp {
151 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#[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}