diff options
| -rw-r--r-- | Cargo.toml | 5 | ||||
| -rw-r--r-- | build.rs | 5 | ||||
| -rw-r--r-- | src/fexpr/grammar.lalrpop | 131 | ||||
| -rw-r--r-- | src/fexpr/mod.rs | 25 | 
4 files changed, 166 insertions, 0 deletions
| @@ -24,3 +24,8 @@ serde = { version = "1.0", features = ["derive"] }  serde_json = "1.0"  dirs = "2.0"  log = { version = "0.4", features = ["std"] } +thiserror = "1.0" +lalrpop-util = "0.18" + +[build-dependencies] +lalrpop = { version = "0.18", features = ["lexer"] } diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..23c7d3f --- /dev/null +++ b/build.rs @@ -0,0 +1,5 @@ +extern crate lalrpop; + +fn main() { +    lalrpop::process_root().unwrap(); +} 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<dyn filters::log::LogFilter> = { +    Disjunction<LogPredicate>, +} + +PlayerFilter: Box<dyn filters::player::PlayerFilter> = { +    Disjunction<PlayerPredicate>, +} + +Disjunction<T>: T = { +    <a:Conjunction<T>> "or" <b:Conjunction<T>> => a | b, +    Conjunction<T>, +} + +Conjunction<T>: T = { +    <a:Negation<T>> "and"? <b:Negation<T>> => a & b, +    Negation<T>, +} + +Negation<T>: T = { +    "not" <T> => ! <>, +    "!" <T> => ! <>, +    T, +} + +LogPredicate: Box<dyn filters::log::LogFilter> = { +    "-success" => filters::log::OutcomeFilter::success(), +    "-wipe" => filters::log::OutcomeFilter::wipe(), +    "-outcome" <Comma<FightOutcome>> => filters::log::OutcomeFilter::new(<>), + +    "-weekday" <Comma<Weekday>> => filters::log::WeekdayFilter::new(<>), +    "-before" <Date> => filters::log::TimeFilter::new(None, Some(<>)), +    "-after" <Date> => filters::log::TimeFilter::new(Some(<>), None), + +    "-boss" <Comma<Boss>> => filters::log::BossFilter::new(<>), + +    "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::NameFilter::new(SearchField::Character, <>), +    "-account" <Regex> => filters::player::NameFilter::new(SearchField::Account, <>), +    "-name" <Regex> => +        filters::player::NameFilter::new(SearchField::Account, <>.clone()) +        | filters::player::NameFilter::new(SearchField::Character, <>), + +    "(" <PlayerFilter> ")", +} + +Regex: Regex = { +    <s:r#""[^"]*""#> =>? Regex::new(&s[1..s.len() - 1]).map_err(|_| ParseError::User { +        error: FError::InvalidRegex(s.into()), +    }), +    <s:word> =>? Regex::new(s).map_err(|e| ParseError::User { +        error: FError::InvalidRegex(s.into()), +    }), +} + +FightOutcome: FightOutcome = { +    <word> =>? <>.parse().map_err(|_| ParseError::User { +        error: FError::InvalidFightOutcome(<>.into()), +    }), +} + +Weekday: Weekday = { +    <word> =>? <>.parse().map_err(|_| ParseError::User { +        error: FError::InvalidWeekday(<>.into()), +    }), +} + +Boss: Boss = { +    <word> =>? <>.parse().map_err(|_| ParseError::User { +        error: FError::InvalidBoss(<>.into()), +    }), +} + +Date: NaiveDateTime = { +    <datetime> =>? NaiveDateTime::parse_from_str(<>, "%Y-%m-%d %H:%M:%S") +        .map_err(|_| ParseError::User { +            error: FError::InvalidTimestamp(<>.into()), +        }), +    <date> =>? NaiveDateTime::parse_from_str(&format!("{} 00:00:00", <>), "%Y-%m-%d %H:%M:%S") +        .map_err(|_| ParseError::User { +            error: FError::InvalidTimestamp(<>.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", +    "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), +} | 
