From 59fffc1bf97f3740a09412ff0c5b538aa8812ae9 Mon Sep 17 00:00:00 2001 From: kageru Date: Tue, 26 Sep 2023 13:40:47 +0200 Subject: [PATCH] add regex support to text search --- src/filter.rs | 15 +++++++++++++++ src/main.rs | 2 +- src/parser.rs | 53 +++++++++++++++++++++++++++++++-------------------- 3 files changed, 48 insertions(+), 22 deletions(-) diff --git a/src/filter.rs b/src/filter.rs index 0771fe9..add7286 100644 --- a/src/filter.rs +++ b/src/filter.rs @@ -81,6 +81,12 @@ fn filter_value(op: &Operator, field_value: &Value, query_value: &Value) -> bool // greater/less than aren’t supported for string fields. _ => false, }, + (Value::String(field), Value::Regex(query)) => match op { + Operator::Equal => query.is_match(field), + Operator::NotEqual => !query.is_match(field), + // greater/less than aren’t supported for string fields. + _ => false, + }, // Currently only for sets the card was released in. (Value::Multiple(field), query @ Value::String(_)) => match op { Operator::Equal => field.iter().any(|f| f == query), @@ -151,4 +157,13 @@ mod tests { let astral_pack_4_filter = parse_filters("set:ap04").unwrap().1; assert!(!astral_pack_4_filter[0](&lacooda)); } + + #[test] + fn regex_filter_test() { + let lacooda = SearchCard::from(&serde_json::from_str::(RAW_MONSTER).unwrap()); + let bls = SearchCard::from(&serde_json::from_str::(RAW_LINK_MONSTER).unwrap()); + let draw_filter = parse_filters("o:/draw \\d cards?/").unwrap().1; + assert!(draw_filter[0](&lacooda)); + assert!(!draw_filter[0](&bls)); + } } diff --git a/src/main.rs b/src/main.rs index dec237a..f5fcc90 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -#![feature(lazy_cell)] +#![feature(lazy_cell, try_blocks)] use actix_web::{get, http::header, web, App, Either, HttpResponse, HttpServer}; use data::{Card, CardInfo, Set}; use filter::SearchCard; diff --git a/src/parser.rs b/src/parser.rs index 2ac3834..827055b 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -14,6 +14,7 @@ use nom::{ sequence::{delimited, preceded, tuple}, IResult, }; +use regex::Regex; pub fn parse_filters(input: &str) -> Result<(Vec, Vec), String> { parse_raw_filters(input).map_err(|e| format!("Error while parsing filters “{input}”: {e:?}")).and_then(|(rest, mut v)| { @@ -47,20 +48,19 @@ fn parse_raw_filters(input: &str) -> IResult<&str, Vec> { } fn word_non_empty(input: &str) -> IResult<&str, &str> { - verify(alt((take_until1(" "), rest)), |s: &str| s.len() >= 2)(input) + verify(alt((take_until1(" "), rest)), |s: &str| !s.is_empty())(input) } fn sanitize(query: &str) -> Result { - if query.contains(OPERATOR_CHARS) || query.is_empty() { - Err(format!("Invalid query: {query}")) + if query.is_empty() { + Err("Query must not be empty".to_owned()) } else { Ok(query.to_lowercase()) } } fn fallback_filter(query: &str) -> Result { - let q = sanitize(query)?; - Ok(RawCardFilter(Field::Name, Operator::Equal, Value::String(q))) + Ok(RawCardFilter(Field::Name, Operator::Equal, Value::String(sanitize(query)?))) } fn parse_raw_filter(input: &str) -> IResult<&str, RawCardFilter> { @@ -87,6 +87,7 @@ fn values(input: &str) -> IResult<&str, Value> { map_res( alt(( delimited(char('"'), take_until1("\""), char('"')), + recognize(delimited(char('/'), take_until1("/"), char('/'))), recognize(separated_list1(char('|'), take_until1(" |"))), take_until1(" "), rest, @@ -106,7 +107,12 @@ fn parse_values(input: &str) -> Result { fn parse_single_value(input: &str) -> Result { Ok(match input.parse() { Ok(n) => Value::Numerical(n), - Err(_) => Value::String(sanitize(input)?), + Err(_) => { + match try { Value::Regex(Regex::new(&input.strip_prefix('/')?.strip_suffix('/')?.to_lowercase()).ok()?) } { + Some(regex) => regex, + None => Value::String(sanitize(input)?), + } + } }) } @@ -232,15 +238,32 @@ impl Display for RawCardFilter { } } -#[derive(Debug, PartialEq, Eq, Clone, Default)] +#[derive(Debug, Clone, Default)] pub enum Value { String(String), + Regex(Regex), Numerical(i32), Multiple(Vec), #[default] None, } +// Manually implementing this because `Regex` isn’t `PartialEq` +impl PartialEq for Value { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Value::String(s1), Value::String(s2)) => s1 == s2, + (Value::Numerical(a), Value::Numerical(b)) => a == b, + (Value::Multiple(v1), Value::Multiple(v2)) => v1 == v2, + (Value::Regex(r1), Value::Regex(r2)) => r1.as_str() == r2.as_str(), + (Value::None, Value::None) => true, + _ => false, + } + } +} + +impl Eq for Value {} + impl Display for Value { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match &self { @@ -251,10 +274,11 @@ impl Display for Value { f.write_str(s) } } + Self::Regex(r) => write!(f, "Regex \"{}\"", r.as_str()), Self::Numerical(n) => write!(f, "{n}"), Self::Multiple(m) => { write!(f, "one of [{}]", m.iter().map(Value::to_string).join(", ")) - }, + } Self::None => f.write_str("none"), } } @@ -277,19 +301,6 @@ mod tests { parse_raw_filter(input) } - #[test_case("atk<=>1")] - #[test_case("atk=50|")] - #[test_case("def=|")] - #[test_case("l===10")] - #[test_case("t=")] - #[test_case("=100")] - #[test_case("a")] - fn unsuccessful_parsing_test(input: &str) { - if let Ok((filters, _)) = parse_filters(input) { - assert!(false, "Should have failed, but parsed as {filters:?}"); - } - } - #[test] fn sequential_parsing_test() { let (rest, filter) = parse_raw_filter("atk>=100 l:4").unwrap();