From 8bf596eeba05160d574bf0bb31c9bff0c7877b41 Mon Sep 17 00:00:00 2001 From: kageru Date: Thu, 26 Jan 2023 15:04:39 +0100 Subject: [PATCH] query language --- Cargo.lock | 39 +++++-- Cargo.toml | 3 +- rustfmt.toml | 7 ++ src/main.rs | 294 ++++++++++++++++++++++++++++++++++----------------- 4 files changed, 237 insertions(+), 106 deletions(-) create mode 100644 rustfmt.toml diff --git a/Cargo.lock b/Cargo.lock index 612cbe3..2d83a80 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,20 +2,34 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "aro" -version = "0.1.0" -dependencies = [ - "serde", - "serde_json", -] - [[package]] name = "itoa" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "proc-macro2" version = "1.0.47" @@ -40,6 +54,15 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" +[[package]] +name = "scryfall-ygo" +version = "0.1.0" +dependencies = [ + "nom", + "serde", + "serde_json", +] + [[package]] name = "serde" version = "1.0.147" diff --git a/Cargo.toml b/Cargo.toml index 0ae5c8c..92413e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,9 @@ [package] -name = "aro" +name = "scryfall-ygo" version = "0.1.0" edition = "2021" [dependencies] serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } +nom = "7.1.3" diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..36e1962 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,7 @@ +newline_style = "Unix" +max_width = 140 +imports_granularity = "Crate" +struct_field_align_threshold = 25 +where_single_line = true +edition = "2021" +use_small_heuristics = "Max" diff --git a/src/main.rs b/src/main.rs index daf66df..0f82594 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,26 +1,151 @@ +#![feature(option_result_contains)] +use nom::{ + bytes::{complete::take_while_m_n, streaming::take_while}, + combinator::{map_res, rest}, + sequence::tuple, + IResult, +}; use serde::{de::Visitor, Deserialize, Deserializer}; +use std::{ + fmt::{self, Display}, + str::FromStr, +}; fn main() -> Result<(), Box> { - let cards: CardInfo = - serde_json::from_reader(std::io::BufReader::new(std::fs::File::open("cards.json")?))?; - println!("{} cards read", cards.data.len()); + let cards = serde_json::from_reader::<_, CardInfo>(std::io::BufReader::new(std::fs::File::open("cards.json")?))?.data; + let query = std::env::args() + .skip(1) + .map(|q| { + query_arg(&q).map(|(_, r)| build_filter(r)).unwrap_or_else(|_| { + println!("Trying to match {} as card name", q); + let q = q.to_lowercase(); + Box::new(move |card: &Card| card.name.to_lowercase().contains(&q)) + }) + }) + .collect:: bool>>>(); + + cards.iter().filter(|card| query.iter().all(|q| q(card))).for_each(|c| println!("{c}")); + Ok(()) } +fn query_arg(input: &str) -> IResult<&str, (Field, Operator, Value)> { + tuple((field, operator, value))(input) +} + +fn build_filter(query: (Field, Operator, Value)) -> Box bool> { + // dbg!("Building filter for {query:?}"); + match query { + (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::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.to_lowercase() == s.to_lowercase()), + (Field::Attribute, Operator::Equals, Value::String(s)) => { + Box::new(move |card| card.attribute.as_ref().map(|s| s.to_lowercase()).contains(&s.to_lowercase())) + } + (Field::Class, Operator::Equals, Value::String(s)) => { + let s = s.to_lowercase(); + Box::new(move |card| card.card_type.iter().map(|t| t.to_lowercase()).any(|t| t == s)) + } + q => { + println!("unknown query: {q:?}"); + Box::new(|_| false) + } + } +} + +fn field(input: &str) -> IResult<&str, Field> { + map_res(take_while(char::is_alphabetic), str::parse)(input) +} + +const OPERATOR_CHARS: &[char] = &['=', '<', '>', ':']; + +fn operator(input: &str) -> IResult<&str, Operator> { + map_res(take_while_m_n(1, 2, |c| OPERATOR_CHARS.contains(&c)), str::parse)(input) +} + +fn value(input: &str) -> IResult<&str, Value> { + map_res(rest, |i: &str| match i.parse() { + Ok(n) => Result::<_, ()>::Ok(Value::Numerical(n)), + Err(_) => Ok(Value::String(i.to_owned())), + })(input) +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +enum Field { + Atk, + Def, + Level, + Type, + Attribute, + Class, +} + +impl FromStr for Field { + type Err = String; + fn from_str(s: &str) -> Result { + Ok(match s.to_lowercase().as_ref() { + "atk" => Self::Atk, + "def" => Self::Def, + "level" | "l" => Self::Level, + "type" | "t" => Self::Type, + "attribute" | "attr" | "a" => Self::Attribute, + "c" | "class" => Self::Class, + _ => Err(s.to_string())?, + }) + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +enum Operator { + Equals, + Less, + LessEqual, + Greater, + GreaterEqual, +} + +impl Operator { + pub fn filter_number(&self, a: Option, b: i32) -> bool { + if let Some(a) = a { + match self { + Self::Equals => a == b, + Self::Less => a < b, + Self::LessEqual => a <= b, + Self::Greater => a > b, + Self::GreaterEqual => a >= b, + } + } else { + false + } + } +} + +impl FromStr for Operator { + type Err = String; + fn from_str(s: &str) -> Result { + Ok(match s { + "=" | "==" | ":" => Self::Equals, + ">=" | "=>" => Self::GreaterEqual, + "<=" | "=<" => Self::LessEqual, + ">" => Self::Greater, + "<" => Self::Less, + _ => Err(s.to_owned())?, + }) + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +enum Value { + String(String), + Numerical(i32), +} + #[derive(Debug, Deserialize, PartialEq, Eq, Clone)] struct CardInfo { data: Vec, } -#[derive(Debug, Deserialize, PartialEq, Eq, Clone)] -struct CardBase { - //#[serde(rename = "type", deserialize_with = "split_types")] - //card_type: Vec, - name: String, - #[serde(rename = "desc")] - text: String, -} - fn split_types<'de, D: Deserializer<'de>>(deserializer: D) -> Result, D::Error> { struct SplittingVisitor; @@ -32,78 +157,57 @@ fn split_types<'de, D: Deserializer<'de>>(deserializer: D) -> Result } fn visit_str(self, v: &str) -> Result { - Ok(v.split_whitespace() - .filter(|t| t != &"Card") - .map(str::to_owned) - .collect()) + Ok(v.split_whitespace().filter(|t| t != &"Card").map(str::to_owned).collect()) } } deserializer.deserialize_any(SplittingVisitor) } -#[derive(Debug, Deserialize, PartialEq, Eq, Clone)] -struct Monster { - // None for ? - atk: Option, - attribute: String, +#[derive(Debug, Deserialize, PartialEq, Eq, Clone, Default)] +#[serde(tag = "type")] +struct Card { + #[serde(rename = "type", deserialize_with = "split_types")] + card_type: Vec, + name: String, + #[serde(rename = "desc")] + text: String, + // Will also be None for ? + atk: Option, + def: Option, + attribute: Option, #[serde(rename = "race")] - r#type: String, - // None for ? or link monsters - def: Option, + r#type: String, // also includes rank - level: Option, + level: Option, + #[serde(rename = "linkval")] + link_rating: Option, + linkmarkers: Option>, } -#[derive(Debug, Deserialize, PartialEq, Eq, Clone)] -#[serde(tag = "type")] -enum Card { - #[serde(alias = "Spell Card", alias = "Trap Card")] - Backrow { - #[serde(flatten)] - base: CardBase, - }, - #[serde(rename = "Skill Card")] - Skill { - #[serde(flatten)] - base: CardBase, - }, - #[serde( - alias = "Effect Monster", - alias = "Flip Effect Monster", - alias = "Fusion Monster", - alias = "Gemini Monster", - alias = "Link Monster", - alias = "Normal Monster", - alias = "Normal Tuner Monster", - alias = "Pendulum Effect Fusion Monster", - alias = "Pendulum Effect Monster", - alias = "Pendulum Effect Ritual Monster", - alias = "Pendulum Flip Effect Monster", - alias = "Pendulum Normal Monster", - alias = "Pendulum Tuner Effect Monster", - alias = "Ritual Effect Monster", - alias = "Ritual Monster", - alias = "Spirit Monster", - alias = "Synchro Monster", - alias = "Synchro Pendulum Effect Monster", - alias = "Synchro Tuner Monster", - alias = "Token", - alias = "Toon Monster", - alias = "Tuner Monster", - alias = "Union Effect Monster", - alias = "XYZ Monster", - alias = "XYZ Pendulum Effect Monster" - )] - Monster { - #[serde(flatten)] - base: CardBase, - #[serde(flatten)] - monster: Monster, - #[serde(default, rename = "linkval")] - link_rating: u8, - #[serde(default)] - linkmarkers: Vec, - }, +impl Display for Card { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} (", &self.name)?; + if let Some(level) = self.level { + write!(f, "Level {level} ")?; + } else if let Some(lr) = self.link_rating { + write!(f, "Link {lr} ")?; + } + if let Some(attr) = &self.attribute { + write!(f, "{attr}/")?; + } + f.write_str(&self.r#type)?; + write!(f, " {})", self.card_type.join(" "))?; + if self.card_type.contains(&String::from("Monster")) { + match (self.atk, self.def) { + (Some(atk), Some(def)) => write!(f, " {atk} ATK / {def} DEF")?, + (Some(atk), None) if self.link_rating.is_some() => write!(f, "{atk} ATK")?, + (None, Some(def)) => write!(f, " ? ATK / {def} DEF")?, + (Some(atk), None) => write!(f, " {atk} ATK / ? DEF")?, + (None, None) => write!(f, " ? ATK / ? DEF")?, + } + } + Ok(()) + } } #[cfg(test)] @@ -123,13 +227,12 @@ mod tests { let coffin: Card = serde_json::from_str(s).unwrap(); assert_eq!( coffin, - Card::Backrow { - base: CardBase { - card_type: vec!["Spell".to_owned()], - name: "The Cheerful Coffin".to_owned(), - text: "Discard up to 3 Monster Cards from your hand to the Graveyard." - .to_owned() - } + Card { + card_type: vec!["Spell".to_owned()], + name: "The Cheerful Coffin".to_owned(), + text: "Discard up to 3 Monster Cards from your hand to the Graveyard.".to_owned(), + r#type: "Normal".to_owned(), + ..Default::default() } ) } @@ -151,21 +254,18 @@ mod tests { let munch: Card = serde_json::from_str(s).unwrap(); assert_eq!( munch, - Card::Monster { - base: CardBase { - card_type: vec!["Effect".to_owned(), "Monster".to_owned()], - name: "Des Lacooda".to_owned(), - text: "Once per turn: You can change this card to face-down Defense Position. When this card is Flip Summoned: Draw 1 card.".to_owned(), - }, - monster: Monster { - atk: Some(500), - def: Some(600), - level: Some(3), - r#type: "Zombie".to_owned(), - attribute: "EARTH".to_owned(), - }, - link_rating: 0, - linkmarkers: vec![] + Card { + card_type: vec!["Effect".to_owned(), "Monster".to_owned()], + name: "Des Lacooda".to_owned(), + text: + "Once per turn: You can change this card to face-down Defense Position. When this card is Flip Summoned: Draw 1 card." + .to_owned(), + atk: Some(500), + def: Some(600), + level: Some(3), + r#type: "Zombie".to_owned(), + attribute: Some("EARTH".to_owned()), + ..Default::default() }, ) }