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

View File

@ -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::<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));
}
}

View File

@ -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;

View File

@ -14,6 +14,7 @@ use nom::{
sequence::{delimited, preceded, tuple},
IResult,
};
use regex::Regex;
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)| {
@ -47,20 +48,19 @@ fn parse_raw_filters(input: &str) -> IResult<&str, Vec<RawCardFilter>> {
}
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> {
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<RawCardFilter, String> {
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<Value, String> {
fn parse_single_value(input: &str) -> Result<Value, String> {
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<Value>),
#[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();