use std::{cmp::Ordering, str::FromStr}; use anyhow::bail; use serde_with::DeserializeFromStr; #[derive(Debug, PartialEq, Eq)] pub enum ConditionOp { Or, And, Not, // Only used in `build()` Space, LeftParen, RightParen, Unknown, // Only used in `eval()` Term(String), } impl ConditionOp { pub fn new(c: char) -> Self { match c { '|' => Self::Or, '&' => Self::And, '!' => Self::Not, '(' => Self::LeftParen, ')' => Self::RightParen, _ if c.is_ascii_whitespace() => Self::Space, _ => Self::Unknown, } } fn prec(&self) -> u8 { match self { Self::Or => 1, Self::And => 2, Self::Not => 3, _ => 0, } } } impl PartialOrd for ConditionOp { fn partial_cmp(&self, other: &Self) -> Option { use Ordering::*; match self.prec().cmp(&other.prec()) { // Keep repeated `!` right-associative by making `! >= !` false. Equal if matches!((self, other), (Self::Not, Self::Not)) => None, ordering => Some(ordering), } } } #[derive(Debug, DeserializeFromStr)] pub struct Condition { ops: Vec, } impl FromStr for Condition { type Err = anyhow::Error; fn from_str(expr: &str) -> Result { let cond = Self::build(expr); if cond.eval(|_| true).is_none() { bail!("Invalid condition: {expr}"); } Ok(cond) } } impl Condition { fn build(expr: &str) -> Self { let mut stack: Vec = vec![]; let mut output: Vec = vec![]; let mut chars = expr.chars().peekable(); while let Some(token) = chars.next() { let op = ConditionOp::new(token); match op { ConditionOp::Or | ConditionOp::And | ConditionOp::Not => { while matches!(stack.last(), Some(last) if last >= &op) { output.push(stack.pop().unwrap()); } stack.push(op); } ConditionOp::Space => continue, ConditionOp::LeftParen => stack.push(op), ConditionOp::RightParen => { while matches!(stack.last(), Some(last) if last != &ConditionOp::LeftParen) { output.push(stack.pop().unwrap()); } stack.pop(); } ConditionOp::Unknown => { let mut s = String::from(token); while matches!(chars.peek(), Some(&c) if ConditionOp::new(c) == op) { s.push(chars.next().unwrap()); } output.push(ConditionOp::Term(s)); } ConditionOp::Term(_) => unreachable!(), } } while let Some(op) = stack.pop() { output.push(op); } Self { ops: output } } pub fn eval(&self, f: impl Fn(&str) -> bool) -> Option { let mut stack: Vec = Vec::with_capacity(self.ops.len()); for op in &self.ops { match op { ConditionOp::Or => { let b = stack.pop()? | stack.pop()?; stack.push(b); } ConditionOp::And => { let b = stack.pop()? & stack.pop()?; stack.push(b); } ConditionOp::Not => { let b = !stack.pop()?; stack.push(b); } ConditionOp::Term(s) => { stack.push(f(s)); } _ => return None, } } if stack.len() == 1 { Some(stack[0]) } else { None } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_condition_not() -> anyhow::Result<()> { let cond: Condition = "!dir".parse()?; assert!(!cond.eval(|s| s == "dir").unwrap()); assert!(cond.eval(|_| false).unwrap()); let cond: Condition = "!!dir".parse()?; assert!(cond.eval(|s| s == "dir").unwrap()); assert!(!cond.eval(|_| false).unwrap()); Ok(()) } }