use std::collections::HashMap;
use serde_json::Value;
use crate::error::Result;
use crate::sql_builder::SqlBuilder;
#[derive(Debug, Clone, Default)]
pub struct SearchCardsParams {
pub name: Option<String>,
pub fuzzy_name: Option<String>,
pub localized_name: Option<String>,
pub set_code: Option<String>,
pub colors: Option<Vec<String>>,
pub color_identity: Option<Vec<String>>,
pub types: Option<String>,
pub rarity: Option<String>,
pub legal_in: Option<String>,
pub mana_value: Option<f64>,
pub mana_value_lte: Option<f64>,
pub mana_value_gte: Option<f64>,
pub text: Option<String>,
pub text_regex: Option<String>,
pub power: Option<String>,
pub toughness: Option<String>,
pub artist: Option<String>,
pub keyword: Option<String>,
pub is_promo: Option<bool>,
pub availability: Option<String>,
pub language: Option<String>,
pub layout: Option<String>,
pub set_type: Option<String>,
pub limit: Option<usize>,
pub offset: Option<usize>,
}
pub struct CardQuery<'a> {
conn: &'a crate::connection::Connection,
}
impl<'a> CardQuery<'a> {
pub fn new(conn: &'a crate::connection::Connection) -> Self {
Self { conn }
}
pub fn get_by_uuid(&self, uuid: &str) -> Result<Option<Value>> {
self.conn.ensure_views(&["cards"])?;
let (sql, params) = SqlBuilder::new("cards")
.where_eq("uuid", uuid)
.limit(1)
.build();
let rows = self.conn.execute(&sql, ¶ms)?;
Ok(rows.into_iter().next().map(|r| serde_json::to_value(r).unwrap_or(Value::Null)))
}
pub fn get_by_uuids(&self, uuids: &[&str]) -> Result<Vec<Value>> {
self.conn.ensure_views(&["cards"])?;
let (sql, params) = SqlBuilder::new("cards")
.where_in("uuid", uuids)
.build();
let rows = self.conn.execute(&sql, ¶ms)?;
Ok(rows_to_values(rows))
}
pub fn get_by_name(&self, name: &str, set_code: Option<&str>) -> Result<Vec<Value>> {
self.conn.ensure_views(&["cards"])?;
let mut qb = SqlBuilder::new("cards");
qb.where_eq("name", name);
if let Some(sc) = set_code {
qb.where_eq("setCode", sc);
}
let (sql, params) = qb.build();
let rows = self.conn.execute(&sql, ¶ms)?;
Ok(rows_to_values(rows))
}
pub fn get_printings(&self, name: &str) -> Result<Vec<Value>> {
self.get_by_name(name, None)
}
pub fn get_atomic(&self, name: &str) -> Result<Vec<Value>> {
self.conn.ensure_views(&["cards"])?;
let (sql, params) = SqlBuilder::new("cards")
.select(&["DISTINCT ON (name, faceName) *"])
.where_eq("name", name)
.build();
let rows = self.conn.execute(&sql, ¶ms)?;
if !rows.is_empty() {
return Ok(rows_to_values(rows));
}
let (sql2, params2) = SqlBuilder::new("cards")
.select(&["DISTINCT ON (name, faceName) *"])
.where_eq("faceName", name)
.build();
let rows2 = self.conn.execute(&sql2, ¶ms2)?;
Ok(rows_to_values(rows2))
}
pub fn find_by_scryfall_id(&self, scryfall_id: &str) -> Result<Vec<Value>> {
self.conn.ensure_views(&["cards", "card_identifiers"])?;
let (sql, params) = SqlBuilder::new("cards c")
.join("JOIN card_identifiers ci ON c.uuid = ci.uuid")
.where_eq("ci.scryfallId", scryfall_id)
.build();
let rows = self.conn.execute(&sql, ¶ms)?;
Ok(rows_to_values(rows))
}
pub fn random(&self, count: usize) -> Result<Vec<Value>> {
self.conn.ensure_views(&["cards"])?;
let sql = format!("SELECT * FROM cards USING SAMPLE {}", count);
let rows = self.conn.execute(&sql, &[])?;
Ok(rows_to_values(rows))
}
pub fn count(&self, filters: &HashMap<String, String>) -> Result<i64> {
self.conn.ensure_views(&["cards"])?;
let mut qb = SqlBuilder::new("cards");
qb.select(&["COUNT(*) AS cnt"]);
for (col, val) in filters {
qb.where_eq(col, val);
}
let (sql, params) = qb.build();
let rows = self.conn.execute(&sql, ¶ms)?;
let cnt = rows
.first()
.and_then(|r| r.get("cnt"))
.and_then(|v| v.as_i64())
.unwrap_or(0);
Ok(cnt)
}
pub fn search(&self, params: &SearchCardsParams) -> Result<Vec<Value>> {
let mut views: Vec<&str> = vec!["cards"];
if params.legal_in.is_some() {
views.push("card_legalities");
}
if params.localized_name.is_some() {
views.push("card_foreign_data");
}
if params.set_type.is_some() {
views.push("sets");
}
self.conn.ensure_views(&views)?;
let mut qb = SqlBuilder::new("cards");
if let Some(ref name) = params.name {
if name.contains('%') {
qb.where_like("cards.name", name);
} else {
qb.where_eq("cards.name", name);
}
}
if let Some(ref fuzzy) = params.fuzzy_name {
qb.where_fuzzy("cards.name", fuzzy, 0.8);
qb.order_by(&[&format!(
"jaro_winkler_similarity(cards.name, '{}') DESC",
fuzzy.replace('\'', "''")
)]);
}
if let Some(ref loc_name) = params.localized_name {
qb.join("JOIN card_foreign_data cfd ON cards.uuid = cfd.uuid");
qb.where_like("cfd.name", &format!("%{}%", loc_name));
}
if let Some(ref sc) = params.set_code {
qb.where_eq("cards.setCode", sc);
}
if let Some(ref colors) = params.colors {
for color in colors {
qb.where_clause(
"list_contains(cards.colors, ?)",
&[color.as_str()],
);
}
}
if let Some(ref ci) = params.color_identity {
for color in ci {
qb.where_clause(
"list_contains(cards.colorIdentity, ?)",
&[color.as_str()],
);
}
}
if let Some(ref types) = params.types {
qb.where_like("cards.type", &format!("%{}%", types));
}
if let Some(ref rarity) = params.rarity {
qb.where_eq("cards.rarity", rarity);
}
if let Some(ref format_name) = params.legal_in {
qb.join("JOIN card_legalities cl ON cards.uuid = cl.uuid");
qb.where_eq("cl.format", format_name);
qb.where_eq("cl.status", "Legal");
}
if let Some(mv) = params.mana_value {
qb.where_eq("cards.manaValue", &mv.to_string());
}
if let Some(mv) = params.mana_value_lte {
qb.where_lte("cards.manaValue", &mv.to_string());
}
if let Some(mv) = params.mana_value_gte {
qb.where_gte("cards.manaValue", &mv.to_string());
}
if let Some(ref text) = params.text {
qb.where_like("cards.text", &format!("%{}%", text));
}
if let Some(ref regex) = params.text_regex {
qb.where_regex("cards.text", regex);
}
if let Some(ref power) = params.power {
qb.where_eq("cards.power", power);
}
if let Some(ref toughness) = params.toughness {
qb.where_eq("cards.toughness", toughness);
}
if let Some(ref artist) = params.artist {
qb.where_like("cards.artist", &format!("%{}%", artist));
}
if let Some(ref kw) = params.keyword {
qb.where_clause(
"list_contains(cards.keywords, ?)",
&[kw.as_str()],
);
}
if let Some(promo) = params.is_promo {
if promo {
qb.where_clause("cards.isPromo IS TRUE", &[]);
} else {
qb.where_clause("cards.isPromo IS NOT TRUE", &[]);
}
}
if let Some(ref avail) = params.availability {
qb.where_clause(
"list_contains(cards.availability, ?)",
&[avail.as_str()],
);
}
if let Some(ref lang) = params.language {
qb.where_eq("cards.language", lang);
}
if let Some(ref layout) = params.layout {
qb.where_eq("cards.layout", layout);
}
if let Some(ref st) = params.set_type {
qb.join("JOIN sets s ON cards.setCode = s.code");
qb.where_eq("s.type", st);
}
let limit = params.limit.unwrap_or(100);
let offset = params.offset.unwrap_or(0);
qb.limit(limit);
qb.offset(offset);
let (sql, sql_params) = qb.build();
let rows = self.conn.execute(&sql, &sql_params)?;
Ok(rows_to_values(rows))
}
}
fn rows_to_values(rows: Vec<HashMap<String, Value>>) -> Vec<Value> {
rows.into_iter()
.map(|r| serde_json::to_value(r).unwrap_or(Value::Null))
.collect()
}