refactor parsing to be testable
This commit is contained in:
parent
940a6061ee
commit
fd5436954a
65
src/main.rs
65
src/main.rs
@ -1,8 +1,11 @@
|
|||||||
#![feature(option_result_contains)]
|
#![feature(option_result_contains)]
|
||||||
use nom::{
|
use nom::{
|
||||||
branch::alt,
|
branch::alt,
|
||||||
bytes::{complete::take_while_m_n, streaming::take_while},
|
bytes::{
|
||||||
combinator::{map_res, rest},
|
complete::{take_until1, take_while_m_n},
|
||||||
|
streaming::take_while,
|
||||||
|
},
|
||||||
|
combinator::{complete, map_res, rest},
|
||||||
sequence::tuple,
|
sequence::tuple,
|
||||||
IResult,
|
IResult,
|
||||||
};
|
};
|
||||||
@ -14,6 +17,7 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
type CardFilter = Box<dyn Fn(&SearchCard) -> bool>;
|
type CardFilter = Box<dyn Fn(&SearchCard) -> bool>;
|
||||||
|
type RawCardFilter = (Field, Operator, Value);
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
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;
|
||||||
@ -21,7 +25,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
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 mut query = Vec::new();
|
||||||
for q in std::env::args().skip(1) {
|
for q in std::env::args().skip(1) {
|
||||||
match query_arg(&q) {
|
match parse_filter(&q) {
|
||||||
Ok((_, filter)) => query.push(filter),
|
Ok((_, filter)) => query.push(filter),
|
||||||
Err(e) => Err(format!("Malformed query fragment {q}: {e:?}"))?,
|
Err(e) => Err(format!("Malformed query fragment {q}: {e:?}"))?,
|
||||||
}
|
}
|
||||||
@ -32,27 +36,43 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn query_arg(input: &str) -> IResult<&str, CardFilter> {
|
fn parse_filter(input: &str) -> IResult<&str, CardFilter> {
|
||||||
alt((map_res(tuple((field, operator, value)), |t| build_filter(t)), map_res(rest, |q: &str| fallback_filter(q))))(input)
|
map_res(parse_raw_filter, build_filter)(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fallback_filter(query: &str) -> Result<CardFilter, String> {
|
fn parse_raw_filter(input: &str) -> IResult<&str, RawCardFilter> {
|
||||||
|
alt((
|
||||||
|
complete(tuple((field, operator, value))),
|
||||||
|
map_res(take_until1(" "), |q| fallback_filter(q)),
|
||||||
|
map_res(rest, |q| fallback_filter(q)),
|
||||||
|
))(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
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}"));
|
||||||
}
|
}
|
||||||
println!("Trying to match {} as card name", query);
|
dbg!("Trying to match {query} as card name");
|
||||||
let q = query.to_lowercase();
|
let q = query.to_lowercase();
|
||||||
Ok(Box::new(move |card: &SearchCard| card.name.contains(&q)))
|
Ok((Field::Name, Operator::Equals, Value::String(q)))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_filter(query: (Field, Operator, Value)) -> Result<CardFilter, String> {
|
fn build_filter(query: RawCardFilter) -> Result<CardFilter, String> {
|
||||||
dbg!(&query);
|
dbg!(&query);
|
||||||
Ok(match query {
|
Ok(match query {
|
||||||
(Field::Atk, op, Value::Numerical(n)) => Box::new(move |card| op.filter_number(card.atk, n)),
|
(Field::Atk, op, Value::Numerical(n)) => Box::new(move |card| op.filter_number(card.atk, n)),
|
||||||
(Field::Def, op, Value::Numerical(n)) => Box::new(move |card| op.filter_number(card.def, n)),
|
(Field::Def, op, Value::Numerical(n)) => Box::new(move |card| op.filter_number(card.def, n)),
|
||||||
|
// ? ATK/DEF is modeled as None in the source json. At least for some monsters.
|
||||||
|
// Let’s at least find those.
|
||||||
|
(Field::Atk, _, Value::String(s)) if s == "?" => Box::new(move |card| card.atk.is_none() && card.card_type.contains("monster")),
|
||||||
|
(Field::Def, _, Value::String(s)) if s == "?" => {
|
||||||
|
Box::new(move |card| card.def.is_none() && card.link_rating.is_none() && card.card_type.contains("monster"))
|
||||||
|
}
|
||||||
(Field::Level, op, Value::Numerical(n)) => Box::new(move |card| op.filter_number(card.level, n)),
|
(Field::Level, op, Value::Numerical(n)) => Box::new(move |card| op.filter_number(card.level, n)),
|
||||||
(Field::Type, Operator::Equals, Value::String(s)) => Box::new(move |card| card.r#type == s),
|
(Field::Type, Operator::Equals, Value::String(s)) => Box::new(move |card| card.r#type == s),
|
||||||
(Field::Class, Operator::Equals, Value::String(s)) => Box::new(move |card| card.card_type.contains(&s)),
|
(Field::Class, Operator::Equals, Value::String(s)) => Box::new(move |card| card.card_type.contains(&s)),
|
||||||
|
(Field::Text, Operator::Equals, Value::String(s)) => Box::new(move |card| card.text.contains(&s)),
|
||||||
|
(Field::Name, Operator::Equals, Value::String(s)) => Box::new(move |card| card.name.contains(&s)),
|
||||||
q => Err(format!("unknown query: {q:?}"))?,
|
q => Err(format!("unknown query: {q:?}"))?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -68,7 +88,7 @@ fn operator(input: &str) -> IResult<&str, Operator> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn value(input: &str) -> IResult<&str, Value> {
|
fn value(input: &str) -> IResult<&str, Value> {
|
||||||
map_res(rest, |i: &str| match i.parse() {
|
map_res(alt((take_until1(" "), rest)), |i: &str| match i.parse() {
|
||||||
Ok(n) => Ok(Value::Numerical(n)),
|
Ok(n) => Ok(Value::Numerical(n)),
|
||||||
Err(_) if i.is_empty() => Err("empty filter argument"),
|
Err(_) if i.is_empty() => Err("empty filter argument"),
|
||||||
Err(_) => Ok(Value::String(i.to_lowercase())),
|
Err(_) => Ok(Value::String(i.to_lowercase())),
|
||||||
@ -83,6 +103,8 @@ enum Field {
|
|||||||
Type,
|
Type,
|
||||||
Attribute,
|
Attribute,
|
||||||
Class,
|
Class,
|
||||||
|
Name,
|
||||||
|
Text,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for Field {
|
impl FromStr for Field {
|
||||||
@ -95,6 +117,7 @@ impl FromStr for Field {
|
|||||||
"type" | "t" => Self::Type,
|
"type" | "t" => Self::Type,
|
||||||
"attribute" | "attr" | "a" => Self::Attribute,
|
"attribute" | "attr" | "a" => Self::Attribute,
|
||||||
"c" | "class" => Self::Class,
|
"c" | "class" => Self::Class,
|
||||||
|
"o" | "eff" | "text" | "effect" | "e" => Self::Text,
|
||||||
_ => Err(s.to_string())?,
|
_ => Err(s.to_string())?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -296,14 +319,28 @@ mod tests {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn query_parsing_test() {
|
||||||
|
assert_eq!(parse_raw_filter("t:PYro"), Ok(("", (Field::Type, Operator::Equals, Value::String("pyro".into())))));
|
||||||
|
assert_eq!(parse_raw_filter("t=pyro"), Ok(("", (Field::Type, Operator::Equals, Value::String("pyro".into())))));
|
||||||
|
assert_eq!(parse_raw_filter("t==pyro"), Ok(("", (Field::Type, Operator::Equals, Value::String("pyro".into())))));
|
||||||
|
assert_eq!(parse_raw_filter("atk>=100"), Ok(("", (Field::Atk, Operator::GreaterEqual, Value::Numerical(100)))));
|
||||||
|
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)))));
|
||||||
|
|
||||||
|
// These will fail during conversion
|
||||||
|
assert!(parse_filter("l===10").is_err());
|
||||||
|
assert!(parse_filter("t=").is_err());
|
||||||
|
assert!(parse_filter("=100").is_err());
|
||||||
|
assert!(parse_filter("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 = query_arg("l=3").unwrap().1;
|
let filter_level_3 = parse_filter("l=3").unwrap().1;
|
||||||
assert!(filter_level_3(&lacooda));
|
assert!(filter_level_3(&lacooda));
|
||||||
let filter_level_5 = query_arg("l=5").unwrap().1;
|
let filter_level_5 = parse_filter("l=5").unwrap().1;
|
||||||
assert!(!filter_level_5(&lacooda));
|
assert!(!filter_level_5(&lacooda));
|
||||||
let filter_level_incorrect = query_arg("l===5").unwrap().1;
|
|
||||||
assert!(!filter_level_incorrect(&lacooda));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user