add regex support to text search

This commit is contained in:
kageru 2023-09-26 13:40:47 +02:00
parent 2446aefba1
commit 59fffc1bf9
3 changed files with 48 additions and 22 deletions

@ -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. // greater/less than aren’t supported for string fields.
_ => false, _ => 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. // Currently only for sets the card was released in.
(Value::Multiple(field), query @ Value::String(_)) => match op { (Value::Multiple(field), query @ Value::String(_)) => match op {
Operator::Equal => field.iter().any(|f| f == query), 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; let astral_pack_4_filter = parse_filters("set:ap04").unwrap().1;
assert!(!astral_pack_4_filter[0](&lacooda)); assert!(!astral_pack_4_filter[0](&lacooda));
} }
#[test]
fn regex_filter_test() {
let lacooda = SearchCard::from(&serde_json::from_str::<Card>(RAW_MONSTER).unwrap());
let bls = SearchCard::from(&serde_json::from_str::<Card>(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));
}
} }

@ -1,4 +1,4 @@
#![feature(lazy_cell)] #![feature(lazy_cell, try_blocks)]
use actix_web::{get, http::header, web, App, Either, HttpResponse, HttpServer}; use actix_web::{get, http::header, web, App, Either, HttpResponse, HttpServer};
use data::{Card, CardInfo, Set}; use data::{Card, CardInfo, Set};
use filter::SearchCard; use filter::SearchCard;

@ -14,6 +14,7 @@ use nom::{
sequence::{delimited, preceded, tuple}, sequence::{delimited, preceded, tuple},
IResult, IResult,
}; };
use regex::Regex;
pub fn parse_filters(input: &str) -> Result<(Vec<RawCardFilter>, Vec<CardFilter>), String> { pub fn parse_filters(input: &str) -> Result<(Vec<RawCardFilter>, Vec<CardFilter>), String> {
parse_raw_filters(input).map_err(|e| format!("Error while parsing filters “{input}”: {e:?}")).and_then(|(rest, mut v)| { 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<RawCardFilter>> {
} }
fn word_non_empty(input: &str) -> IResult<&str, &str> { 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<String, String> { fn sanitize(query: &str) -> Result<String, String> {
if query.contains(OPERATOR_CHARS) || query.is_empty() { if query.is_empty() {
Err(format!("Invalid query: {query}")) Err("Query must not be empty".to_owned())
} else { } else {
Ok(query.to_lowercase()) Ok(query.to_lowercase())
} }
} }
fn fallback_filter(query: &str) -> Result<RawCardFilter, String> { fn fallback_filter(query: &str) -> Result<RawCardFilter, String> {
let q = sanitize(query)?; Ok(RawCardFilter(Field::Name, Operator::Equal, Value::String(sanitize(query)?)))
Ok(RawCardFilter(Field::Name, Operator::Equal, Value::String(q)))
} }
fn parse_raw_filter(input: &str) -> IResult<&str, RawCardFilter> { fn parse_raw_filter(input: &str) -> IResult<&str, RawCardFilter> {
@ -87,6 +87,7 @@ fn values(input: &str) -> IResult<&str, Value> {
map_res( map_res(
alt(( alt((
delimited(char('"'), take_until1("\""), char('"')), delimited(char('"'), take_until1("\""), char('"')),
recognize(delimited(char('/'), take_until1("/"), char('/'))),
recognize(separated_list1(char('|'), take_until1(" |"))), recognize(separated_list1(char('|'), take_until1(" |"))),
take_until1(" "), take_until1(" "),
rest, rest,
@ -106,7 +107,12 @@ fn parse_values(input: &str) -> Result<Value, String> {
fn parse_single_value(input: &str) -> Result<Value, String> { fn parse_single_value(input: &str) -> Result<Value, String> {
Ok(match input.parse() { Ok(match input.parse() {
Ok(n) => Value::Numerical(n), 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 { pub enum Value {
String(String), String(String),
Regex(Regex),
Numerical(i32), Numerical(i32),
Multiple(Vec<Value>), Multiple(Vec<Value>),
#[default] #[default]
None, 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 { impl Display for Value {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self { match &self {
@ -251,10 +274,11 @@ impl Display for Value {
f.write_str(s) f.write_str(s)
} }
} }
Self::Regex(r) => write!(f, "Regex \"{}\"", r.as_str()),
Self::Numerical(n) => write!(f, "{n}"), Self::Numerical(n) => write!(f, "{n}"),
Self::Multiple(m) => { Self::Multiple(m) => {
write!(f, "one of [{}]", m.iter().map(Value::to_string).join(", ")) write!(f, "one of [{}]", m.iter().map(Value::to_string).join(", "))
}, }
Self::None => f.write_str("none"), Self::None => f.write_str("none"),
} }
} }
@ -277,19 +301,6 @@ mod tests {
parse_raw_filter(input) 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] #[test]
fn sequential_parsing_test() { fn sequential_parsing_test() {
let (rest, filter) = parse_raw_filter("atk>=100 l:4").unwrap(); let (rest, filter) = parse_raw_filter("atk>=100 l:4").unwrap();