diff --git a/Cargo.lock b/Cargo.lock index c6b5174..60ec1c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -259,6 +259,7 @@ dependencies = [ name = "dreadbot" version = "0.1.0" dependencies = [ + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "reqwest 0.9.20 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.100 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/Cargo.toml b/Cargo.toml index b979c93..d7d5254 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,3 +13,4 @@ serde_json = "1.0" serenity = "0.7.0" regex = "1" sha2 = "0.8.0" +lazy_static = "1.4.0" diff --git a/src/card.rs b/src/card.rs index 066feca..81fa512 100644 --- a/src/card.rs +++ b/src/card.rs @@ -1,65 +1,70 @@ pub type Cents = u32; pub fn format_cents(amount: Cents) -> String { - let dollars = amount / 100; - let remainder = amount % 100; + let dollars = amount / 100; + let remainder = amount % 100; - format!("{}.{:02}", dollars, remainder) + format!("{}.{:02}", dollars, remainder) } #[derive(Debug)] pub struct Card { - pub quantity: u32, - pub name: String, - pub price: Option + pub quantity: u32, + pub name: String, + pub price: Option, } impl Card { - pub fn from_goldfish_line(line: &str) -> Option { - if line.is_empty() { return None } + pub fn from_goldfish_line(line: &str) -> Option { + if line.is_empty() { + return None; + } - let mut splitter = line.splitn(2, " "); - let quantity_string = splitter.next().unwrap(); - let name_string = splitter.next().unwrap(); - let quantity_parsed = quantity_string.parse::(); + let mut splitter = line.splitn(2, " "); + let quantity_string = splitter.next().unwrap(); + let name_string = splitter.next().unwrap(); + let quantity_parsed = quantity_string.parse::(); - match quantity_parsed { - Ok(quantity) => Some(Card { - quantity: quantity, - name: String::from(name_string).replace("/", " // "), - price: None - }), - Err(_) => None + match quantity_parsed { + Ok(quantity) => Some(Card { + quantity: quantity, + name: String::from(name_string).replace("/", " // "), + price: None, + }), + Err(_) => None, + } } - } - pub fn info_string(&self) -> String { - match &self.price { - Some(amount) => format!( - "{} {} ({} each, {} total)", - self.quantity, self.name, format_cents(*amount), format_cents(*amount * self.quantity) - ), - None => format!("{} {} (unpriced)", self.quantity, self.name) + pub fn info_string(&self) -> String { + match &self.price { + Some(amount) => format!( + "{} {} ({} each, {} total)", + self.quantity, + self.name, + format_cents(*amount), + format_cents(*amount * self.quantity) + ), + None => format!("{} {} (unpriced)", self.quantity, self.name), + } } - } } #[test] fn test_card_creation() { - let card = Card::from_goldfish_line("4 Winding Constrictor").unwrap(); - assert_eq!(card.name, "Winding Constrictor"); - assert_eq!(card.quantity, 4); + let card = Card::from_goldfish_line("4 Winding Constrictor").unwrap(); + assert_eq!(card.name, "Winding Constrictor"); + assert_eq!(card.quantity, 4); } #[test] fn test_empty_card() { - let card = Card::from_goldfish_line(""); - assert_eq!(card.is_none(), true); + let card = Card::from_goldfish_line(""); + assert_eq!(card.is_none(), true); } #[test] fn test_parses_split_card() { - let card = Card::from_goldfish_line("4 Fire/Ice").unwrap(); - assert_eq!(card.name, "Fire // Ice"); - assert_eq!(card.quantity, 4); + let card = Card::from_goldfish_line("4 Fire/Ice").unwrap(); + assert_eq!(card.name, "Fire // Ice"); + assert_eq!(card.quantity, 4); } diff --git a/src/deck.rs b/src/deck.rs index 0490f82..66869ff 100644 --- a/src/deck.rs +++ b/src/deck.rs @@ -1,239 +1,262 @@ use super::card::{Card, Cents}; -use super::scryfall::{PricingSource}; -use sha2::{Sha256, Digest}; +use super::scryfall::PricingSource; +use sha2::{Digest, Sha256}; #[derive(Debug)] pub struct Deck { - goldfish_id: String, - mainboard: Vec, - sideboard: Vec + goldfish_id: String, + mainboard: Vec, + sideboard: Vec, } impl Deck { - pub fn from_goldfish_block(goldfish_id: String, block: String) -> Self { - let mut mainboard: Vec = Vec::new(); - let mut sideboard: Vec = Vec::new(); - let mut sideboard_flag = false; - - for line in block.split("\r\n") { - match Card::from_goldfish_line(line) { - Some(card) => { - if sideboard_flag { sideboard.push(card) } else { mainboard.push(card) } - }, - None => sideboard_flag = true - }; + pub fn from_goldfish_block(goldfish_id: String, block: String) -> Self { + let mut mainboard: Vec = Vec::new(); + let mut sideboard: Vec = Vec::new(); + let mut sideboard_flag = false; + + for line in block.split("\r\n") { + match Card::from_goldfish_line(line) { + Some(card) => { + if sideboard_flag { + sideboard.push(card) + } else { + mainboard.push(card) + } + } + None => sideboard_flag = true, + }; + } + + mainboard.sort_by(|a, b| a.name.cmp(&b.name)); + sideboard.sort_by(|a, b| a.name.cmp(&b.name)); + + Deck { + goldfish_id: goldfish_id, + mainboard: mainboard, + sideboard: sideboard, + } } - mainboard.sort_by(|a, b| a.name.cmp(&b.name)); - sideboard.sort_by(|a, b| a.name.cmp(&b.name)); - - Deck { - goldfish_id: goldfish_id, - mainboard: mainboard, - sideboard: sideboard + fn update_card_pricing(card: &mut Card, entry: &PricingSource) { + if card.name == entry.front_name || card.name == entry.name { + card.price = Some(entry.price); + } } - } - fn update_card_pricing(card: &mut Card, entry: &PricingSource) { - if card.name == entry.front_name || card.name == entry.name { - card.price = Some(entry.price); - } - } + pub fn update_pricing(&mut self, scryfall_entries: Vec) { + for entry in scryfall_entries { + for card in &mut self.mainboard { + Self::update_card_pricing(card, &entry); + } - pub fn update_pricing(&mut self, scryfall_entries: Vec) { - for entry in scryfall_entries { - for card in &mut self.mainboard { - Self::update_card_pricing(card, &entry); - } + for card in &mut self.sideboard { + Self::update_card_pricing(card, &entry); + } + } + } - for card in &mut self.sideboard { - Self::update_card_pricing(card, &entry); - } + pub fn cards<'a>(&'a self) -> DeckIter<'a> { + DeckIter { + deck: self, + index: 0, + } } - } - pub fn cards<'a>(&'a self) -> DeckIter<'a> { - DeckIter { deck: self, index: 0 } - } + fn sum_prices(cards: &Vec) -> Cents { + let mut total_cents: Cents = 0; - fn sum_prices(cards: &Vec) -> Cents { - let mut total_cents: Cents = 0; + for card in cards { + total_cents = match &card.price { + Some(amount) => total_cents + card.quantity * *amount, + None => total_cents, + }; + } - for card in cards { - total_cents = match &card.price { - Some(amount) => total_cents + card.quantity * *amount, - None => total_cents - }; + total_cents } - total_cents - } + pub fn mainboard_pricing(&self) -> Cents { + Deck::sum_prices(&self.mainboard) + } - pub fn mainboard_pricing(&self) -> Cents { - Deck::sum_prices(&self.mainboard) - } + pub fn sideboard_pricing(&self) -> Cents { + Deck::sum_prices(&self.sideboard) + } - pub fn sideboard_pricing(&self) -> Cents { - Deck::sum_prices(&self.sideboard) - } + pub fn info_string(&self) -> String { + let mut info = String::new(); - pub fn info_string(&self) -> String { - let mut info = String::new(); + info += "```\n"; - info += "```\n"; + info += "Mainboard:\n"; + for card in &self.mainboard { + info += &card.info_string(); + info += "\n"; + } - info += "Mainboard:\n"; - for card in &self.mainboard { - info += &card.info_string(); - info += "\n"; - } + info += "\nSideboard:\n"; + for card in &self.sideboard { + info += &card.info_string(); + info += "\n"; + } - info += "\nSideboard:\n"; - for card in &self.sideboard { - info += &card.info_string(); - info += "\n"; + info += "```"; + info } - info += "```"; - info - } + pub fn to_hash(&self) -> String { + let mut hasher = Sha256::new(); - pub fn to_hash(&self) -> String { - let mut hasher = Sha256::new(); + for card in &self.mainboard { + hasher.input(format!("#{} {}", card.quantity, card.name)); + } - for card in &self.mainboard { - hasher.input(format!("#{} {}", card.quantity, card.name)); - } + hasher.input("||"); - hasher.input("||"); + for card in &self.mainboard { + hasher.input(format!("#{} {}", card.quantity, card.name)); + } - for card in &self.mainboard { - hasher.input(format!("#{} {}", card.quantity, card.name)); - } + let mut hash_string = String::new(); + for byte in hasher.result()[..6].iter() { + hash_string += &format!("{:02X}", byte); + } - let mut hash_string = String::new(); - for byte in hasher.result()[..6].iter() { - hash_string += &format!("{:02X}", byte); + hash_string } - - hash_string - } } pub struct DeckIter<'a> { - deck: &'a Deck, - index: usize + deck: &'a Deck, + index: usize, } -impl <'a> Iterator for DeckIter<'a> { - type Item = &'a Card; - - fn next(&mut self) -> Option { - let mainboard_size = self.deck.mainboard.len(); - if self.index < mainboard_size { - let card = self.deck.mainboard.get(self.index); - self.index += 1; - return card; - } - - let shifted_index = self.index - mainboard_size; - if shifted_index < self.deck.sideboard.len() { - let card = self.deck.sideboard.get(shifted_index); - self.index += 1; - return card; +impl<'a> Iterator for DeckIter<'a> { + type Item = &'a Card; + + fn next(&mut self) -> Option { + let mainboard_size = self.deck.mainboard.len(); + if self.index < mainboard_size { + let card = self.deck.mainboard.get(self.index); + self.index += 1; + return card; + } + + let shifted_index = self.index - mainboard_size; + if shifted_index < self.deck.sideboard.len() { + let card = self.deck.sideboard.get(shifted_index); + self.index += 1; + return card; + } + + None } - - None - } } #[test] fn test_deck_creation() { - let deck_text = "4 Treasure Hunt\r\n4 Zombie Infestation\r\n26 Island\r\n26 Swamp\r\n\r\n15 Good Sideboard Card"; - let id = "test id"; - let deck = Deck::from_goldfish_block(String::from(id), String::from(deck_text)); - - assert_eq!(deck.goldfish_id, String::from(id)); - assert_eq!(deck.mainboard.len(), 4); - assert_eq!(deck.mainboard.get(0).unwrap().quantity, 26); - assert_eq!(deck.mainboard.get(0).unwrap().name, "Island"); - - assert_eq!(deck.sideboard.len(), 1); - assert_eq!(deck.sideboard.get(0).unwrap().quantity, 15); - assert_eq!(deck.sideboard.get(0).unwrap().name, "Good Sideboard Card"); + let deck_text = "4 Treasure Hunt\r\n4 Zombie Infestation\r\n26 Island\r\n26 Swamp\r\n\r\n15 Good Sideboard Card"; + let id = "test id"; + let deck = Deck::from_goldfish_block(String::from(id), String::from(deck_text)); + + assert_eq!(deck.goldfish_id, String::from(id)); + assert_eq!(deck.mainboard.len(), 4); + assert_eq!(deck.mainboard.get(0).unwrap().quantity, 26); + assert_eq!(deck.mainboard.get(0).unwrap().name, "Island"); + + assert_eq!(deck.sideboard.len(), 1); + assert_eq!(deck.sideboard.get(0).unwrap().quantity, 15); + assert_eq!(deck.sideboard.get(0).unwrap().name, "Good Sideboard Card"); } #[test] fn test_iterator() { - let deck_text = "10 Island\r\n4 Treasure Hunt\r\n4 Zombie Infestation\r\n\r\n26 Island"; - let id = "test id"; - let deck = Deck::from_goldfish_block(String::from(id), String::from(deck_text)); - let mut deck_iter = deck.cards(); - - assert_eq!(deck_iter.next().unwrap().name, "Island"); - assert_eq!(deck_iter.next().unwrap().name, "Treasure Hunt"); - assert_eq!(deck_iter.next().unwrap().name, "Zombie Infestation"); - assert_eq!(deck_iter.next().unwrap().name, "Island"); - assert_eq!(deck_iter.next().is_none(), true); + let deck_text = "10 Island\r\n4 Treasure Hunt\r\n4 Zombie Infestation\r\n\r\n26 Island"; + let id = "test id"; + let deck = Deck::from_goldfish_block(String::from(id), String::from(deck_text)); + let mut deck_iter = deck.cards(); + + assert_eq!(deck_iter.next().unwrap().name, "Island"); + assert_eq!(deck_iter.next().unwrap().name, "Treasure Hunt"); + assert_eq!(deck_iter.next().unwrap().name, "Zombie Infestation"); + assert_eq!(deck_iter.next().unwrap().name, "Island"); + assert_eq!(deck_iter.next().is_none(), true); } #[test] fn test_pricing_update() { - let deck_text = "10 Island\r\n4 Treasure Hunt"; - let id = "test id"; - let mut deck = Deck::from_goldfish_block(String::from(id), String::from(deck_text)); + let deck_text = "10 Island\r\n4 Treasure Hunt"; + let id = "test id"; + let mut deck = Deck::from_goldfish_block(String::from(id), String::from(deck_text)); - let mut scryfall_entries: Vec = Vec::new(); - scryfall_entries.push(PricingSource { - name: String::from("Island"), - front_name: String::from("Island"), - price: 100 - }); + let mut scryfall_entries: Vec = Vec::new(); + scryfall_entries.push(PricingSource { + name: String::from("Island"), + front_name: String::from("Island"), + price: 100, + }); - deck.update_pricing(scryfall_entries); + deck.update_pricing(scryfall_entries); - let island = deck.mainboard.get(0).unwrap(); - assert_eq!(island.price, Some(100)); + let island = deck.mainboard.get(0).unwrap(); + assert_eq!(island.price, Some(100)); - let treasure_hunt = deck.mainboard.get(1).unwrap(); - assert_eq!(treasure_hunt.price, None); + let treasure_hunt = deck.mainboard.get(1).unwrap(); + assert_eq!(treasure_hunt.price, None); } #[test] fn test_mainboard_pricing() { - let mut cards: Vec = Vec::new(); - cards.push(Card { quantity: 10, name: String::from("Island"), price: Some(100) }); - cards.push(Card { quantity: 1, name: String::from("Island"), price: None }); - - let deck = Deck { - mainboard: cards, - sideboard: Vec::new(), - goldfish_id: String::from("test") - }; - - assert_eq!(deck.mainboard_pricing(), 1000); + let mut cards: Vec = Vec::new(); + cards.push(Card { + quantity: 10, + name: String::from("Island"), + price: Some(100), + }); + cards.push(Card { + quantity: 1, + name: String::from("Island"), + price: None, + }); + + let deck = Deck { + mainboard: cards, + sideboard: Vec::new(), + goldfish_id: String::from("test"), + }; + + assert_eq!(deck.mainboard_pricing(), 1000); } #[test] fn test_sideboard_pricing() { - let mut cards: Vec = Vec::new(); - cards.push(Card { quantity: 10, name: String::from("Island"), price: Some(100) }); - cards.push(Card { quantity: 1, name: String::from("Island"), price: None }); - - let deck = Deck { - mainboard: Vec::new(), - sideboard: cards, - goldfish_id: String::from("test") - }; - - assert_eq!(deck.sideboard_pricing(), 1000); + let mut cards: Vec = Vec::new(); + cards.push(Card { + quantity: 10, + name: String::from("Island"), + price: Some(100), + }); + cards.push(Card { + quantity: 1, + name: String::from("Island"), + price: None, + }); + + let deck = Deck { + mainboard: Vec::new(), + sideboard: cards, + goldfish_id: String::from("test"), + }; + + assert_eq!(deck.sideboard_pricing(), 1000); } #[test] fn test_to_hash() { - let deck_text = "10 Island\r\n4 Treasure Hunt\r\n4 Zombie Infestation\r\n\r\n26 Island"; - let id = "test id"; - let deck = Deck::from_goldfish_block(String::from(id), String::from(deck_text)); + let deck_text = "10 Island\r\n4 Treasure Hunt\r\n4 Zombie Infestation\r\n\r\n26 Island"; + let id = "test id"; + let deck = Deck::from_goldfish_block(String::from(id), String::from(deck_text)); - assert_eq!(deck.to_hash(), "D0DFF733D658"); + assert_eq!(deck.to_hash(), "D0DFF733D658"); } diff --git a/src/main.rs b/src/main.rs index 9596d78..e401c0b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,25 +1,25 @@ mod goldfish; extern crate regex; -mod deck; mod card; +mod deck; mod scryfall; -use card::{Cents, format_cents}; -use goldfish::{retrieve_deck}; +use card::{format_cents, Cents}; use deck::Deck; +use goldfish::retrieve_deck; +use lazy_static::lazy_static; use regex::Regex; -use std::env; use serenity::{ model::{channel::Message, gateway::Ready}, prelude::*, }; +use std::env; const MAINDECK_LIMIT: Cents = 20_00; const SIDEBOARD_LIMIT: Cents = 5_00; const DREADBOT_PREFIX: &str = r"^\$\$(.*)$"; -const HELP_TEXT: &str = -r" +const HELP_TEXT: &str = r" ``` Dreadbot is the official pricing method of paper dreadful. @@ -31,20 +31,29 @@ $$info - Receive an itemized list of prices for a deck. The response is lengthy so try to keep this to PMs. ``` "; +lazy_static! { + static ref PREFIX_REGEX: Regex = Regex::new(DREADBOT_PREFIX).unwrap(); + static ref INFO_REGEX: Regex = + Regex::new(r"^info https://www\.mtggoldfish\.com/deck/(\d*).*$").unwrap(); + static ref HASH_REGEX: Regex = + Regex::new(r"^hash https://www\.mtggoldfish\.com/deck/(\d*).*$").unwrap(); + static ref VERIFY_REGEX: Regex = + Regex::new(r"^verify https://www\.mtggoldfish\.com/deck/(\d*).*$").unwrap(); +} struct Handler; fn fetch_deck(id: &str) -> Option { let response = match retrieve_deck(id) { Ok(resp) => resp, - _ => return None + _ => return None, }; let mut deck = Deck::from_goldfish_block(String::from(id), response); let scryfall_resp = match scryfall::request_pricing(&deck) { Ok(resp) => resp, - _ => return None + _ => return None, }; deck.update_pricing(scryfall_resp); @@ -63,7 +72,7 @@ fn respond_to_deck(ctx: &Context, msg: &Message, deck: &Deck) -> bool { let maindeck_price = deck.mainboard_pricing(); let sideboard_price = deck.sideboard_pricing(); let formatted_maindeck = format_cents(maindeck_price); - let formatted_sideboard= format_cents(sideboard_price); + let formatted_sideboard = format_cents(sideboard_price); let maindeck_over = deck.mainboard_pricing() <= MAINDECK_LIMIT; let sideboard_over = deck.sideboard_pricing() <= SIDEBOARD_LIMIT; @@ -93,15 +102,20 @@ fn respond_to_deck(ctx: &Context, msg: &Message, deck: &Deck) -> bool { respond(ctx, &msg, &response) } -fn retrieve_or_error(ctx: &Context, msg: &Message, regex: Regex, parsed_message: &str) -> Option { +fn retrieve_or_error( + ctx: &Context, + msg: &Message, + regex: &Regex, + parsed_message: &str, +) -> Option { let captures = match regex.captures(parsed_message) { Some(c) => c, - None => return None + None => return None, }; let id = match captures.get(1) { Some(c) => c.as_str(), - None => return None + None => return None, }; let deck = fetch_deck(id); @@ -118,11 +132,7 @@ fn dreadbot_help(ctx: &Context, msg: &Message) -> bool { } fn dreadbot_verify(ctx: &Context, msg: &Message, parsed_message: &str) -> bool { - let regex = - Regex::new(r"^verify https://www\.mtggoldfish\.com/deck/(\d*).*$") - .unwrap(); - - if let Some(deck) = retrieve_or_error(&ctx, &msg, regex, parsed_message) { + if let Some(deck) = retrieve_or_error(&ctx, &msg, &VERIFY_REGEX, parsed_message) { return respond_to_deck(ctx, &msg, &deck); } @@ -130,11 +140,7 @@ fn dreadbot_verify(ctx: &Context, msg: &Message, parsed_message: &str) -> bool { } fn dreadbot_info(ctx: &Context, msg: &Message, parsed_message: &str) -> bool { - let regex = - Regex::new(r"^info https://www\.mtggoldfish\.com/deck/(\d*).*$") - .unwrap(); - - if let Some(deck) = retrieve_or_error(&ctx, &msg, regex, parsed_message) { + if let Some(deck) = retrieve_or_error(&ctx, &msg, &INFO_REGEX, parsed_message) { return respond(ctx, &msg, &deck.info_string()); } @@ -142,11 +148,7 @@ fn dreadbot_info(ctx: &Context, msg: &Message, parsed_message: &str) -> bool { } fn dreadbot_hash(ctx: &Context, msg: &Message, parsed_message: &str) -> bool { - let regex = - Regex::new(r"^hash https://www\.mtggoldfish\.com/deck/(\d*).*$") - .unwrap(); - - if let Some(deck) = retrieve_or_error(&ctx, &msg, regex, parsed_message) { + if let Some(deck) = retrieve_or_error(&ctx, &msg, &HASH_REGEX, parsed_message) { return respond(ctx, &msg, &format!("Deck hash: {}", &deck.to_hash())); } @@ -159,9 +161,15 @@ impl EventHandler for Handler { if let Some(captures) = regex.captures(&msg.content) { if let Some(remaining_message) = captures.get(1) { - if dreadbot_verify(&ctx, &msg, remaining_message.as_str()) { return } - if dreadbot_info(&ctx, &msg, remaining_message.as_str()) { return } - if dreadbot_hash(&ctx, &msg, remaining_message.as_str()) { return } + if dreadbot_verify(&ctx, &msg, remaining_message.as_str()) { + return; + } + if dreadbot_info(&ctx, &msg, remaining_message.as_str()) { + return; + } + if dreadbot_hash(&ctx, &msg, remaining_message.as_str()) { + return; + } // Fallback to the help message dreadbot_help(&ctx, &msg); @@ -175,11 +183,9 @@ impl EventHandler for Handler { } fn main() { - let token = env::var("DISCORD_TOKEN") - .expect("Expected a token in the environment"); + let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment"); - let mut client = Client::new(&token, Handler) - .expect("Err creating client"); + let mut client = Client::new(&token, Handler).expect("Err creating client"); if let Err(why) = client.start() { println!("Client error: {:?}", why); diff --git a/src/scryfall.rs b/src/scryfall.rs index 1f044fb..75516fb 100644 --- a/src/scryfall.rs +++ b/src/scryfall.rs @@ -1,60 +1,55 @@ extern crate serde_derive; -use serde::{Deserialize}; use super::card::{Card, Cents}; use super::deck::Deck; +use serde::Deserialize; #[derive(Deserialize, Debug)] pub struct ScryfallResponse { - pub data: Vec, - pub next_page: Option + pub data: Vec, + pub next_page: Option, } #[derive(Deserialize, Debug)] pub struct ScryfallData { - pub name: String, - pub prices: ScryfallPrices, - pub card_faces: Option> + pub name: String, + pub prices: ScryfallPrices, + pub card_faces: Option>, } #[derive(Deserialize, Debug)] pub struct ScryfallCardFaces { - pub name: String + pub name: String, } #[derive(Deserialize, Debug)] pub struct ScryfallPrices { - pub usd: Option, - pub usd_foil: Option + pub usd: Option, + pub usd_foil: Option, } #[derive(Debug)] pub struct PricingSource { - pub name: String, - pub price: Cents, - pub front_name: String + pub name: String, + pub price: Cents, + pub front_name: String, } -const BASIC_LAND_NAMES: &'static [&'static str] = &[ - "Swamp", - "Island", - "Forest", - "Mountain", - "Plains" -]; +const BASIC_LAND_NAMES: &'static [&'static str] = + &["Swamp", "Island", "Forest", "Mountain", "Plains"]; fn format_scryfall_param(card: &Card) -> String { - format!("!\"{}\"", card.name) + format!("!\"{}\"", card.name) } fn get_nonfoil_price(data: &ScryfallData) -> Option { let str_price = match &data.prices.usd { - Some(price) => price, - None => return None + Some(price) => price, + None => return None, }; let price = match str_price.parse::() { - Ok(p32) => (p32 * 100f32) as Cents, - _ => return None + Ok(p32) => (p32 * 100f32) as Cents, + _ => return None, }; Some(price) @@ -62,114 +57,114 @@ fn get_nonfoil_price(data: &ScryfallData) -> Option { fn get_foil_price(data: &ScryfallData) -> Option { let str_price = match &data.prices.usd_foil { - Some(price) => price, - None => return None + Some(price) => price, + None => return None, }; let price = match str_price.parse::() { - Ok(p32) => (p32 * 100f32) as Cents, - _ => return None + Ok(p32) => (p32 * 100f32) as Cents, + _ => return None, }; Some(price) } fn get_price(data: &ScryfallData) -> Option { - let nonfoil_price = get_nonfoil_price(data); - let foil_price = get_foil_price(data); - - match (nonfoil_price, foil_price) { - (Some(nonfoil), Some(foil)) => Some(std::cmp::min(nonfoil, foil)), - (Some(nonfoil), None) => Some(nonfoil), - (None, Some(foil)) => Some(foil), - _ => None - } + let nonfoil_price = get_nonfoil_price(data); + let foil_price = get_foil_price(data); + + match (nonfoil_price, foil_price) { + (Some(nonfoil), Some(foil)) => Some(std::cmp::min(nonfoil, foil)), + (Some(nonfoil), None) => Some(nonfoil), + (None, Some(foil)) => Some(foil), + _ => None, + } } fn reduce_pricing(entries: Vec) -> Vec { - let mut prices: Vec = Vec::new(); - - for entry in entries { - let price = match get_price(&entry) { - Some(price) => price, - None => continue - }; - - let previous_entry = prices.iter_mut().find(|ps| ps.name == entry.name); - - // If it exists, update if the new price is lower - if let Some(previous_price) = previous_entry { - if price < previous_price.price { - previous_price.price = price; - } - - // Otherwise add it - } else { - // For double sided cards, we need to save their front name for goldfish - let front_name = if let Some(faces) = entry.card_faces { - faces.get(0).unwrap().name.clone() - } else { - entry.name.clone() - }; - - prices.push(PricingSource { name: entry.name, price: price, front_name: front_name }); + let mut prices: Vec = Vec::new(); + + for entry in entries { + let price = match get_price(&entry) { + Some(price) => price, + None => continue, + }; + + let previous_entry = prices.iter_mut().find(|ps| ps.name == entry.name); + + // If it exists, update if the new price is lower + if let Some(previous_price) = previous_entry { + if price < previous_price.price { + previous_price.price = price; + } + + // Otherwise add it + } else { + // For double sided cards, we need to save their front name for goldfish + let front_name = if let Some(faces) = entry.card_faces { + faces.get(0).unwrap().name.clone() + } else { + entry.name.clone() + }; + + prices.push(PricingSource { + name: entry.name, + price: price, + front_name: front_name, + }); + } } - } - prices + prices } pub fn request_pricing(deck: &Deck) -> Result, Box> { - let mut name_params = String::new(); - let mut first_flag = true; - for card in deck.cards() { - // Advance the first flag if true - let is_first = first_flag; - first_flag = false; - - // If it is a basic, do not add it to the list. This returns hundreds of cards each - if let Some(_) = BASIC_LAND_NAMES.iter().find(|name| *name == &card.name) { - continue; + let mut name_params = String::new(); + let mut first_flag = true; + for card in deck.cards() { + // Advance the first flag if true + let is_first = first_flag; + first_flag = false; + + // If it is a basic, do not add it to the list. This returns hundreds of cards each + if let Some(_) = BASIC_LAND_NAMES.iter().find(|name| *name == &card.name) { + continue; + } + + // Add to it + if !is_first { + name_params += " OR "; + } + name_params += &format_scryfall_param(card) } - // Add to it - if !is_first { name_params += " OR "; } - name_params += &format_scryfall_param(card) - } - - // If there are no names, the query returns all cards. Thats bad! Return now. - if name_params.is_empty() { return Ok(Vec::new()); } + // If there are no names, the query returns all cards. Thats bad! Return now. + if name_params.is_empty() { + return Ok(Vec::new()); + } - // Start a list of ScryfallData in case there are multiple requests - let mut data: Vec = Vec::new(); + // Start a list of ScryfallData in case there are multiple requests + let mut data: Vec = Vec::new(); - // Build the initial query - let query = + // Build the initial query + let query = format!("https://api.scryfall.com/cards/search?unique=prints&q=-is:oversized -is:digital -border:gold usd>0 ({})", name_params) .replace(" ", "%20") .replace("\"", "%22"); - // Send it and merge - let mut response: ScryfallResponse = - reqwest::Client::new() - .get(&query) - .send()? - .json()?; + // Send it and merge + let mut response: ScryfallResponse = reqwest::Client::new().get(&query).send()?.json()?; - data.append(&mut response.data); + data.append(&mut response.data); - // Consume until there is no more - while let Some(next_url) = response.next_page { - response = - reqwest::Client::new() - .get(&next_url) - .send()? - .json()?; + // Consume until there is no more + while let Some(next_url) = response.next_page { + response = reqwest::Client::new().get(&next_url).send()?.json()?; - data.append(&mut response.data); - } + data.append(&mut response.data); + } - Ok(reduce_pricing(data)) + Ok(reduce_pricing(data)) } #[test] @@ -187,60 +182,66 @@ fn test_api_call() { #[test] fn test_reduce_pricing() { - let mut scryfall_mock: Vec = Vec::new(); - - scryfall_mock.push(ScryfallData{ - name: String::from("Island"), - card_faces: None, - prices: ScryfallPrices { - usd: Some(String::from("1.00")), - usd_foil: Some(String::from("10.00")) - } - }); - - scryfall_mock.push(ScryfallData{ - name: String::from("Island"), - card_faces: None, - prices: ScryfallPrices { - usd: Some(String::from("0.50")), - usd_foil: Some(String::from("10.00")) - } - }); - - scryfall_mock.push(ScryfallData{ - name: String::from("Island"), - card_faces: None, - prices: ScryfallPrices { - usd: Some(String::from("2.00")), - usd_foil: Some(String::from("10.00")) - } - }); - - let reduced_prices = reduce_pricing(scryfall_mock); - assert_eq!(reduced_prices.len(), 1); - assert_eq!(reduced_prices.get(0).unwrap().name, "Island"); - assert_eq!(reduced_prices.get(0).unwrap().price, 50 as Cents); + let mut scryfall_mock: Vec = Vec::new(); + + scryfall_mock.push(ScryfallData { + name: String::from("Island"), + card_faces: None, + prices: ScryfallPrices { + usd: Some(String::from("1.00")), + usd_foil: Some(String::from("10.00")), + }, + }); + + scryfall_mock.push(ScryfallData { + name: String::from("Island"), + card_faces: None, + prices: ScryfallPrices { + usd: Some(String::from("0.50")), + usd_foil: Some(String::from("10.00")), + }, + }); + + scryfall_mock.push(ScryfallData { + name: String::from("Island"), + card_faces: None, + prices: ScryfallPrices { + usd: Some(String::from("2.00")), + usd_foil: Some(String::from("10.00")), + }, + }); + + let reduced_prices = reduce_pricing(scryfall_mock); + assert_eq!(reduced_prices.len(), 1); + assert_eq!(reduced_prices.get(0).unwrap().name, "Island"); + assert_eq!(reduced_prices.get(0).unwrap().price, 50 as Cents); } #[test] fn test_multiple_requests() { - let block: String = String::from("22 Air Elemental\r\n27 Counterspell\r\n28 Dark Ritual\r\n27 Disenchant\r\n21 Evolving Wilds\r\n25 Fireball\r\n34 Giant Growth\r\n25 Llanowar Elves\r\n21 Pacifism\r\n27 Serra Angel\r\n20 Shatter\r\n22 Shivan Dragon\r\n23 Stone Rain\r\n21 Swords to Plowshares\r\n21 Terror\r\n20 Unsummon"); - let deck = Deck::from_goldfish_block(String::from("10108"), block); - println!("{:?}", deck); + let block: String = String::from("22 Air Elemental\r\n27 Counterspell\r\n28 Dark Ritual\r\n27 Disenchant\r\n21 Evolving Wilds\r\n25 Fireball\r\n34 Giant Growth\r\n25 Llanowar Elves\r\n21 Pacifism\r\n27 Serra Angel\r\n20 Shatter\r\n22 Shivan Dragon\r\n23 Stone Rain\r\n21 Swords to Plowshares\r\n21 Terror\r\n20 Unsummon"); + let deck = Deck::from_goldfish_block(String::from("10108"), block); + println!("{:?}", deck); - let scryfall_resp = request_pricing(&deck).unwrap(); - println!("{:?}", scryfall_resp); + let scryfall_resp = request_pricing(&deck).unwrap(); + println!("{:?}", scryfall_resp); - assert_eq!(scryfall_resp.len(), 16); + assert_eq!(scryfall_resp.len(), 16); } #[test] fn test_double_sided_card_requests() { - let block: String = String::from("1 Delver of Secrets"); - let deck = Deck::from_goldfish_block(String::from("10108"), block); + let block: String = String::from("1 Delver of Secrets"); + let deck = Deck::from_goldfish_block(String::from("10108"), block); - let scryfall_resp = request_pricing(&deck).unwrap(); + let scryfall_resp = request_pricing(&deck).unwrap(); - assert_eq!(scryfall_resp.get(0).unwrap().name, "Delver of Secrets // Insectile Aberration"); - assert_eq!(scryfall_resp.get(0).unwrap().front_name, "Delver of Secrets"); + assert_eq!( + scryfall_resp.get(0).unwrap().name, + "Delver of Secrets // Insectile Aberration" + ); + assert_eq!( + scryfall_resp.get(0).unwrap().front_name, + "Delver of Secrets" + ); }