//! 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, playerclass::PlayerClass, FightOutcome}; use std::{error, fmt}; use itertools::Itertools; use lalrpop_util::{lalrpop_mod, lexer::Token, ParseError}; use thiserror::Error; // Lalrpop chokes on the associated type specification (it doesn't expect the =), so we need to // define those aliases here in Rust and then import and use them in the grammar. trait DateProducer = filters::values::Producer>; trait DurationProducer = filters::values::Producer; trait CountProducer = filters::values::Producer; lalrpop_mod!(#[allow(clippy::all)] pub grammar, "/fexpr/grammar.rs"); #[derive(Debug)] pub struct FError { location: usize, 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}")] Regex(#[from] regex::Error), #[error("invalid fight outcome")] FightOutcome, #[error("invalid weekday")] Weekday, #[error("invalid timestamp: {0}")] Timestamp(#[from] chrono::format::ParseError), #[error("invalid boss name")] Boss, #[error("invalid class name")] Class, } /// Shortcut to create a new parser and parse the given input. pub fn parse_logfilter( input: &str, ) -> Result, ParseError, FError>> { 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, } } /// "Re-quotes" a list of string pieces to a long, whitespace separated string. /// /// This function is needed because the shell already does some argument parsing, so if the user /// specifies `-player "godric gobbledygook"` on the command line, we will get `["-player", "godric /// gobbledygook"]` as the arguments. Howvever, our parser expects a single string, so we re-join /// the pieces and apply the quotes where necessary. /// /// Note that this works on a "best guess" method, as we cannot reconstruct the shell's quotes 1:1. /// This means that some things that work on the command line won't work in the REPL, and vice /// versa. /// /// ``` /// assert_eq!( /// requote(&["-player", "godric gobbledygook"]), /// r#"-player "godric gobbledygook""#, /// ); /// ``` pub fn requote, T: IntoIterator>(items: T) -> String { const SPECIAL_CHARS: &[char] = &[' ', '.', '^', '$', '+', '+']; items .into_iter() .map(|part| { let part = part.as_ref(); if part.contains(SPECIAL_CHARS) { format!(r#""{}""#, part) } else { part.into() } }) .join(" ") } #[cfg(test)] mod tests { use super::*; #[test] fn test_requote() { assert_eq!( requote(&["-player", "godric gobbledygook"]), r#"-player "godric gobbledygook""#, ); assert_eq!(requote(&["-player", "godric"]), r#"-player godric"#,); assert_eq!(requote(&["-player", "g.dric"]), r#"-player "g.dric""#,); } }