use crate::ast::Expr;
use crate::eval::EvalCtx;
use crate::model::{Filter, LogicFilter, NotFilter};
use crate::parser::parse;
#[derive(Debug, Clone)]
pub enum CompiledFilter {
True,
Expr(Expr),
Invalid(String),
And(Vec<CompiledFilter>),
Or(Vec<CompiledFilter>),
Not(Box<CompiledFilter>),
}
impl CompiledFilter {
pub fn matches(&self, ctx: &EvalCtx) -> bool {
match self {
CompiledFilter::True => true,
CompiledFilter::Invalid(_) => false,
CompiledFilter::Expr(e) => ctx.eval(e).is_truthy(),
CompiledFilter::And(items) => items.iter().all(|f| f.matches(ctx)),
CompiledFilter::Or(items) => items.iter().any(|f| f.matches(ctx)),
CompiledFilter::Not(inner) => !inner.matches(ctx),
}
}
pub fn errors(&self, out: &mut Vec<String>) {
match self {
CompiledFilter::Invalid(msg) => out.push(msg.clone()),
CompiledFilter::And(items) | CompiledFilter::Or(items) => {
items.iter().for_each(|f| f.errors(out))
}
CompiledFilter::Not(inner) => inner.errors(out),
_ => {}
}
}
}
pub fn compile(filter: &Option<Filter>) -> CompiledFilter {
match filter {
None => CompiledFilter::True,
Some(f) => compile_node(f),
}
}
pub fn combine(global: &Option<Filter>, view: &Option<Filter>) -> CompiledFilter {
let parts: Vec<CompiledFilter> = [global, view]
.into_iter()
.filter_map(|f| f.as_ref().map(compile_node))
.collect();
match parts.len() {
0 => CompiledFilter::True,
1 => parts.into_iter().next().unwrap(),
_ => CompiledFilter::And(parts),
}
}
fn compile_node(f: &Filter) -> CompiledFilter {
match f {
Filter::Expr(s) => match parse(s) {
Ok(e) => CompiledFilter::Expr(e),
Err(msg) => CompiledFilter::Invalid(format!("{s}: {msg}")),
},
Filter::List(items) => CompiledFilter::And(items.iter().map(compile_node).collect()),
Filter::Logic(logic) => compile_logic(logic),
}
}
fn compile_logic(logic: &LogicFilter) -> CompiledFilter {
let mut parts: Vec<CompiledFilter> = Vec::new();
if let Some(and) = &logic.and {
parts.push(CompiledFilter::And(and.iter().map(compile_node).collect()));
}
if let Some(or) = &logic.or {
parts.push(CompiledFilter::Or(or.iter().map(compile_node).collect()));
}
if let Some(not) = &logic.not {
let inner = match &**not {
NotFilter::One(f) => compile_node(f),
NotFilter::Many(fs) => CompiledFilter::And(fs.iter().map(compile_node).collect()),
};
parts.push(CompiledFilter::Not(Box::new(inner)));
}
match parts.len() {
0 => CompiledFilter::True,
1 => parts.into_iter().next().unwrap(),
_ => CompiledFilter::And(parts),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::parse_base;
use crate::note::{Corpus, Note};
use crate::value::{BaseLink, Value};
use std::collections::BTreeMap;
fn matches(filter: &Option<Filter>, note: &Note) -> bool {
let corpus = Corpus::new(vec![note.clone()]);
let formulas = BTreeMap::new();
let ctx = EvalCtx::new(note, &corpus, &formulas);
compile(filter).matches(&ctx)
}
fn note_in_folder(folder: &str, cats: Vec<&str>) -> Note {
let mut n = Note::default();
n.folder = folder.to_string();
n.properties.insert(
"categories".into(),
Value::List(
cats.into_iter()
.map(|c| Value::Link(BaseLink::new(c)))
.collect(),
),
);
n
}
#[test]
fn and_with_negation() {
let base = parse_base(
"filters:\n and:\n - categories.contains(link(\"Books\"))\n - '!file.inFolder(\"Misc\")'\n",
)
.unwrap();
assert!(matches(
&base.filters,
¬e_in_folder("Library", vec!["Books"])
));
assert!(!matches(
&base.filters,
¬e_in_folder("Misc", vec!["Books"])
));
assert!(!matches(
&base.filters,
¬e_in_folder("Library", vec!["Films"])
));
}
#[test]
fn or_combinator() {
let base =
parse_base("filters:\n or:\n - file.hasTag(\"a\")\n - file.hasTag(\"b\")\n")
.unwrap();
let mut n = Note::default();
n.tags = vec!["b".into()];
assert!(matches(&base.filters, &n));
n.tags = vec!["c".into()];
assert!(!matches(&base.filters, &n));
}
#[test]
fn combine_global_and_view() {
let base = parse_base("filters:\n and:\n - file.hasTag(\"x\")\n").unwrap();
let view_filter: Option<Filter> = Some(Filter::Expr("file.hasTag(\"y\")".into()));
let combined = combine(&base.filters, &view_filter);
let mut n = Note::default();
n.tags = vec!["x".into(), "y".into()];
let corpus = Corpus::new(vec![n.clone()]);
let formulas = BTreeMap::new();
let ctx = EvalCtx::new(&n, &corpus, &formulas);
assert!(combined.matches(&ctx));
let mut n2 = Note::default();
n2.tags = vec!["x".into()];
let corpus2 = Corpus::new(vec![n2.clone()]);
let ctx2 = EvalCtx::new(&n2, &corpus2, &formulas);
assert!(!combined.matches(&ctx2));
}
#[test]
fn invalid_expression_excludes_and_reports() {
let f = Some(Filter::Expr("this is ( not valid".into()));
let compiled = compile(&f);
let mut errs = Vec::new();
compiled.errors(&mut errs);
assert_eq!(errs.len(), 1);
assert!(!matches(&f, &Note::default()));
}
}