use the many combinators to parse the entire query at once

This commit is contained in:
kageru 2023-01-26 18:12:37 +01:00
parent fd5436954a
commit d906f18845
2 changed files with 52 additions and 31 deletions

@ -1,5 +1,5 @@
[package] [package]
name = "scryfall-ygo" name = "aro"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"

@ -1,12 +1,11 @@
#![feature(option_result_contains)] #![feature(option_result_contains)]
use nom::{ use nom::{
branch::alt, branch::alt,
bytes::{ bytes::complete::{take_until1, take_while, take_while_m_n},
complete::{take_until1, take_while_m_n}, character::complete::{alphanumeric1, multispace0},
streaming::take_while,
},
combinator::{complete, map_res, rest}, combinator::{complete, map_res, rest},
sequence::tuple, multi::many1,
sequence::{preceded, tuple},
IResult, IResult,
}; };
use serde::Deserialize; use serde::Deserialize;
@ -23,36 +22,46 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let cards = serde_json::from_reader::<_, CardInfo>(std::io::BufReader::new(std::fs::File::open("cards.json")?))?.data; let cards = serde_json::from_reader::<_, CardInfo>(std::io::BufReader::new(std::fs::File::open("cards.json")?))?.data;
let search_cards: Vec<_> = cards.iter().map(SearchCard::from).collect(); let search_cards: Vec<_> = cards.iter().map(SearchCard::from).collect();
let cards_by_id: HashMap<_, _> = cards.into_iter().map(|c| (c.id, c)).collect(); let cards_by_id: HashMap<_, _> = cards.into_iter().map(|c| (c.id, c)).collect();
let mut query = Vec::new(); let raw_query = std::env::args().nth(1).unwrap();
for q in std::env::args().skip(1) { let query = parse_filters(&raw_query)?;
match parse_filter(&q) {
Ok((_, filter)) => query.push(filter),
Err(e) => Err(format!("Malformed query fragment {q}: {e:?}"))?,
}
}
search_cards.iter().filter(|card| query.iter().all(|q| q(card))).for_each(|c| println!("{}", cards_by_id.get(&c.id).unwrap())); search_cards.iter().filter(|card| query.iter().all(|q| q(card))).for_each(|c| println!("{}", cards_by_id.get(&c.id).unwrap()));
Ok(()) Ok(())
} }
fn parse_filter(input: &str) -> IResult<&str, CardFilter> { fn parse_filters(input: &str) -> Result<Vec<CardFilter>, String> {
map_res(parse_raw_filter, build_filter)(input) parse_raw_filters(input).map_err(|e| format!("Error while parsing filters “{input}”: {e:?}")).and_then(|(rest, v)| {
if rest.is_empty() {
Ok(v.into_iter().map(build_filter).collect::<Result<Vec<_>, _>>()?)
} else {
Err(format!("Input was not fully parsed. Left over: “{rest}"))
}
})
}
fn parse_raw_filters(input: &str) -> IResult<&str, Vec<RawCardFilter>> {
many1(parse_raw_filter)(input)
} }
fn parse_raw_filter(input: &str) -> IResult<&str, RawCardFilter> { fn parse_raw_filter(input: &str) -> IResult<&str, RawCardFilter> {
alt(( preceded(
complete(tuple((field, operator, value))), multispace0,
map_res(take_until1(" "), |q| fallback_filter(q)), alt((
map_res(rest, |q| fallback_filter(q)), complete(tuple((field, operator, value))),
))(input) map_res(take_until1(" "), |q| fallback_filter(q)),
// I would like to use `rest` here, but that results in a pattern that can be empty
// which can lead to infinite loops while parsing and is therefore disallowed by nom.
// I would need something like rest1 or a way to assert that the rest isn’t empty.
map_res(alphanumeric1, |q| fallback_filter(q)),
)),
)(input)
} }
fn fallback_filter(query: &str) -> Result<RawCardFilter, String> { fn fallback_filter(query: &str) -> Result<RawCardFilter, String> {
if query.contains(&OPERATOR_CHARS[..]) { if query.contains(&OPERATOR_CHARS[..]) {
return Err(format!("Invalid query: {query}")); return Err(format!("Invalid query: {query}"));
} }
dbg!("Trying to match {query} as card name"); #[cfg(debug_assertions)]
println!("Trying to match {query} as card name");
let q = query.to_lowercase(); let q = query.to_lowercase();
Ok((Field::Name, Operator::Equals, Value::String(q))) Ok((Field::Name, Operator::Equals, Value::String(q)))
} }
@ -328,19 +337,31 @@ mod tests {
assert_eq!(parse_raw_filter("Necrovalley"), Ok(("", (Field::Name, Operator::Equals, Value::String("necrovalley".into()))))); assert_eq!(parse_raw_filter("Necrovalley"), Ok(("", (Field::Name, Operator::Equals, Value::String("necrovalley".into())))));
assert_eq!(parse_raw_filter("l=10"), Ok(("", (Field::Level, Operator::Equals, Value::Numerical(10))))); assert_eq!(parse_raw_filter("l=10"), Ok(("", (Field::Level, Operator::Equals, Value::Numerical(10)))));
let (rest, filter) = parse_raw_filter("atk>=100 l:4").unwrap();
assert_eq!(filter, (Field::Atk, Operator::GreaterEqual, Value::Numerical(100)));
assert_eq!(parse_raw_filter(rest), Ok(("", (Field::Level, Operator::Equals, Value::Numerical(4)))));
assert_eq!(
parse_raw_filters("atk>=100 l:4"),
Ok((
"",
vec![(Field::Atk, Operator::GreaterEqual, Value::Numerical(100)), (Field::Level, Operator::Equals, Value::Numerical(4))]
))
);
// These will fail during conversion // These will fail during conversion
assert!(parse_filter("l===10").is_err()); assert!(parse_filters("l===10").is_err());
assert!(parse_filter("t=").is_err()); assert!(parse_filters("t=").is_err());
assert!(parse_filter("=100").is_err()); assert!(parse_filters("=100").is_err());
assert!(parse_filter("atk<=>1").is_err()); assert!(parse_filters("atk<=>1").is_err());
} }
#[test] #[test]
fn level_filter_test() { fn level_filter_test() {
let lacooda = SearchCard::from(&serde_json::from_str::<Card>(RAW_MONSTER).unwrap()); let lacooda = SearchCard::from(&serde_json::from_str::<Card>(RAW_MONSTER).unwrap());
let filter_level_3 = parse_filter("l=3").unwrap().1; let filter_level_3 = parse_filters("l=3").unwrap();
assert!(filter_level_3(&lacooda)); assert!(filter_level_3[0](&lacooda));
let filter_level_5 = parse_filter("l=5").unwrap().1; let filter_level_5 = parse_filters("l=5").unwrap();
assert!(!filter_level_5(&lacooda)); assert!(!filter_level_5[0](&lacooda));
} }
} }