diff --git a/src/data.rs b/src/data.rs index 9a005f7..05c9e53 100644 --- a/src/data.rs +++ b/src/data.rs @@ -1,5 +1,5 @@ use serde::Deserialize; -use std::fmt::{self, Display}; +use std::fmt::{self, Display, Write}; #[derive(Debug, Deserialize, PartialEq, Eq, Clone)] pub struct CardInfo { @@ -26,6 +26,26 @@ pub struct Card { pub link_rating: Option, #[serde(rename = "linkmarkers")] pub link_arrows: Option>, + #[serde(default)] + pub card_sets: Vec, +} + +#[derive(Debug, Deserialize, PartialEq, Eq, Clone, Default)] +pub struct CardSet { + pub set_name: String, + pub set_code: String, + pub set_rarity: String, +} + +impl Card { + pub fn extended_info(&self) -> Result { + let mut s = String::with_capacity(1000); + s.push_str("

Printings:

"); + for printing in &self.card_sets { + write!(s, "{}: {} ({})
", printing.set_name, printing.set_code, printing.set_rarity)?; + } + Ok(s) + } } impl Display for Card { @@ -70,7 +90,23 @@ pub mod tests { "name": "The Cheerful Coffin", "type": "Spell Card", "desc": "Discard up to 3 Monster Cards from your hand to the Graveyard.", - "race": "Normal" + "race": "Normal", + "card_sets": [ + { + "set_name": "Dark Beginning 1", + "set_code": "DB1-EN167", + "set_rarity": "Common", + "set_rarity_code": "(C)", + "set_price": "1.41" + }, + { + "set_name": "Metal Raiders", + "set_code": "MRD-059", + "set_rarity": "Common", + "set_rarity_code": "(C)", + "set_price": "1.55" + } + ] }"#; pub const RAW_MONSTER: &str = r#" @@ -83,7 +119,23 @@ pub mod tests { "def": 600, "level": 3, "race": "Zombie", - "attribute": "EARTH" + "attribute": "EARTH", + "card_sets": [ + { + "set_name": "Astral Pack Three", + "set_code": "AP03-EN018", + "set_rarity": "Common", + "set_rarity_code": "(C)", + "set_price": "1.24" + }, + { + "set_name": "Gold Series", + "set_code": "GLD1-EN010", + "set_rarity": "Common", + "set_rarity_code": "(C)", + "set_price": "2.07" + } + ] }"#; #[test] @@ -97,6 +149,14 @@ pub mod tests { 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(), + card_sets: vec![ + CardSet { + set_name: "Dark Beginning 1".to_owned(), + set_code: "DB1-EN167".to_owned(), + set_rarity: "Common".to_owned(), + }, + CardSet { set_name: "Metal Raiders".to_owned(), set_code: "MRD-059".to_owned(), set_rarity: "Common".to_owned() } + ], ..Default::default() } ) @@ -119,6 +179,14 @@ pub mod tests { level: Some(3), r#type: "Zombie".to_owned(), attribute: Some("EARTH".to_owned()), + card_sets: vec![ + CardSet { + set_name: "Astral Pack Three".to_owned(), + set_code: "AP03-EN018".to_owned(), + set_rarity: "Common".to_owned(), + }, + CardSet { set_name: "Gold Series".to_owned(), set_code: "GLD1-EN010".to_owned(), set_rarity: "Common".to_owned() } + ], ..Default::default() }, ) diff --git a/src/filter.rs b/src/filter.rs index 28ea3e7..ec4f159 100644 --- a/src/filter.rs +++ b/src/filter.rs @@ -18,6 +18,7 @@ pub struct SearchCard { level: Option, link_rating: Option, link_arrows: Option>, + sets: Vec, } impl From<&Card> for SearchCard { @@ -34,6 +35,7 @@ impl From<&Card> for SearchCard { level: card.level, link_rating: card.link_rating, link_arrows: card.link_arrows.as_ref().map(|arrows| arrows.iter().map(|a| a.to_lowercase()).collect()), + sets: card.card_sets.iter().filter_map(|s| s.set_code.split('-').next().map(str::to_lowercase)).collect(), } } } @@ -72,6 +74,7 @@ pub fn build_filter(query: RawCardFilter) -> Result { RawCardFilter(Field::Text, Operator::NotEqual, Value::String(s)) => Box::new(move |card| !card.text.contains(&s)), RawCardFilter(Field::Name, Operator::Equal, Value::String(s)) => Box::new(move |card| card.name.contains(&s)), RawCardFilter(Field::Name, Operator::NotEqual, Value::String(s)) => Box::new(move |card| !card.name.contains(&s)), + RawCardFilter(Field::Set, Operator::Equal, Value::String(s)) => Box::new(move |card| card.sets.contains(&s)), q => Err(format!("unknown query: {q:?}"))?, }) } @@ -85,8 +88,8 @@ mod tests { fn level_filter_test() { let lacooda = SearchCard::from(&serde_json::from_str::(RAW_MONSTER).unwrap()); let filter_level_3 = parse_filters("l=3").unwrap(); - assert!(filter_level_3[0].1(&lacooda)); + assert!(filter_level_3.1[0](&lacooda)); let filter_level_5 = parse_filters("l=5").unwrap(); - assert!(!filter_level_5[0].1(&lacooda)); + assert!(!filter_level_5.1[0](&lacooda)); } } diff --git a/src/main.rs b/src/main.rs index 214fd2e..aeb9b41 100644 --- a/src/main.rs +++ b/src/main.rs @@ -83,9 +83,11 @@ async fn card_info(card_id: web::Path) -> Result {card} + {} "#, IMG_HOST.as_str(), card.id, + card.extended_info().unwrap_or_else(|_| String::new()), )?; } None => res.push_str("Card not found"), diff --git a/src/parser.rs b/src/parser.rs index f6c9a06..4b5376c 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -66,11 +66,12 @@ pub enum Field { Def = 2, Level = 3, LinkRating = 4, - Type = 5, - Attribute = 6, - Class = 7, - Name = 8, - Text = 9, + Set = 5, + Type = 6, + Attribute = 7, + Class = 8, + Name = 9, + Text = 10, } impl Display for Field { @@ -85,6 +86,7 @@ impl Display for Field { Self::Atk => "ATK", Self::Def => "DEF", Self::LinkRating => "link rating", + Self::Set => "set", }) } } @@ -102,6 +104,7 @@ impl FromStr for Field { "o" | "eff" | "text" | "effect" | "e" => Self::Text, "lr" | "linkrating" => Self::LinkRating, "name" => Self::Name, + "set" | "s" => Self::Set, _ => Err(s.to_string())?, }) } diff --git a/static/help.html b/static/help.html index 915c700..174269d 100644 --- a/static/help.html +++ b/static/help.html @@ -12,6 +12,7 @@ Currently supported search fields are:
  • The type (or t) of a card (this is “Warrior”, “Pyro”, “Insect”, etc. for monsters, but also “quick-play”, “counter”, or “normal” for Spells/Traps).
  • The attribute (or attr or a) of a card. This is “Light”, “Dark”, “Earth”, etc.
  • The text (or effect, eff, e, or o) of a card. This is either the effect or flavor text (for normal monsters). For pendulum cards, this searches in both pendulum and monster effects. The o alias is to help my muscle memory coming from Scryfall.
  • +
  • The set (or s) a card was printed in. This considers all printings, not just the original.
  • Anything not associated with a search field is interpreted as a search in the card name, so l:4 utopia will show all level/rank 4 monsters with “Utopia” in their name.
    If your search contains spaces (e.g. searching for an effect that says “destroy that target”), the text must be quoted like effect:"destroy that target". @@ -32,4 +33,5 @@ The following search operators are supported:
  • All “Blue-eyes” fusion monsters except the ones that are level 12: c:fusion l!=12 blue-eyes
  • All Synchro monsters that are Dark attribute, level 5 or higher, and have exactly 2200 ATK: c:synchro a:dark l>=5 atk:2200
  • All counter traps that can negate summons: c:trap t:counter e:"negate the summon"
  • +
  • All effect monsters printed in Legend of Blue-Eyes: set:lob c:effect