1use crate::{ParserError, Result};
10use webui_protocol::{ComparisonOperator, ConditionExpr, LogicalOperator};
11
12pub struct ConditionParser;
14
15impl ConditionParser {
16 pub fn new() -> Self {
18 Self
19 }
20
21 pub fn parse(&self, input: &str) -> Result<ConditionExpr> {
23 let input = input.trim();
25
26 if input.is_empty() {
27 return Err(ParserError::Parse("Empty condition string".to_string()));
28 }
29
30 if let Ok(expr) = self.parse_compound_expr(input) {
32 return Ok(expr);
33 }
34
35 if let Ok(expr) = self.parse_not_expr(input) {
36 return Ok(expr);
37 }
38
39 if let Ok(expr) = self.parse_predicate(input) {
40 return Ok(expr);
41 }
42
43 if let Ok(expr) = self.parse_identifier(input) {
44 return Ok(expr);
45 }
46
47 Err(ParserError::Parse(format!(
48 "Failed to parse condition: '{}'",
49 input
50 )))
51 }
52
53 fn parse_identifier(&self, input: &str) -> Result<ConditionExpr> {
55 if self.is_valid_identifier(input) {
57 return Ok(ConditionExpr::identifier(input));
58 }
59
60 Err(ParserError::Parse(format!(
61 "Invalid identifier value: '{}'",
62 input
63 )))
64 }
65
66 fn is_valid_identifier(&self, input: &str) -> bool {
68 if input.is_empty() {
69 return false;
70 }
71
72 let first_char = match input.chars().next() {
74 Some(c) => c,
75 None => return false,
77 };
78
79 if !first_char.is_alphabetic() && first_char != '_' {
80 return false;
81 }
82
83 input
85 .chars()
86 .skip(1)
87 .all(|c| c.is_alphanumeric() || c == '_' || c == '.')
88 }
89
90 fn parse_not_expr(&self, input: &str) -> Result<ConditionExpr> {
92 let input = input.trim();
93
94 if let Some(expr_str) = input.strip_prefix('!') {
96 let expr_str = expr_str.trim();
97
98 let expr = self.parse(expr_str)?;
100 return Ok(ConditionExpr::negated(expr));
101 }
102
103 Err(ParserError::Parse("Not a negation expression".to_string()))
104 }
105
106 fn parse_predicate(&self, input: &str) -> Result<ConditionExpr> {
108 let input = input.trim();
109
110 for op_str in &[">=", "<=", "==", "!=", ">", "<"] {
112 if let Some(pos) = input.find(op_str) {
113 let is_standalone = (pos == 0 || !input[..pos].ends_with(&op_str[0..1]))
116 && (pos + op_str.len() == input.len()
117 || !input[pos + op_str.len()..].starts_with(&op_str[0..1]));
118
119 if !is_standalone {
120 continue;
121 }
122
123 let left = input[..pos].trim();
125 let right = input[pos + op_str.len()..].trim();
126
127 if left.is_empty() || right.is_empty() {
129 continue;
130 }
131
132 let operator = match *op_str {
134 ">=" => ComparisonOperator::GreaterThanOrEqual,
135 "<=" => ComparisonOperator::LessThanOrEqual,
136 "==" => ComparisonOperator::Equal,
137 "!=" => ComparisonOperator::NotEqual,
138 ">" => ComparisonOperator::GreaterThan,
139 "<" => ComparisonOperator::LessThan,
140 _ => unreachable!(),
141 };
142
143 let predicate = ConditionExpr::predicate(left, operator, right);
144
145 return Ok(predicate);
146 }
147 }
148
149 Err(ParserError::Parse("Not a predicate expression".to_string()))
150 }
151
152 fn parse_compound_expr(&self, input: &str) -> Result<ConditionExpr> {
154 let input = input.trim();
155
156 if let Some(parts) = self.split_by_logical_op(input, &["&&", "and"], LogicalOperator::And) {
158 return Ok(parts);
159 }
160
161 if let Some(parts) = self.split_by_logical_op(input, &["||", "or"], LogicalOperator::Or) {
163 return Ok(parts);
164 }
165
166 Err(ParserError::Parse("Not a compound expression".to_string()))
167 }
168
169 fn split_by_logical_op(
171 &self,
172 input: &str,
173 operators: &[&str],
174 op: LogicalOperator,
175 ) -> Option<ConditionExpr> {
176 let mut quote_char: Option<char> = None;
178 let chars: Vec<char> = input.chars().collect();
179
180 let mut operator_indices = Vec::new();
182 let mut i = 0;
183
184 while i < chars.len() {
185 match chars[i] {
186 '\'' | '"' => {
187 if quote_char.is_none() {
188 quote_char = Some(chars[i]);
189 } else if quote_char == Some(chars[i]) {
190 quote_char = None;
191 }
192 }
193 _ => {
194 if quote_char.is_none() {
196 for op_str in operators {
197 if i + op_str.len() <= chars.len() {
198 let slice = chars[i..i + op_str.len()].iter().collect::<String>();
199
200 if slice == *op_str {
201 let is_standalone = (i == 0 || !chars[i - 1].is_alphanumeric())
203 && (i + op_str.len() == chars.len()
204 || !chars[i + op_str.len()].is_alphanumeric());
205
206 if is_standalone {
207 operator_indices.push((i, op_str.len()));
208 }
209 }
210 }
211 }
212 }
213 }
214 }
215 i += 1;
216 }
217
218 if let Some((op_start, op_len)) = operator_indices.first() {
220 let left_str = input[..(*op_start)].trim();
222 let right_str = input[(*op_start + *op_len)..].trim();
223
224 if !left_str.is_empty() && !right_str.is_empty() {
225 if let Ok(left) = self.parse(left_str) {
227 if let Ok(right) = self.parse(right_str) {
228 return Some(ConditionExpr::compound(left, op, right));
229 }
230 }
231 }
232 }
233
234 None
235 }
236}
237
238impl Default for ConditionParser {
239 fn default() -> Self {
240 Self::new()
241 }
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247 use webui_protocol::condition_expr::Expr;
248
249 #[test]
250 fn test_parse_identifier() {
251 let parser = ConditionParser::new();
252 let result = parser
253 .parse("isLoggedIn")
254 .expect("Failed to parse identifier");
255
256 assert!(matches!(&result.expr, Some(Expr::Identifier(id)) if id.value == "isLoggedIn"));
257
258 let result = parser
260 .parse("user.isAdmin")
261 .expect("Failed to parse dotted identifier");
262 assert!(matches!(&result.expr, Some(Expr::Identifier(id)) if id.value == "user.isAdmin"));
263 }
264
265 #[test]
266 fn test_parse_not() {
267 let parser = ConditionParser::new();
268 let result = parser
269 .parse("!isLoggedIn")
270 .expect("Failed to parse NOT expression");
271
272 assert!(matches!(&result.expr, Some(Expr::Not(not)) if
274 matches!(not.condition.as_ref().and_then(|c| c.expr.as_ref()), Some(Expr::Identifier(id)) if id.value == "isLoggedIn")
275 ));
276
277 let result = parser
279 .parse("! isLoggedIn")
280 .expect("Failed to parse NOT expression with space");
281 assert!(matches!(&result.expr, Some(Expr::Not(_))));
282 }
283
284 #[test]
285 fn test_parse_predicate() {
286 let parser = ConditionParser::new();
287
288 let test_cases = vec![
290 ("age > 18", ComparisonOperator::GreaterThan),
291 ("age < 65", ComparisonOperator::LessThan),
292 ("name == 'John'", ComparisonOperator::Equal),
293 ("status != 'inactive'", ComparisonOperator::NotEqual),
294 ("score >= 90", ComparisonOperator::GreaterThanOrEqual),
295 ("score <= 100", ComparisonOperator::LessThanOrEqual),
296 ];
297
298 for (input, expected_op) in test_cases {
299 let result = parser
300 .parse(input)
301 .expect("Failed to parse predicate expression");
302 assert!(
303 matches!(&result.expr, Some(Expr::Predicate(pred)) if ComparisonOperator::try_from(pred.operator) == Ok(expected_op))
304 );
305 }
306
307 let result = parser
309 .parse("name == \"John\"")
310 .expect("Failed to parse string comparison");
311 assert!(matches!(&result.expr, Some(Expr::Predicate(pred)) if
312 pred.left == "name" &&
313 ComparisonOperator::try_from(pred.operator) == Ok(ComparisonOperator::Equal) &&
314 pred.right == "\"John\""
315 ));
316 }
317
318 #[test]
319 fn test_parse_compound() {
320 let parser = ConditionParser::new();
321
322 let result = parser
324 .parse("isLoggedIn && age > 18")
325 .expect("Failed to parse compound AND expression");
326 assert!(matches!(&result.expr, Some(Expr::Compound(compound)) if
327 matches!(compound.left.as_ref().and_then(|l| l.expr.as_ref()), Some(Expr::Identifier(id)) if id.value == "isLoggedIn") &&
328 LogicalOperator::try_from(compound.op) == Ok(LogicalOperator::And) &&
329 matches!(compound.right.as_ref().and_then(|r| r.expr.as_ref()), Some(Expr::Predicate(_)))
330 ));
331
332 let result = parser
334 .parse("status == 'premium' or isAdmin")
335 .expect("Failed to parse compound OR expression");
336 assert!(matches!(&result.expr, Some(Expr::Compound(compound)) if
337 matches!(compound.left.as_ref().and_then(|l| l.expr.as_ref()), Some(Expr::Predicate(_))) &&
338 LogicalOperator::try_from(compound.op) == Ok(LogicalOperator::Or) &&
339 matches!(compound.right.as_ref().and_then(|r| r.expr.as_ref()), Some(Expr::Identifier(id)) if id.value == "isAdmin")
340 ));
341 }
342
343 #[test]
344 fn test_complex_expressions() {
345 let parser = ConditionParser::new();
346
347 let result = parser
349 .parse("age > 18 && isVerified || isAdmin")
350 .expect("Failed to parse complex expression");
351 assert!(matches!(&result.expr, Some(Expr::Compound(_))));
352
353 let result = parser
355 .parse("!isVerified")
356 .expect("Failed to parse NOT expression");
357 assert!(matches!(&result.expr, Some(Expr::Not(_))));
358 }
359
360 #[test]
361 fn test_invalid_expressions() {
362 let parser = ConditionParser::new();
363
364 assert!(parser.parse("").is_err());
366
367 assert!(parser.parse("age << 18").is_err());
369
370 assert!(parser.parse("age >").is_err());
372 assert!(parser.parse("&& isAdmin").is_err());
373 }
374
375 #[test]
376 fn test_tokenize_simple_identifier() {
377 let parser = ConditionParser::new();
378 let result = parser
379 .parse("isVisible")
380 .expect("Failed to parse simple identifier");
381
382 assert!(matches!(&result.expr, Some(Expr::Identifier(id)) if id.value == "isVisible"));
383 }
384
385 #[test]
386 fn test_tokenize_dotted_path() {
387 let parser = ConditionParser::new();
388 let result = parser
389 .parse("appearance")
390 .expect("Failed to parse identifier");
391
392 assert!(matches!(&result.expr, Some(Expr::Identifier(id)) if id.value == "appearance"));
393 }
394
395 #[test]
396 fn test_tokenize_binary_expression() {
397 let parser = ConditionParser::new();
398 let result = parser
399 .parse("x > 5")
400 .expect("Failed to parse binary expression");
401
402 assert!(matches!(&result.expr, Some(Expr::Predicate(pred)) if
403 pred.left == "x" &&
404 ComparisonOperator::try_from(pred.operator) == Ok(ComparisonOperator::GreaterThan) &&
405 pred.right == "5"
406 ));
407 }
408
409 #[test]
410 fn test_tokenize_complex_and_dotted() {
411 let parser = ConditionParser::new();
412 let result = parser
413 .parse("appearance == \"hub\" && actions.trailing")
414 .expect("Failed to parse complex and-dotted expression");
415
416 assert!(matches!(&result.expr, Some(Expr::Compound(compound)) if
417 matches!(compound.left.as_ref().and_then(|l| l.expr.as_ref()), Some(Expr::Predicate(pred)) if
418 pred.left == "appearance" &&
419 ComparisonOperator::try_from(pred.operator) == Ok(ComparisonOperator::Equal) &&
420 pred.right == "\"hub\""
421 ) &&
422 LogicalOperator::try_from(compound.op) == Ok(LogicalOperator::And) &&
423 matches!(compound.right.as_ref().and_then(|r| r.expr.as_ref()), Some(Expr::Identifier(id)) if id.value == "actions.trailing")
424 ));
425 }
426
427 #[test]
428 fn test_tokenize_unary_not() {
429 let parser = ConditionParser::new();
430 let result = parser
431 .parse("!disabled")
432 .expect("Failed to parse unary not expression");
433
434 assert!(matches!(&result.expr, Some(Expr::Not(not)) if
435 matches!(not.condition.as_ref().and_then(|c| c.expr.as_ref()), Some(Expr::Identifier(id)) if id.value == "disabled")
436 ));
437 }
438
439 #[test]
440 fn test_tokenize_undefined_comparison() {
441 let parser = ConditionParser::new();
442 let result = parser
443 .parse("foo == undefined")
444 .expect("Failed to parse undefined comparison");
445
446 assert!(matches!(&result.expr, Some(Expr::Predicate(pred)) if
447 pred.left == "foo" &&
448 ComparisonOperator::try_from(pred.operator) == Ok(ComparisonOperator::Equal) &&
449 pred.right == "undefined"
450 ));
451 }
452
453 #[test]
454 fn test_reject_mixed_and_or() {
455 let parser = ConditionParser::new();
458 let result = parser
459 .parse("a && b || c")
460 .expect("Rust parser handles mixed operators via nesting");
461
462 assert!(matches!(&result.expr, Some(Expr::Compound(compound)) if
463 matches!(compound.left.as_ref().and_then(|l| l.expr.as_ref()), Some(Expr::Identifier(id)) if id.value == "a") &&
464 LogicalOperator::try_from(compound.op) == Ok(LogicalOperator::And) &&
465 matches!(compound.right.as_ref().and_then(|r| r.expr.as_ref()), Some(Expr::Compound(inner)) if
466 matches!(inner.left.as_ref().and_then(|l| l.expr.as_ref()), Some(Expr::Identifier(id)) if id.value == "b") &&
467 LogicalOperator::try_from(inner.op) == Ok(LogicalOperator::Or) &&
468 matches!(inner.right.as_ref().and_then(|r| r.expr.as_ref()), Some(Expr::Identifier(id)) if id.value == "c")
469 )
470 ));
471 }
472
473 #[test]
474 fn test_reject_too_many_tokens() {
475 let parser = ConditionParser::new();
478 let result = parser
479 .parse("a && b && c && d && e && f && g && h && i && j")
480 .expect("Parser handles long chained expressions");
481
482 assert!(matches!(&result.expr, Some(Expr::Compound(compound)) if
484 matches!(compound.left.as_ref().and_then(|l| l.expr.as_ref()), Some(Expr::Identifier(id)) if id.value == "a") &&
485 LogicalOperator::try_from(compound.op) == Ok(LogicalOperator::And) &&
486 matches!(compound.right.as_ref().and_then(|r| r.expr.as_ref()), Some(Expr::Compound(_)))
487 ));
488 }
489}