use async_trait::async_trait;
use url::Url;
use crate::list::ListIter;
use crate::Card;
pub mod advanced;
pub mod param;
pub mod query;
#[async_trait]
pub trait Search {
fn write_query(&self, url: &mut Url) -> crate::Result<()>;
#[cfg(test)]
fn query_string(&self) -> crate::Result<String> {
let mut url = Url::parse("http://localhost")?;
self.write_query(&mut url)?;
Ok(url.query().unwrap_or_default().to_string())
}
async fn search(&self) -> crate::Result<ListIter<Card>> {
Card::search(self).await
}
async fn search_all(&self) -> crate::Result<Vec<Card>> {
Card::search_all(self).await
}
async fn random(&self) -> crate::Result<Card> {
Card::search_random(self).await
}
}
impl<T: Search + ?Sized> Search for &T {
fn write_query(&self, url: &mut Url) -> crate::Result<()> {
<T as Search>::write_query(*self, url)
}
}
impl<T: Search + ?Sized> Search for &mut T {
fn write_query(&self, url: &mut Url) -> crate::Result<()> {
<T as Search>::write_query(*self, url)
}
}
#[inline]
fn write_query_string<S: ToString + ?Sized>(query: &S, url: &mut Url) -> crate::Result<()> {
url.query_pairs_mut()
.append_pair("q", query.to_string().as_str());
Ok(())
}
impl Search for str {
fn write_query(&self, url: &mut Url) -> crate::Result<()> {
write_query_string(self, url)
}
}
impl Search for String {
fn write_query(&self, url: &mut Url) -> crate::Result<()> {
write_query_string(self, url)
}
}
pub mod prelude {
pub use super::advanced::{SearchOptions, SortDirection, SortOrder, UniqueStrategy};
pub use super::param::compare::{eq, gt, gte, lt, lte, neq};
pub use super::param::criteria::{CardIs, PrintingIs};
pub use super::param::value::{
artist, artist_count, banned, block, border_color, cheapest, cmc, collector_number, color,
color_count, color_identity, color_identity_count, cube, date, devotion, eur, flavor_text,
format, frame, full_oracle_text, game, illustration_count, in_game, in_language, in_rarity,
in_set, in_set_type, keyword, language, loyalty, mana, name, oracle_text,
paper_print_count, paper_set_count, pow_tou, power, print_count, produces, rarity,
restricted, set, set_count, set_type, tix, toughness, type_line, usd, usd_foil, watermark,
year, Devotion, NumProperty, Regex,
};
pub use super::param::{exact, Param};
pub use super::query::{not, Query};
pub use super::Search;
}
#[cfg(test)]
mod tests {
use super::prelude::*;
use crate::Card;
use futures::stream::StreamExt;
#[tokio::test]
async fn basic_search() {
let cards = SearchOptions::new()
.query(Query::And(vec![
name("lightning"),
name("helix"),
cmc(eq(2)),
]))
.unique(UniqueStrategy::Prints)
.search()
.await
.unwrap()
.into_stream()
.map(|c| c.unwrap())
.collect::<Vec<_>>()
.await;
assert!(cards.len() > 1);
for card in cards {
assert_eq!(card.name, "Lightning Helix")
}
}
#[tokio::test]
async fn basic_search_buffered() {
let cards = SearchOptions::new()
.query(Query::And(vec![
name("lightning"),
name("helix"),
cmc(eq(2)),
]))
.unique(UniqueStrategy::Prints)
.search()
.await
.unwrap()
.into_stream_buffered(10)
.map(|c| c.unwrap())
.collect::<Vec<_>>()
.await;
assert!(cards.len() > 1);
for card in cards {
assert_eq!(card.name, "Lightning Helix")
}
}
#[tokio::test]
async fn random_works_with_search_options() {
assert!(SearchOptions::new()
.query(keyword("storm"))
.unique(UniqueStrategy::Art)
.sort(SortOrder::Usd, SortDirection::Ascending)
.extras(true)
.multilingual(true)
.variations(true)
.random()
.await
.unwrap()
.oracle_text
.unwrap()
.to_lowercase()
.contains("storm"));
}
#[tokio::test]
async fn finds_alpha_lotus() {
let mut search = SearchOptions::new();
search
.query(exact("Black Lotus"))
.unique(UniqueStrategy::Prints)
.sort(SortOrder::Released, SortDirection::Ascending);
eprintln!("search query: {}", search.query_string().unwrap());
assert_eq!(
Card::search(&search)
.await
.expect("search failed")
.into_stream()
.next()
.await
.expect("empty stream")
.expect("deserialization failed")
.set
.to_string(),
"lea",
);
}
#[tokio::test]
async fn finds_alpha_lotus_buffered() {
let mut search = SearchOptions::new();
search
.query(exact("Black Lotus"))
.unique(UniqueStrategy::Prints)
.sort(SortOrder::Released, SortDirection::Ascending);
eprintln!("search query: {}", search.query_string().unwrap());
assert_eq!(
Card::search(&search)
.await
.expect("search failed")
.into_stream_buffered(10)
.next()
.await
.expect("empty stream")
.expect("deserialization failed")
.set
.to_string(),
"lea",
);
}
#[tokio::test]
async fn rarity_comparison() {
use crate::card::Rarity;
let cards = SearchOptions::new()
.query(rarity(gt(Rarity::Mythic)))
.search()
.await
.unwrap()
.into_stream()
.collect::<Vec<_>>()
.await;
assert!(cards.len() >= 9, "Couldn't find the Power Nine from VMA.");
assert!(cards
.into_iter()
.map(|c| c.unwrap())
.all(|c| c.rarity > Rarity::Mythic));
}
#[tokio::test]
async fn rarity_comparison_buffered() {
use crate::card::Rarity;
let cards = SearchOptions::new()
.query(rarity(gt(Rarity::Mythic)))
.search()
.await
.expect("search failed")
.into_stream_buffered(10)
.collect::<Vec<_>>()
.await;
assert!(cards.len() >= 9, "Couldn't find the Power Nine from VMA.");
assert!(
cards
.into_iter()
.map(|c| c.unwrap())
.all(|c| c.rarity > Rarity::Mythic),
"rarity should above mythic"
);
}
#[tokio::test]
async fn numeric_property_comparison() {
let card = Card::search_random(Query::And(vec![
power(eq(NumProperty::Toughness)),
pow_tou(eq(NumProperty::Cmc)),
not(CardIs::Funny),
not(CardIs::Transform),
not(CardIs::Flip),
not(CardIs::ModalDfc),
]))
.await
.unwrap();
let power = card
.power
.and_then(|s| s.parse::<u32>().ok())
.unwrap_or_default();
let toughness = card
.toughness
.and_then(|s| s.parse::<u32>().ok())
.unwrap_or_default();
assert_eq!(power, toughness);
assert_eq!(power + toughness, card.cmc.unwrap_or_default() as u32);
let card = Card::search(pow_tou(gt(NumProperty::Year)))
.await
.unwrap()
.into_stream()
.map(|c| c.unwrap())
.any(|c| async move { &c.name == "Infinity Elemental" })
.await;
assert!(card);
}
#[tokio::test]
async fn numeric_property_comparison_buffered() {
let card = Card::search_random(Query::And(vec![
power(eq(NumProperty::Toughness)),
pow_tou(eq(NumProperty::Cmc)),
not(CardIs::Funny),
]))
.await
.unwrap();
let power = card
.power
.and_then(|s| s.parse::<u32>().ok())
.unwrap_or_default();
let toughness = card
.toughness
.and_then(|s| s.parse::<u32>().ok())
.unwrap_or_default();
assert_eq!(
power, toughness,
"power was not equal to toughness for card {}",
card.name
);
assert_eq!(
power + toughness,
card.cmc.unwrap_or_default() as u32,
"power and toughness added was not equal to cmc for card {}",
card.name
);
let card = Card::search(pow_tou(gt(NumProperty::Year)))
.await
.unwrap()
.into_stream_buffered(10)
.map(|c| c.unwrap())
.any(|c| async move { &c.name == "Infinity Elemental" })
.await;
assert!(card);
}
#[test]
fn query_string_sanity_check() {
let query = cmc(4).and(name("Yargle"));
assert_eq!(
query.query_string().unwrap(),
"q=%28cmc%3A4+AND+name%3A%22Yargle%22%29"
);
}
}