aboutsummaryrefslogtreecommitdiff
path: root/src/fexpr
diff options
context:
space:
mode:
Diffstat (limited to 'src/fexpr')
-rw-r--r--src/fexpr/grammar.lalrpop171
-rw-r--r--src/fexpr/mod.rs66
2 files changed, 237 insertions, 0 deletions
diff --git a/src/fexpr/grammar.lalrpop b/src/fexpr/grammar.lalrpop
new file mode 100644
index 0000000..58ec052
--- /dev/null
+++ b/src/fexpr/grammar.lalrpop
@@ -0,0 +1,171 @@
+use super::{
+ FError,
+ FErrorKind,
+ 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<dyn filters::log::LogFilter> = {
+ Disjunction<LogPredicate>,
+}
+
+PlayerFilter: Box<dyn filters::player::PlayerFilter> = {
+ Disjunction<PlayerPredicate>,
+}
+
+Disjunction<T>: T = {
+ <a:Disjunction<T>> "or" <b:Conjunction<T>> => a | b,
+ Conjunction<T>,
+}
+
+Conjunction<T>: T = {
+ <a:Conjunction<T>> "and"? <b:Negation<T>> => a & b,
+ Negation<T>,
+}
+
+Negation<T>: T = {
+ "not" <Negation<T>> => ! <>,
+ "!" <Negation<T>> => ! <>,
+ T,
+}
+
+LogPredicate: Box<dyn filters::log::LogFilter> = {
+ "-success" => filters::log::success(),
+ "-wipe" => filters::log::wipe(),
+ "-outcome" <Comma<FightOutcome>> => filters::log::outcome(<>),
+
+ "-weekday" <Comma<Weekday>> => filters::log::weekday(<>),
+ "-before" <Date> => filters::log::before(<>),
+ "-after" <Date> => filters::log::after(<>),
+
+ "-boss" <Comma<Boss>> => filters::log::boss(<>),
+
+ "-include" => filters::constant(true),
+ "-exclude" => filters::constant(false),
+
+ "-player" <Regex> => filters::player::any(
+ filters::player::character(<>.clone())
+ | filters::player::account(<>)
+ ),
+
+ "all" "(" "player" ":" <PlayerFilter> ")" => filters::player::all(<>),
+ "any" "(" "player" ":" <PlayerFilter> ")" => filters::player::any(<>),
+ "exists" "(" "player" ":" <PlayerFilter> ")" => filters::player::any(<>),
+
+ "(" <LogFilter> ")",
+}
+
+PlayerPredicate: Box<dyn filters::player::PlayerFilter> = {
+ "-character" <Regex> => filters::player::character(<>),
+ "-account" <Regex> => filters::player::account(<>),
+ "-name" <Regex> =>
+ filters::player::account(<>.clone())
+ | filters::player::character(<>),
+
+ "(" <PlayerFilter> ")",
+}
+
+Regex: Regex = {
+ <l:@L> <s:regex> =>? Regex::new(&s[1..s.len() - 1]).map_err(|error| ParseError::User {
+ error: FError {
+ location: l,
+ data: s.to_string(),
+ kind: error.into(),
+ }
+ }),
+ <l:@L> <s:word> =>? Regex::new(s).map_err(|error| ParseError::User {
+ error: FError {
+ location: l,
+ data: s.to_string(),
+ kind: error.into(),
+ }
+ }),
+}
+
+FightOutcome: FightOutcome = {
+ <l:@L> <w:word> =>? w.parse().map_err(|_| ParseError::User {
+ error: FError {
+ location: l,
+ data: w.into(),
+ kind: FErrorKind::InvalidFightOutcome,
+ }
+ }),
+}
+
+Weekday: Weekday = {
+ <l:@L> <w:word> =>? w.parse().map_err(|_| ParseError::User {
+ error: FError {
+ location: l,
+ data: w.into(),
+ kind: FErrorKind::InvalidWeekday,
+ }
+ }),
+}
+
+Boss: Boss = {
+ <l:@L> <w:word> =>? w.parse().map_err(|_| ParseError::User {
+ error: FError {
+ location: l,
+ data: w.into(),
+ kind: FErrorKind::InvalidBoss,
+ }
+ }),
+}
+
+Date: NaiveDateTime = {
+ <l:@L> <d:datetime> =>? 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(),
+ }
+ }),
+ <l:@L> <d:date> =>? 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(),
+ }
+ }),
+}
+
+Comma<T>: HashSet<T> = {
+ <v:(<T> ",")*> <e:T> => {
+ let mut result = v.into_iter().collect::<HashSet<_>>();
+ result.insert(e);
+ result
+ },
+}
+
+match {
+ "player" => "player",
+ "not" => "not",
+ "or" => "or",
+ "and" => "and",
+ "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,
+ r#""[^"]*""# => regex,
+
+ _
+}
diff --git a/src/fexpr/mod.rs b/src/fexpr/mod.rs
new file mode 100644
index 0000000..5610aba
--- /dev/null
+++ b/src/fexpr/mod.rs
@@ -0,0 +1,66 @@
+//! 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 std::{error, fmt};
+
+use lalrpop_util::{lalrpop_mod, lexer::Token, ParseError};
+use thiserror::Error;
+
+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 FErrorKind {
+ #[error("invalid regular expression: {0}")]
+ InvalidRegex(#[from] regex::Error),
+ #[error("invalid fight outcome")]
+ InvalidFightOutcome,
+ #[error("invalid weekday")]
+ InvalidWeekday,
+ #[error("invalid timestamp: {0}")]
+ 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<'a>(
+ input: &'a str,
+) -> Result<Box<dyn filters::log::LogFilter>, ParseError<usize, Token<'a>, FError>> {
+ grammar::LogFilterParser::new().parse(input)
+}
+
+/// Extract the location from the given error.
+pub fn location<T>(err: &ParseError<usize, T, FError>) -> 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,
+ }
+}