diff --git a/Cargo.lock b/Cargo.lock index e3e8550..232c588 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -56,6 +56,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + [[package]] name = "libc" version = "0.2.152" @@ -172,6 +178,7 @@ dependencies = [ name = "tag" version = "0.1.0" dependencies = [ + "lazy_static", "pest", "pest_derive", "walkdir", diff --git a/Cargo.toml b/Cargo.toml index 751bf2b..46b7b97 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +lazy_static = "1.4.0" pest = "2.7.6" pest_derive = "2.7.6" walkdir = "2.4.0" diff --git a/src/main.rs b/src/main.rs index f5149f6..a4d69dd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,8 @@ -use tag::search::get_tags_from_files; +use pest::Parser; +use tag::{ + parsers::query::{construct_query_ast, evaluate_ast, QueryParser, Rule}, + search::get_tags_from_files, +}; fn main() -> Result<(), Box> { let tagged_files = get_tags_from_files("testfiles")?; @@ -7,5 +11,17 @@ fn main() -> Result<(), Box> { println!("File {} contains {:?}", file.path.display(), file.tags); } + let ast = construct_query_ast( + QueryParser::parse(Rule::tagsearch, "(#a & #b | (#c & #d)) & #e") + .unwrap() + .next() + .unwrap() + .into_inner(), + vec!["#a", "#c", "#d", "#e"], + ); + + println!("{:#?}", ast); + println!("{}", evaluate_ast(ast)); + Ok(()) } diff --git a/src/parsers.rs b/src/parsers.rs index 6ed1cd6..a202265 100644 --- a/src/parsers.rs +++ b/src/parsers.rs @@ -9,17 +9,94 @@ pub mod tagline { } pub mod query { + use pest::{iterators::Pairs, pratt_parser::PrattParser}; use pest_derive::Parser; + /// Expr represents an AST for a search query. + #[derive(Debug, PartialEq, Clone)] + pub enum Expr { + Bool(bool), + Operation { + lhs: Box, + op: Op, + rhs: Box, + }, + } + + /// Op is an Operation that can be used in a query. + #[derive(Debug, PartialEq, Clone)] + pub enum Op { + And, + Or, + } + + lazy_static::lazy_static! { + static ref PRATT_PARSER: PrattParser = { + use pest::pratt_parser::{Assoc::*, Op}; + use Rule::*; + + PrattParser::new() + // & and | are evaluated with the same precedence + .op(Op::infix(and, Left) | Op::infix(or, Left)) + }; + } + #[derive(Parser)] #[grammar = "query.pest"] /// QueryParser is responsible for parsing the search query. /// The relevant rule is `tagsearch`. pub struct QueryParser; + + /// construct_query_ast() creates an AST from a string of symbols + /// lexed by the QueryParser and a list of tags. + pub fn construct_query_ast(pairs: Pairs, tags: Vec<&str>) -> Expr { + PRATT_PARSER + .map_primary(|primary| match primary.as_rule() { + Rule::tag => Expr::Bool(tags.contains(&primary.as_str().trim())), + Rule::expr => construct_query_ast(primary.into_inner(), tags.clone()), + rule => unreachable!("Expected tag, found {:?}", rule), + }) + .map_infix(|lhs, op, rhs| { + let op = match op.as_rule() { + Rule::or => Op::Or, + Rule::and => Op::And, + rule => unreachable!("Expected operation, found {:?}", rule), + }; + + Expr::Operation { + lhs: Box::new(lhs), + op, + rhs: Box::new(rhs), + } + }) + .parse(pairs) + } + + /// evaluate_ast() evaluates an AST created by construct_query_ast() + /// and returns the result. + pub fn evaluate_ast(ast: Expr) -> bool { + match ast { + Expr::Bool(value) => value, + Expr::Operation { lhs, op, rhs } => { + let left = evaluate_ast(*lhs); + let right = evaluate_ast(*rhs); + match op { + Op::Or => left | right, + Op::And => left & right, + } + } + } + } } #[cfg(test)] mod tests { + use crate::parsers::query::construct_query_ast; + use crate::parsers::query::evaluate_ast; + use crate::parsers::query::Expr; + use crate::parsers::query::Op; + use crate::parsers::query::QueryParser; + use super::query; use super::tagline; @@ -151,4 +228,111 @@ mod tests { assert_eq!(test_case.input, res.unwrap().as_str()) }) } + + #[test] + fn test_construct_query_ast() { + struct TestCase<'a> { + name: &'a str, + input_query: &'a str, + input_tags: Vec, + expected_ast: Expr, + } + + let test_cases = [ + TestCase { + name: "success_flat", + input_query: "#a & #b", + input_tags: vec![], + expected_ast: Expr::Operation { + lhs: Box::new(Expr::Bool(false)), + op: Op::And, + rhs: Box::new(Expr::Bool(false)), + }, + }, + TestCase { + name: "success_nested", + input_query: "#a & #b | (#c & #d)", + input_tags: vec!["#c".to_string(), "#d".to_string()], + expected_ast: Expr::Operation { + lhs: Box::new(Expr::Operation { + lhs: Box::new(Expr::Bool(false)), + op: Op::And, + rhs: Box::new(Expr::Bool(false)), + }), + op: Op::Or, + rhs: Box::new(Expr::Operation { + lhs: Box::new(Expr::Bool(true)), + op: Op::And, + rhs: Box::new(Expr::Bool(true)), + }), + }, + }, + ]; + + test_cases.iter().for_each(|test_case| { + println!("test_construct_query_ast: \n\t{}", test_case.name); + + let ast = construct_query_ast( + QueryParser::parse(query::Rule::tagsearch, test_case.input_query) + .unwrap() + .next() + .unwrap() + .into_inner(), + test_case + .input_tags + .iter() + .map(|tag| tag.as_str()) + .collect(), + ); + + assert_eq!(test_case.expected_ast, ast); + }) + } + + #[test] + fn test_evaluate_ast() { + struct TestCase<'a> { + name: &'a str, + input_ast: Expr, + expected_result: bool, + } + + let test_cases = [ + TestCase { + name: "success_flat", + input_ast: Expr::Operation { + lhs: Box::new(Expr::Bool(true)), + op: Op::And, + rhs: Box::new(Expr::Bool(true)), + }, + expected_result: true, + }, + TestCase { + name: "success_nested", + input_ast: Expr::Operation { + lhs: Box::new(Expr::Operation { + lhs: Box::new(Expr::Bool(false)), + op: Op::And, + rhs: Box::new(Expr::Bool(false)), + }), + op: Op::Or, + rhs: Box::new(Expr::Operation { + lhs: Box::new(Expr::Bool(true)), + op: Op::And, + rhs: Box::new(Expr::Bool(false)), + }), + }, + expected_result: false, + }, + ]; + + test_cases.iter().for_each(|test_case| { + println!("test_evaluate_ast: \n\t{}", test_case.name); + + assert_eq!( + test_case.expected_result, + evaluate_ast(test_case.input_ast.clone()) + ) + }) + } }