From 74dc6574650a157ab57779dc633e140d020b792a Mon Sep 17 00:00:00 2001
From: Daniel <kingdread@gmx.de>
Date: Mon, 4 May 2020 15:25:33 +0200
Subject: add -class player filter

---
 raidgrep.1.asciidoc       |  8 ++++++++
 src/fexpr/grammar.lalrpop | 13 +++++++++++++
 src/fexpr/mod.rs          |  4 +++-
 src/filters/player.rs     | 26 ++++++++++++++++++++++++--
 src/main.rs               |  1 +
 src/playerclass.rs        |  6 ++++++
 6 files changed, 55 insertions(+), 3 deletions(-)

diff --git a/raidgrep.1.asciidoc b/raidgrep.1.asciidoc
index 78dcf78..1cbdf85 100644
--- a/raidgrep.1.asciidoc
+++ b/raidgrep.1.asciidoc
@@ -139,6 +139,14 @@ The following predicates have to be wrapped in either a *any(player: ...)* or
     Shorthand that matches if the character or the account name match the given
     regular expression.
 
+*-class* 'CLASSES'::
+    Match the player if they have one of the given classes. Note that a core
+    class won't match its elite specializations, so _Guardian_ won't match
+    _Dragonhunter_. +
+    +
+    Names can be comma separated, in which case the player must have any of the
+    listed classes.
+
 === Boss Names
 
 Bosses can be referred to by their official name, although if that name
diff --git a/src/fexpr/grammar.lalrpop b/src/fexpr/grammar.lalrpop
index 654722b..f91da19 100644
--- a/src/fexpr/grammar.lalrpop
+++ b/src/fexpr/grammar.lalrpop
@@ -3,6 +3,7 @@ use super::{
     FErrorKind,
     FightOutcome,
     filters,
+    PlayerClass,
     SearchField,
 };
 use evtclib::Boss;
@@ -77,6 +78,8 @@ PlayerPredicate: Box<dyn filters::player::PlayerFilter> = {
         filters::player::account(<>.clone())
         | filters::player::character(<>),
 
+    "-class" <Comma<PlayerClass>> => filters::player::class(<>),
+
     "(" <PlayerFilter> ")",
 }
 
@@ -135,6 +138,16 @@ Boss: Boss = {
     }),
 }
 
+PlayerClass: PlayerClass = {
+    <l:@L> <w:word> =>? w.parse().map_err(|_| ParseError::User {
+        error: FError {
+            location: l,
+            data: w.into(),
+            kind: FErrorKind::InvalidClass,
+        }
+    }),
+}
+
 Date: DateTime<Utc> = {
     <l:@L> <d:datetime> =>? Local.datetime_from_str(d, "%Y-%m-%d %H:%M:%S")
         .map_err(|error| ParseError::User {
diff --git a/src/fexpr/mod.rs b/src/fexpr/mod.rs
index 2bdbfe7..452d66c 100644
--- a/src/fexpr/mod.rs
+++ b/src/fexpr/mod.rs
@@ -3,7 +3,7 @@
 //! 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};
+use super::{filters, playerclass::PlayerClass, FightOutcome, SearchField};
 
 use std::{error, fmt};
 
@@ -44,6 +44,8 @@ pub enum FErrorKind {
     InvalidTimestamp(#[from] chrono::format::ParseError),
     #[error("invalid boss name")]
     InvalidBoss,
+    #[error("invalid class name")]
+    InvalidClass,
 }
 
 /// Shortcut to create a new parser and parse the given input.
diff --git a/src/filters/player.rs b/src/filters/player.rs
index 3af2be2..2b14eb0 100644
--- a/src/filters/player.rs
+++ b/src/filters/player.rs
@@ -3,12 +3,12 @@
 //! Additionally, it provides methods to lift a player filter to a log filter with [`any`][any] and
 //! [`all`][all].
 use super::{
-    super::{guilds, EarlyLogResult, LogResult, Player, SearchField},
+    super::{guilds, playerclass::PlayerClass, EarlyLogResult, LogResult, Player, SearchField},
     log::LogFilter,
     Filter, Inclusion,
 };
 
-use std::convert::TryFrom;
+use std::{collections::HashSet, convert::TryFrom};
 
 use evtclib::{Agent, AgentKind};
 
@@ -113,3 +113,25 @@ pub fn character(regex: Regex) -> Box<dyn PlayerFilter> {
 pub fn account(regex: Regex) -> Box<dyn PlayerFilter> {
     name(SearchField::Account, regex)
 }
+
+#[derive(Clone, Debug)]
+struct ClassFilter(HashSet<PlayerClass>);
+
+impl Filter<Agent, Player> for ClassFilter {
+    fn filter_early(&self, agent: &Agent) -> Inclusion {
+        if let AgentKind::Player(ref player) = agent.kind() {
+            self.0.contains(&player.into()).into()
+        } else {
+            Inclusion::Unknown
+        }
+    }
+
+    fn filter(&self, player: &Player) -> bool {
+        self.0.contains(&player.profession)
+    }
+}
+
+/// Construct a `PlayerFilter` that matches only the given classes.
+pub fn class(classes: HashSet<PlayerClass>) -> Box<dyn PlayerFilter> {
+    Box::new(ClassFilter(classes))
+}
diff --git a/src/main.rs b/src/main.rs
index d9f0817..bf4c472 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -43,6 +43,7 @@ const APP_NAME: &str = "raidgrep";
 ///     -character REGEX        True if the character name matches the regex.
 ///     -account REGEX          True if the account name matches the regex.
 ///     -name REGEX             True if either character or account name match.
+///     -class CLASSES          True if the player has one of the listed classes.
 ///
 ///     -success                Only include successful logs.
 ///     -wipe                   Only include failed logs.
diff --git a/src/playerclass.rs b/src/playerclass.rs
index 77b9794..247e8b1 100644
--- a/src/playerclass.rs
+++ b/src/playerclass.rs
@@ -59,6 +59,12 @@ impl From<(Profession, Option<EliteSpec>)> for PlayerClass {
     }
 }
 
+impl From<&evtclib::Player> for PlayerClass {
+    fn from(player: &evtclib::Player) -> Self {
+        (player.profession(), player.elite()).into()
+    }
+}
+
 impl fmt::Display for PlayerClass {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         use EliteSpec::*;
-- 
cgit v1.2.3