query language
This commit is contained in:
parent
91a56a77dc
commit
8bf596eeba
39
Cargo.lock
generated
39
Cargo.lock
generated
@ -2,20 +2,34 @@
|
|||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 3
|
version = 3
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "aro"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc"
|
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]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.47"
|
version = "1.0.47"
|
||||||
@ -40,6 +54,15 @@ version = "1.0.11"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"
|
checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scryfall-ygo"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"nom",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.147"
|
version = "1.0.147"
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "aro"
|
name = "scryfall-ygo"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
nom = "7.1.3"
|
||||||
|
7
rustfmt.toml
Normal file
7
rustfmt.toml
Normal file
@ -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"
|
294
src/main.rs
294
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 serde::{de::Visitor, Deserialize, Deserializer};
|
||||||
|
use std::{
|
||||||
|
fmt::{self, Display},
|
||||||
|
str::FromStr,
|
||||||
|
};
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let cards: CardInfo =
|
let cards = serde_json::from_reader::<_, CardInfo>(std::io::BufReader::new(std::fs::File::open("cards.json")?))?.data;
|
||||||
serde_json::from_reader(std::io::BufReader::new(std::fs::File::open("cards.json")?))?;
|
let query = std::env::args()
|
||||||
println!("{} cards read", cards.data.len());
|
.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::<Vec<Box<dyn Fn(&Card) -> bool>>>();
|
||||||
|
|
||||||
|
cards.iter().filter(|card| query.iter().all(|q| q(card))).for_each(|c| println!("{c}"));
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn query_arg(input: &str) -> IResult<&str, (Field, Operator, Value)> {
|
||||||
|
tuple((field, operator, value))(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_filter(query: (Field, Operator, Value)) -> Box<dyn Fn(&Card) -> 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<Self, Self::Err> {
|
||||||
|
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<i32>, 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<Self, Self::Err> {
|
||||||
|
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)]
|
#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
|
||||||
struct CardInfo {
|
struct CardInfo {
|
||||||
data: Vec<Card>,
|
data: Vec<Card>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
|
|
||||||
struct CardBase {
|
|
||||||
//#[serde(rename = "type", deserialize_with = "split_types")]
|
|
||||||
//card_type: Vec<String>,
|
|
||||||
name: String,
|
|
||||||
#[serde(rename = "desc")]
|
|
||||||
text: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn split_types<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Vec<String>, D::Error> {
|
fn split_types<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Vec<String>, D::Error> {
|
||||||
struct SplittingVisitor;
|
struct SplittingVisitor;
|
||||||
|
|
||||||
@ -32,78 +157,57 @@ fn split_types<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Vec<String>
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
|
fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
|
||||||
Ok(v.split_whitespace()
|
Ok(v.split_whitespace().filter(|t| t != &"Card").map(str::to_owned).collect())
|
||||||
.filter(|t| t != &"Card")
|
|
||||||
.map(str::to_owned)
|
|
||||||
.collect())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
deserializer.deserialize_any(SplittingVisitor)
|
deserializer.deserialize_any(SplittingVisitor)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
|
#[derive(Debug, Deserialize, PartialEq, Eq, Clone, Default)]
|
||||||
struct Monster {
|
#[serde(tag = "type")]
|
||||||
// None for ?
|
struct Card {
|
||||||
atk: Option<i32>,
|
#[serde(rename = "type", deserialize_with = "split_types")]
|
||||||
attribute: String,
|
card_type: Vec<String>,
|
||||||
|
name: String,
|
||||||
|
#[serde(rename = "desc")]
|
||||||
|
text: String,
|
||||||
|
// Will also be None for ?
|
||||||
|
atk: Option<i32>,
|
||||||
|
def: Option<i32>,
|
||||||
|
attribute: Option<String>,
|
||||||
#[serde(rename = "race")]
|
#[serde(rename = "race")]
|
||||||
r#type: String,
|
r#type: String,
|
||||||
// None for ? or link monsters
|
|
||||||
def: Option<i32>,
|
|
||||||
// also includes rank
|
// also includes rank
|
||||||
level: Option<u8>,
|
level: Option<i32>,
|
||||||
|
#[serde(rename = "linkval")]
|
||||||
|
link_rating: Option<i32>,
|
||||||
|
linkmarkers: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
|
impl Display for Card {
|
||||||
#[serde(tag = "type")]
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
enum Card {
|
write!(f, "{} (", &self.name)?;
|
||||||
#[serde(alias = "Spell Card", alias = "Trap Card")]
|
if let Some(level) = self.level {
|
||||||
Backrow {
|
write!(f, "Level {level} ")?;
|
||||||
#[serde(flatten)]
|
} else if let Some(lr) = self.link_rating {
|
||||||
base: CardBase,
|
write!(f, "Link {lr} ")?;
|
||||||
},
|
}
|
||||||
#[serde(rename = "Skill Card")]
|
if let Some(attr) = &self.attribute {
|
||||||
Skill {
|
write!(f, "{attr}/")?;
|
||||||
#[serde(flatten)]
|
}
|
||||||
base: CardBase,
|
f.write_str(&self.r#type)?;
|
||||||
},
|
write!(f, " {})", self.card_type.join(" "))?;
|
||||||
#[serde(
|
if self.card_type.contains(&String::from("Monster")) {
|
||||||
alias = "Effect Monster",
|
match (self.atk, self.def) {
|
||||||
alias = "Flip Effect Monster",
|
(Some(atk), Some(def)) => write!(f, " {atk} ATK / {def} DEF")?,
|
||||||
alias = "Fusion Monster",
|
(Some(atk), None) if self.link_rating.is_some() => write!(f, "{atk} ATK")?,
|
||||||
alias = "Gemini Monster",
|
(None, Some(def)) => write!(f, " ? ATK / {def} DEF")?,
|
||||||
alias = "Link Monster",
|
(Some(atk), None) => write!(f, " {atk} ATK / ? DEF")?,
|
||||||
alias = "Normal Monster",
|
(None, None) => write!(f, " ? ATK / ? DEF")?,
|
||||||
alias = "Normal Tuner Monster",
|
}
|
||||||
alias = "Pendulum Effect Fusion Monster",
|
}
|
||||||
alias = "Pendulum Effect Monster",
|
Ok(())
|
||||||
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<String>,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@ -123,13 +227,12 @@ mod tests {
|
|||||||
let coffin: Card = serde_json::from_str(s).unwrap();
|
let coffin: Card = serde_json::from_str(s).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
coffin,
|
coffin,
|
||||||
Card::Backrow {
|
Card {
|
||||||
base: CardBase {
|
card_type: vec!["Spell".to_owned()],
|
||||||
card_type: vec!["Spell".to_owned()],
|
name: "The Cheerful Coffin".to_owned(),
|
||||||
name: "The Cheerful Coffin".to_owned(),
|
text: "Discard up to 3 Monster Cards from your hand to the Graveyard.".to_owned(),
|
||||||
text: "Discard up to 3 Monster Cards from your hand to the Graveyard."
|
r#type: "Normal".to_owned(),
|
||||||
.to_owned()
|
..Default::default()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -151,21 +254,18 @@ mod tests {
|
|||||||
let munch: Card = serde_json::from_str(s).unwrap();
|
let munch: Card = serde_json::from_str(s).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
munch,
|
munch,
|
||||||
Card::Monster {
|
Card {
|
||||||
base: CardBase {
|
card_type: vec!["Effect".to_owned(), "Monster".to_owned()],
|
||||||
card_type: vec!["Effect".to_owned(), "Monster".to_owned()],
|
name: "Des Lacooda".to_owned(),
|
||||||
name: "Des Lacooda".to_owned(),
|
text:
|
||||||
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(),
|
"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),
|
||||||
atk: Some(500),
|
def: Some(600),
|
||||||
def: Some(600),
|
level: Some(3),
|
||||||
level: Some(3),
|
r#type: "Zombie".to_owned(),
|
||||||
r#type: "Zombie".to_owned(),
|
attribute: Some("EARTH".to_owned()),
|
||||||
attribute: "EARTH".to_owned(),
|
..Default::default()
|
||||||
},
|
|
||||||
link_rating: 0,
|
|
||||||
linkmarkers: vec![]
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user