pub type Lookup<'a> = dyn Fn(&str) -> String + 'a;
pub fn eval(cond: &str, lookup: &Lookup) -> bool {
Pred::parse(cond).eval(lookup)
}
enum Pred {
Truthy(String),
Cmp {
left: String,
equal: bool,
right: Operand,
},
Not(Box<Pred>),
}
enum Operand {
Ref(String),
Literal(String),
}
impl Pred {
fn parse(cond: &str) -> Pred {
let cond = cond.trim();
if let Some(inner) = cond.strip_prefix('!') {
return Pred::Not(Box::new(Pred::parse(inner)));
}
for (op, equal) in [("==", true), ("!=", false)] {
if let Some((l, r)) = cond.split_once(op) {
return Pred::Cmp {
left: vref(l),
equal,
right: Operand::parse(r),
};
}
}
Pred::Truthy(vref(cond))
}
fn eval(&self, lookup: &Lookup) -> bool {
match self {
Pred::Truthy(path) => !lookup(path).is_empty(),
Pred::Cmp { left, equal, right } => (lookup(left) == right.value(lookup)) == *equal,
Pred::Not(inner) => !inner.eval(lookup),
}
}
}
impl Operand {
fn parse(s: &str) -> Operand {
let s = s.trim();
if let Some(name) = s.strip_prefix('$') {
Operand::Ref(name.to_string())
} else {
Operand::Literal(unquote(s).to_string())
}
}
fn value(&self, lookup: &Lookup) -> String {
match self {
Operand::Ref(path) => lookup(path),
Operand::Literal(lit) => lit.clone(),
}
}
}
fn vref(s: &str) -> String {
s.trim().trim_start_matches('$').to_string()
}
fn unquote(s: &str) -> &str {
let bytes = s.as_bytes();
if bytes.len() >= 2 {
let (first, last) = (bytes[0], bytes[bytes.len() - 1]);
if (first == b'\'' && last == b'\'') || (first == b'"' && last == b'"') {
return &s[1..s.len() - 1];
}
}
s
}
#[cfg(test)]
mod tests {
use super::eval;
use std::collections::BTreeMap;
fn ctx(pairs: &[(&str, &str)]) -> impl Fn(&str) -> String {
let map: BTreeMap<String, String> = pairs
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
move |path: &str| map.get(path).cloned().unwrap_or_default()
}
#[test]
fn truthiness_is_nonemptiness() {
let c = ctx(&[("label", "Hi"), ("empty", "")]);
assert!(eval("$label", &c));
assert!(!eval("$empty", &c));
assert!(!eval("$missing", &c));
}
#[test]
fn negation() {
let c = ctx(&[("label", "Hi")]);
assert!(!eval("!$label", &c)); assert!(eval("!$missing", &c)); assert!(eval("!!$label", &c)); }
#[test]
fn equality_against_literal() {
let c = ctx(&[("icon", "book"), ("cols", "2")]);
assert!(eval("$icon == 'book'", &c));
assert!(eval("$icon == \"book\"", &c));
assert!(!eval("$icon == 'code'", &c));
assert!(eval("$cols == 2", &c)); }
#[test]
fn disequality_and_ref_rhs() {
let c = ctx(&[("a", "x"), ("b", "y"), ("c", "x")]);
assert!(eval("$a != 'y'", &c));
assert!(!eval("$a != 'x'", &c));
assert!(eval("$a == $c", &c)); assert!(eval("$a != $b", &c));
}
#[test]
fn empty_condition_is_false() {
let c = ctx(&[]);
assert!(!eval("", &c));
assert!(!eval(" ", &c));
}
}