flow_execution/
condition.rs1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28enum CmpOp {
29 Eq,
30 Ne,
31 Lt,
32 Le,
33 Gt,
34 Ge,
35}
36
37#[derive(Debug, Clone, PartialEq, Eq)]
38enum Tok {
39 Val(String),
40 Op(CmpOp),
41 And,
42 Or,
43}
44
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub enum EvalError {
47 Empty,
49 ExpectedValue,
51 Trailing,
53 UnterminatedString,
55}
56
57pub fn eval(expr: &str) -> Result<bool, EvalError> {
59 let toks = tokenize(expr)?;
60 if toks.is_empty() {
61 return Err(EvalError::Empty);
62 }
63 let mut p = Parser { toks, pos: 0 };
64 let value = p.parse_or()?;
65 if p.pos != p.toks.len() {
66 return Err(EvalError::Trailing);
67 }
68 Ok(value)
69}
70
71fn tokenize(expr: &str) -> Result<Vec<Tok>, EvalError> {
72 let chars: Vec<char> = expr.chars().collect();
73 let mut toks = Vec::new();
74 let mut i = 0;
75 while i < chars.len() {
76 let c = chars[i];
77 if c.is_whitespace() {
78 i += 1;
79 continue;
80 }
81 match c {
82 '\'' | '"' => {
83 let quote = c;
84 let mut s = String::new();
85 i += 1;
86 let mut closed = false;
87 while i < chars.len() {
88 if chars[i] == quote {
89 closed = true;
90 i += 1;
91 break;
92 }
93 s.push(chars[i]);
94 i += 1;
95 }
96 if !closed {
97 return Err(EvalError::UnterminatedString);
98 }
99 toks.push(Tok::Val(s));
100 }
101 '=' if peek(&chars, i + 1) == Some('=') => {
102 toks.push(Tok::Op(CmpOp::Eq));
103 i += 2;
104 }
105 '!' if peek(&chars, i + 1) == Some('=') => {
106 toks.push(Tok::Op(CmpOp::Ne));
107 i += 2;
108 }
109 '<' if peek(&chars, i + 1) == Some('=') => {
110 toks.push(Tok::Op(CmpOp::Le));
111 i += 2;
112 }
113 '>' if peek(&chars, i + 1) == Some('=') => {
114 toks.push(Tok::Op(CmpOp::Ge));
115 i += 2;
116 }
117 '<' => {
118 toks.push(Tok::Op(CmpOp::Lt));
119 i += 1;
120 }
121 '>' => {
122 toks.push(Tok::Op(CmpOp::Gt));
123 i += 1;
124 }
125 '&' if peek(&chars, i + 1) == Some('&') => {
126 toks.push(Tok::And);
127 i += 2;
128 }
129 '|' if peek(&chars, i + 1) == Some('|') => {
130 toks.push(Tok::Or);
131 i += 2;
132 }
133 _ => {
134 let start = i;
136 while i < chars.len() {
137 let d = chars[i];
138 if d.is_whitespace() || matches!(d, '=' | '!' | '<' | '>' | '&' | '|') {
139 break;
140 }
141 i += 1;
142 }
143 toks.push(Tok::Val(chars[start..i].iter().collect()));
144 }
145 }
146 }
147 Ok(toks)
148}
149
150fn peek(chars: &[char], i: usize) -> Option<char> {
151 chars.get(i).copied()
152}
153
154struct Parser {
155 toks: Vec<Tok>,
156 pos: usize,
157}
158
159impl Parser {
160 fn parse_or(&mut self) -> Result<bool, EvalError> {
161 let mut value = self.parse_and()?;
162 while matches!(self.toks.get(self.pos), Some(Tok::Or)) {
163 self.pos += 1;
164 let rhs = self.parse_and()?;
165 value = value || rhs;
166 }
167 Ok(value)
168 }
169
170 fn parse_and(&mut self) -> Result<bool, EvalError> {
171 let mut value = self.parse_cmp()?;
172 while matches!(self.toks.get(self.pos), Some(Tok::And)) {
173 self.pos += 1;
174 let rhs = self.parse_cmp()?;
175 value = value && rhs;
176 }
177 Ok(value)
178 }
179
180 fn parse_cmp(&mut self) -> Result<bool, EvalError> {
181 let lhs = self.expect_value()?;
182 if let Some(Tok::Op(op)) = self.toks.get(self.pos).cloned() {
183 self.pos += 1;
184 let rhs = self.expect_value()?;
185 Ok(compare(op, &lhs, &rhs))
186 } else {
187 Ok(truthy(&lhs))
188 }
189 }
190
191 fn expect_value(&mut self) -> Result<String, EvalError> {
192 match self.toks.get(self.pos) {
193 Some(Tok::Val(s)) => {
194 let s = s.clone();
195 self.pos += 1;
196 Ok(s)
197 }
198 _ => Err(EvalError::ExpectedValue),
199 }
200 }
201}
202
203fn compare(op: CmpOp, a: &str, b: &str) -> bool {
204 if let (Ok(x), Ok(y)) = (a.trim().parse::<f64>(), b.trim().parse::<f64>()) {
205 match op {
206 CmpOp::Eq => x == y,
207 CmpOp::Ne => x != y,
208 CmpOp::Lt => x < y,
209 CmpOp::Le => x <= y,
210 CmpOp::Gt => x > y,
211 CmpOp::Ge => x >= y,
212 }
213 } else {
214 match op {
215 CmpOp::Eq => a == b,
216 CmpOp::Ne => a != b,
217 CmpOp::Lt => a < b,
218 CmpOp::Le => a <= b,
219 CmpOp::Gt => a > b,
220 CmpOp::Ge => a >= b,
221 }
222 }
223}
224
225fn truthy(s: &str) -> bool {
226 let t = s.trim();
227 if t.eq_ignore_ascii_case("true") {
228 return true;
229 }
230 if t.eq_ignore_ascii_case("false") || t.is_empty() {
231 return false;
232 }
233 if let Ok(n) = t.parse::<f64>() {
234 return n != 0.0;
235 }
236 true
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242
243 #[test]
244 fn numeric_comparisons() {
245 assert_eq!(eval("5 > 0"), Ok(true));
246 assert_eq!(eval("0 > 0"), Ok(false));
247 assert_eq!(eval("3 == 3"), Ok(true));
248 assert_eq!(eval("3 >= 3"), Ok(true));
249 assert_eq!(eval("2 <= 1"), Ok(false));
250 assert_eq!(eval("2 != 3"), Ok(true));
251 }
252
253 #[test]
254 fn string_equality() {
255 assert_eq!(eval("done == 'done'"), Ok(true));
256 assert_eq!(eval("'a' == \"b\""), Ok(false));
257 assert_eq!(eval("ok != fail"), Ok(true));
258 }
259
260 #[test]
261 fn bare_value_truthiness() {
262 assert_eq!(eval("true"), Ok(true));
263 assert_eq!(eval("false"), Ok(false));
264 assert_eq!(eval("1"), Ok(true));
265 assert_eq!(eval("0"), Ok(false));
266 assert_eq!(eval("anything"), Ok(true));
267 }
268
269 #[test]
270 fn and_binds_tighter_than_or() {
271 assert_eq!(eval("0 > 1 && 1 > 0 || 5 == 5"), Ok(true));
273 assert_eq!(eval("1 == 1 && 2 == 3"), Ok(false));
275 assert_eq!(eval("1 == 1 || 2 == 3"), Ok(true));
277 }
278
279 #[test]
280 fn empty_resolved_reference_fails_closed() {
281 assert_eq!(eval(" > 0"), Err(EvalError::ExpectedValue));
284 assert_eq!(eval(""), Err(EvalError::Empty));
285 assert_eq!(eval(" "), Err(EvalError::Empty));
286 }
287
288 #[test]
289 fn malformed_expressions_error() {
290 assert_eq!(eval("5 5"), Err(EvalError::Trailing));
291 assert_eq!(eval("5 =="), Err(EvalError::ExpectedValue));
292 assert_eq!(eval("'unclosed"), Err(EvalError::UnterminatedString));
293 }
294}