From e19519e155af95698807f377a5f6b525e255c4e5 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 18 Apr 2020 15:12:21 +0200 Subject: first version of the new filter pipeline --- src/fexpr/grammar.lalrpop | 131 ++++++++++++++++++++++++++++++++++++++++++++++ src/fexpr/mod.rs | 25 +++++++++ 2 files changed, 156 insertions(+) create mode 100644 src/fexpr/grammar.lalrpop create mode 100644 src/fexpr/mod.rs (limited to 'src/fexpr') diff --git a/src/fexpr/grammar.lalrpop b/src/fexpr/grammar.lalrpop new file mode 100644 index 0000000..cb16153 --- /dev/null +++ b/src/fexpr/grammar.lalrpop @@ -0,0 +1,131 @@ +use super::{ + FError, + FightOutcome, + filters, + SearchField, + Weekday, +}; +use evtclib::statistics::gamedata::Boss; +use std::collections::HashSet; +use lalrpop_util::ParseError; + +use chrono::NaiveDateTime; +use regex::Regex; + +grammar; + +extern { + type Error = FError; +} + +pub LogFilter: Box = { + Disjunction, +} + +PlayerFilter: Box = { + Disjunction, +} + +Disjunction: T = { + > "or" > => a | b, + Conjunction, +} + +Conjunction: T = { + > "and"? > => a & b, + Negation, +} + +Negation: T = { + "not" => ! <>, + "!" => ! <>, + T, +} + +LogPredicate: Box = { + "-success" => filters::log::OutcomeFilter::success(), + "-wipe" => filters::log::OutcomeFilter::wipe(), + "-outcome" > => filters::log::OutcomeFilter::new(<>), + + "-weekday" > => filters::log::WeekdayFilter::new(<>), + "-before" => filters::log::TimeFilter::new(None, Some(<>)), + "-after" => filters::log::TimeFilter::new(Some(<>), None), + + "-boss" > => filters::log::BossFilter::new(<>), + + "all" "(" "player" ":" ")" => filters::player::all(<>), + "any" "(" "player" ":" ")" => filters::player::any(<>), + "exists" "(" "player" ":" ")" => filters::player::any(<>), + + "(" ")", +} + +PlayerPredicate: Box = { + "-character" => filters::player::NameFilter::new(SearchField::Character, <>), + "-account" => filters::player::NameFilter::new(SearchField::Account, <>), + "-name" => + filters::player::NameFilter::new(SearchField::Account, <>.clone()) + | filters::player::NameFilter::new(SearchField::Character, <>), + + "(" ")", +} + +Regex: Regex = { + =>? Regex::new(&s[1..s.len() - 1]).map_err(|_| ParseError::User { + error: FError::InvalidRegex(s.into()), + }), + =>? Regex::new(s).map_err(|e| ParseError::User { + error: FError::InvalidRegex(s.into()), + }), +} + +FightOutcome: FightOutcome = { + =>? <>.parse().map_err(|_| ParseError::User { + error: FError::InvalidFightOutcome(<>.into()), + }), +} + +Weekday: Weekday = { + =>? <>.parse().map_err(|_| ParseError::User { + error: FError::InvalidWeekday(<>.into()), + }), +} + +Boss: Boss = { + =>? <>.parse().map_err(|_| ParseError::User { + error: FError::InvalidBoss(<>.into()), + }), +} + +Date: NaiveDateTime = { + =>? NaiveDateTime::parse_from_str(<>, "%Y-%m-%d %H:%M:%S") + .map_err(|_| ParseError::User { + error: FError::InvalidTimestamp(<>.into()), + }), + =>? NaiveDateTime::parse_from_str(&format!("{} 00:00:00", <>), "%Y-%m-%d %H:%M:%S") + .map_err(|_| ParseError::User { + error: FError::InvalidTimestamp(<>.into()), + }), +} + +Comma: HashSet = { + ",")*> => { + let mut result = v.into_iter().collect::>(); + result.insert(e); + result + }, +} + +match { + "player" => "player", + "not" => "not", + "any" => "any", + "all" => "all", + "exists" => "exists", + + r"\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d" => datetime, + r"\d\d\d\d-\d\d-\d\d" => date, + r"\w+" => word, + + _ +} diff --git a/src/fexpr/mod.rs b/src/fexpr/mod.rs new file mode 100644 index 0000000..f2b1090 --- /dev/null +++ b/src/fexpr/mod.rs @@ -0,0 +1,25 @@ +//! Filter expression language. +//! +//! This module contains methods to parse a given string into an abstract filter tree, check its +//! type and convert it to a [`Filter`][super::filters::Filter]. +// Make it available in the grammar mod. +use super::{filters, FightOutcome, SearchField, Weekday}; +use lalrpop_util::lalrpop_mod; + +use thiserror::Error; + +lalrpop_mod!(pub grammar, "/fexpr/grammar.rs"); + +#[derive(Debug, Error)] +pub enum FError { + #[error("invalid regular expression: {0}")] + InvalidRegex(String), + #[error("invalid fight outcome: {0}")] + InvalidFightOutcome(String), + #[error("invalid weekday: {0}")] + InvalidWeekday(String), + #[error("invalid timestamp: {0}")] + InvalidTimestamp(String), + #[error("invalid boss name: {0}")] + InvalidBoss(String), +} -- cgit v1.2.3 From 569c17607297dbbd57462a9603861d9fe619fd2d Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 20 Apr 2020 13:47:00 +0200 Subject: Add -player as a shortcut to search player names --- src/fexpr/grammar.lalrpop | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'src/fexpr') diff --git a/src/fexpr/grammar.lalrpop b/src/fexpr/grammar.lalrpop index cb16153..48349a1 100644 --- a/src/fexpr/grammar.lalrpop +++ b/src/fexpr/grammar.lalrpop @@ -53,6 +53,11 @@ LogPredicate: Box = { "-boss" > => filters::log::BossFilter::new(<>), + "-player" => filters::player::any( + filters::player::NameFilter::new(SearchField::Character, <>.clone()) + | filters::player::NameFilter::new(SearchField::Account, <>) + ), + "all" "(" "player" ":" ")" => filters::player::all(<>), "any" "(" "player" ":" ")" => filters::player::any(<>), "exists" "(" "player" ":" ")" => filters::player::any(<>), -- cgit v1.2.3 From 0e4e148a0890ba206df40cffe5a5f1cc47c8079e Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 20 Apr 2020 14:27:42 +0200 Subject: hook up new expression parser to command line args This method is not perfect yet, because 1. The items are not documented as they were before 2. You need to separate the args with --, otherwise Clap tries to parse them as optional flags This should be fixed (especially the documentation part) before merging into master. --- src/fexpr/mod.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'src/fexpr') diff --git a/src/fexpr/mod.rs b/src/fexpr/mod.rs index f2b1090..aafdea7 100644 --- a/src/fexpr/mod.rs +++ b/src/fexpr/mod.rs @@ -4,7 +4,7 @@ //! type and convert it to a [`Filter`][super::filters::Filter]. // Make it available in the grammar mod. use super::{filters, FightOutcome, SearchField, Weekday}; -use lalrpop_util::lalrpop_mod; +use lalrpop_util::{lalrpop_mod, lexer::Token, ParseError}; use thiserror::Error; @@ -23,3 +23,9 @@ pub enum FError { #[error("invalid boss name: {0}")] InvalidBoss(String), } + +pub fn parse_logfilter( + input: &str, +) -> Result, ParseError> { + grammar::LogFilterParser::new().parse(input) +} -- cgit v1.2.3 From 185a5b2f802f9d05c3eb40f807c0488f168c6661 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 21 Apr 2020 13:59:28 +0200 Subject: better error outputs --- src/fexpr/grammar.lalrpop | 62 +++++++++++++++++++++++++++++++++++------------ src/fexpr/mod.rs | 57 ++++++++++++++++++++++++++++++++++--------- 2 files changed, 92 insertions(+), 27 deletions(-) (limited to 'src/fexpr') diff --git a/src/fexpr/grammar.lalrpop b/src/fexpr/grammar.lalrpop index 48349a1..4e6ac89 100644 --- a/src/fexpr/grammar.lalrpop +++ b/src/fexpr/grammar.lalrpop @@ -1,5 +1,6 @@ use super::{ FError, + FErrorKind, FightOutcome, filters, SearchField, @@ -76,40 +77,68 @@ PlayerPredicate: Box = { } Regex: Regex = { - =>? Regex::new(&s[1..s.len() - 1]).map_err(|_| ParseError::User { - error: FError::InvalidRegex(s.into()), + =>? Regex::new(&s[1..s.len() - 1]).map_err(|error| ParseError::User { + error: FError { + location: l, + data: s.to_string(), + kind: error.into(), + } }), - =>? Regex::new(s).map_err(|e| ParseError::User { - error: FError::InvalidRegex(s.into()), + =>? Regex::new(s).map_err(|error| ParseError::User { + error: FError { + location: l, + data: s.to_string(), + kind: error.into(), + } }), } FightOutcome: FightOutcome = { - =>? <>.parse().map_err(|_| ParseError::User { - error: FError::InvalidFightOutcome(<>.into()), + =>? w.parse().map_err(|_| ParseError::User { + error: FError { + location: l, + data: w.into(), + kind: FErrorKind::InvalidFightOutcome, + } }), } Weekday: Weekday = { - =>? <>.parse().map_err(|_| ParseError::User { - error: FError::InvalidWeekday(<>.into()), + =>? w.parse().map_err(|_| ParseError::User { + error: FError { + location: l, + data: w.into(), + kind: FErrorKind::InvalidWeekday, + } }), } Boss: Boss = { - =>? <>.parse().map_err(|_| ParseError::User { - error: FError::InvalidBoss(<>.into()), + =>? w.parse().map_err(|_| ParseError::User { + error: FError { + location: l, + data: w.into(), + kind: FErrorKind::InvalidBoss, + } }), } Date: NaiveDateTime = { - =>? NaiveDateTime::parse_from_str(<>, "%Y-%m-%d %H:%M:%S") - .map_err(|_| ParseError::User { - error: FError::InvalidTimestamp(<>.into()), + =>? NaiveDateTime::parse_from_str(d, "%Y-%m-%d %H:%M:%S") + .map_err(|error| ParseError::User { + error: FError { + location: l, + data: d.into(), + kind: error.into(), + } }), - =>? NaiveDateTime::parse_from_str(&format!("{} 00:00:00", <>), "%Y-%m-%d %H:%M:%S") - .map_err(|_| ParseError::User { - error: FError::InvalidTimestamp(<>.into()), + =>? NaiveDateTime::parse_from_str(&format!("{} 00:00:00", d), "%Y-%m-%d %H:%M:%S") + .map_err(|error| ParseError::User { + error: FError { + location: l, + data: d.into(), + kind: error.into(), + } }), } @@ -131,6 +160,7 @@ match { r"\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d" => datetime, r"\d\d\d\d-\d\d-\d\d" => date, r"\w+" => word, + r#""[^"]*""# => regex, _ } diff --git a/src/fexpr/mod.rs b/src/fexpr/mod.rs index aafdea7..5754d94 100644 --- a/src/fexpr/mod.rs +++ b/src/fexpr/mod.rs @@ -4,28 +4,63 @@ //! type and convert it to a [`Filter`][super::filters::Filter]. // Make it available in the grammar mod. use super::{filters, FightOutcome, SearchField, Weekday}; -use lalrpop_util::{lalrpop_mod, lexer::Token, ParseError}; +use std::{error, fmt}; + +use lalrpop_util::{lalrpop_mod, lexer::Token, ParseError}; use thiserror::Error; -lalrpop_mod!(pub grammar, "/fexpr/grammar.rs"); +lalrpop_mod!(#[allow(clippy::all)] pub grammar, "/fexpr/grammar.rs"); + +#[derive(Debug)] +pub struct FError { + location: usize, + data: String, + kind: FErrorKind, +} + +impl fmt::Display for FError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{} (at {})", self.kind, self.location) + } +} + +impl error::Error for FError { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + Some(&self.kind) + } +} #[derive(Debug, Error)] -pub enum FError { +pub enum FErrorKind { #[error("invalid regular expression: {0}")] - InvalidRegex(String), - #[error("invalid fight outcome: {0}")] - InvalidFightOutcome(String), - #[error("invalid weekday: {0}")] - InvalidWeekday(String), + InvalidRegex(#[from] regex::Error), + #[error("invalid fight outcome")] + InvalidFightOutcome, + #[error("invalid weekday")] + InvalidWeekday, #[error("invalid timestamp: {0}")] - InvalidTimestamp(String), - #[error("invalid boss name: {0}")] - InvalidBoss(String), + InvalidTimestamp(#[from] chrono::format::ParseError), + #[error("invalid boss name")] + InvalidBoss, } +/// Shortcut to create a new parser and parse the given input. pub fn parse_logfilter( input: &str, ) -> Result, ParseError> { grammar::LogFilterParser::new().parse(input) } + +/// Extract the location from the given error. +pub fn location(err: &ParseError) -> usize { + match *err { + ParseError::InvalidToken { location } => location, + ParseError::UnrecognizedEOF { location, .. } => location, + ParseError::UnrecognizedToken { + token: (l, _, _), .. + } => l, + ParseError::ExtraToken { token: (l, _, _) } => l, + ParseError::User { ref error } => error.location, + } +} -- cgit v1.2.3 From d1f277892ec127b1fb83ad56de59b29c32695661 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 21 Apr 2020 14:03:13 +0200 Subject: add "or" and "and" to the list of tokens Otherwise they'd get tokenized as word and we couldn't build conjunctions/disjunctions. --- src/fexpr/grammar.lalrpop | 2 ++ 1 file changed, 2 insertions(+) (limited to 'src/fexpr') diff --git a/src/fexpr/grammar.lalrpop b/src/fexpr/grammar.lalrpop index 4e6ac89..f559ff1 100644 --- a/src/fexpr/grammar.lalrpop +++ b/src/fexpr/grammar.lalrpop @@ -153,6 +153,8 @@ Comma: HashSet = { match { "player" => "player", "not" => "not", + "or" => "or", + "and" => "and", "any" => "any", "all" => "all", "exists" => "exists", -- cgit v1.2.3 From 0ad7a333dc2b45f0ba658ea455284d086294a088 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 21 Apr 2020 14:06:15 +0200 Subject: grammar: fix precendence rules If we don't allow the higher-tier on the left side, we cannot chain multiple or/and on the same level. Since or is associative, we shouldn't expect the user to write (... or (... or ...)) and instead provide the flattened version as well. --- src/fexpr/grammar.lalrpop | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'src/fexpr') diff --git a/src/fexpr/grammar.lalrpop b/src/fexpr/grammar.lalrpop index f559ff1..caaaf7f 100644 --- a/src/fexpr/grammar.lalrpop +++ b/src/fexpr/grammar.lalrpop @@ -28,18 +28,18 @@ PlayerFilter: Box = { } Disjunction: T = { - > "or" > => a | b, + > "or" > => a | b, Conjunction, } Conjunction: T = { - > "and"? > => a & b, + > "and"? > => a & b, Negation, } Negation: T = { - "not" => ! <>, - "!" => ! <>, + "not" > => ! <>, + "!" > => ! <>, T, } -- cgit v1.2.3 From 0a27adbc0bf3bbbf87fea9e55c00c38f61d55058 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 21 Apr 2020 14:23:50 +0200 Subject: add a small repl --- src/fexpr/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'src/fexpr') diff --git a/src/fexpr/mod.rs b/src/fexpr/mod.rs index 5754d94..5610aba 100644 --- a/src/fexpr/mod.rs +++ b/src/fexpr/mod.rs @@ -46,9 +46,9 @@ pub enum FErrorKind { } /// Shortcut to create a new parser and parse the given input. -pub fn parse_logfilter( - input: &str, -) -> Result, ParseError> { +pub fn parse_logfilter<'a>( + input: &'a str, +) -> Result, ParseError, FError>> { grammar::LogFilterParser::new().parse(input) } -- cgit v1.2.3 From 5dbea93266c3a30dac5ec6f5a7915d73a440f573 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 25 Apr 2020 13:14:30 +0200 Subject: use free functions instead of Filter::new Having a ::new on each of the filter types was a bit weird, especially because we returned Box instead of Self (and clippy rightfully complained). With this patch, we now have a bunch of normal functions, and we don't show to the outside how a filter is actually implemented (or what struct is behind it). --- src/fexpr/grammar.lalrpop | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) (limited to 'src/fexpr') diff --git a/src/fexpr/grammar.lalrpop b/src/fexpr/grammar.lalrpop index caaaf7f..d8f64fa 100644 --- a/src/fexpr/grammar.lalrpop +++ b/src/fexpr/grammar.lalrpop @@ -44,19 +44,19 @@ Negation: T = { } LogPredicate: Box = { - "-success" => filters::log::OutcomeFilter::success(), - "-wipe" => filters::log::OutcomeFilter::wipe(), - "-outcome" > => filters::log::OutcomeFilter::new(<>), + "-success" => filters::log::success(), + "-wipe" => filters::log::wipe(), + "-outcome" > => filters::log::outcome(<>), - "-weekday" > => filters::log::WeekdayFilter::new(<>), - "-before" => filters::log::TimeFilter::new(None, Some(<>)), - "-after" => filters::log::TimeFilter::new(Some(<>), None), + "-weekday" > => filters::log::weekday(<>), + "-before" => filters::log::before(<>), + "-after" => filters::log::after(<>), - "-boss" > => filters::log::BossFilter::new(<>), + "-boss" > => filters::log::boss(<>), "-player" => filters::player::any( - filters::player::NameFilter::new(SearchField::Character, <>.clone()) - | filters::player::NameFilter::new(SearchField::Account, <>) + filters::player::character(<>.clone()) + | filters::player::account(<>) ), "all" "(" "player" ":" ")" => filters::player::all(<>), @@ -67,11 +67,11 @@ LogPredicate: Box = { } PlayerPredicate: Box = { - "-character" => filters::player::NameFilter::new(SearchField::Character, <>), - "-account" => filters::player::NameFilter::new(SearchField::Account, <>), + "-character" => filters::player::character(<>), + "-account" => filters::player::account(<>), "-name" => - filters::player::NameFilter::new(SearchField::Account, <>.clone()) - | filters::player::NameFilter::new(SearchField::Character, <>), + filters::player::account(<>.clone()) + | filters::player::character(<>), "(" ")", } -- cgit v1.2.3 From 9bbd5db2a6caae10f0ab2cf2625fbc34485a4ce9 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 25 Apr 2020 13:22:00 +0200 Subject: add -include and -exclude --- src/fexpr/grammar.lalrpop | 3 +++ 1 file changed, 3 insertions(+) (limited to 'src/fexpr') diff --git a/src/fexpr/grammar.lalrpop b/src/fexpr/grammar.lalrpop index d8f64fa..58ec052 100644 --- a/src/fexpr/grammar.lalrpop +++ b/src/fexpr/grammar.lalrpop @@ -54,6 +54,9 @@ LogPredicate: Box = { "-boss" > => filters::log::boss(<>), + "-include" => filters::constant(true), + "-exclude" => filters::constant(false), + "-player" => filters::player::any( filters::player::character(<>.clone()) | filters::player::account(<>) -- cgit v1.2.3