Add release year and filter for it

This commit is contained in:
kageru 2023-02-02 11:34:58 +01:00
parent eee93f0d6e
commit 892bbb1cc0
7 changed files with 5648 additions and 36 deletions

1
Cargo.lock generated

@ -209,6 +209,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"test-case", "test-case",
"time",
] ]
[[package]] [[package]]

@ -9,6 +9,7 @@ serde = { version = "1.0", features = ["derive"] }
nom = "7.1.3" nom = "7.1.3"
actix-web = { version = "4.3.0", default_features = false, features = ["macros"] } actix-web = { version = "4.3.0", default_features = false, features = ["macros"] }
itertools = "0.10.5" itertools = "0.10.5"
time = { version = "0.3.17", features = ["serde", "serde-human-readable"] }
[dev-dependencies] [dev-dependencies]
test-case = "2.2.2" test-case = "2.2.2"

5569
sets.json Normal file

File diff suppressed because it is too large Load Diff

@ -1,5 +1,8 @@
use serde::Deserialize; use serde::Deserialize;
use std::fmt::{self, Display, Write}; use std::fmt::{self, Display, Write};
use time::Date;
use crate::SETS_BY_NAME;
#[derive(Debug, Deserialize, PartialEq, Eq, Clone)] #[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
pub struct CardInfo { pub struct CardInfo {
@ -37,12 +40,22 @@ pub struct CardSet {
pub set_rarity: String, pub set_rarity: String,
} }
#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
pub struct Set {
pub set_name: String,
pub tcg_date: Option<Date>,
}
impl Card { impl Card {
pub fn extended_info(&self) -> Result<String, fmt::Error> { pub fn extended_info(&self) -> Result<String, fmt::Error> {
let mut s = String::with_capacity(1000); let mut s = String::with_capacity(1000);
s.push_str("<h3>Printings:</h3>"); s.push_str("<h3>Printings:</h3>");
for printing in &self.card_sets { for printing in &self.card_sets {
write!(s, "{}: {} ({})<br/>", printing.set_name, printing.set_code, printing.set_rarity)?; write!(s, "{}: {} ({})", printing.set_name, printing.set_code, printing.set_rarity)?;
if let Some(date) = SETS_BY_NAME.get(&printing.set_name.to_lowercase()).and_then(|s| s.tcg_date) {
write!(s, " - {}", date)?;
}
s.push_str("<br/>");
} }
Ok(s) Ok(s)
} }

@ -1,41 +1,53 @@
use time::Date;
use crate::{ use crate::{
data::Card, data::Card,
parser::{Field, Operator, RawCardFilter, Value, OPERATOR_CHARS}, parser::{Field, Operator, RawCardFilter, Value, OPERATOR_CHARS},
SETS_BY_NAME,
}; };
/// A struct derived from `Card` that has all fields lowercased for easier search /// A struct derived from `Card` that has all fields lowercased for easier search
#[derive(Debug, PartialEq, Eq, Clone)] #[derive(Debug, PartialEq, Eq, Clone)]
pub struct SearchCard { pub struct SearchCard {
pub id: usize, pub id: usize,
card_type: String, card_type: String,
name: String, name: String,
text: String, text: String,
atk: Option<i32>, atk: Option<i32>,
def: Option<i32>, def: Option<i32>,
attribute: Option<String>, attribute: Option<String>,
r#type: String, r#type: String,
// also includes rank // also includes rank
level: Option<i32>, level: Option<i32>,
link_rating: Option<i32>, link_rating: Option<i32>,
link_arrows: Option<Vec<String>>, link_arrows: Option<Vec<String>>,
sets: Vec<String>, sets: Vec<String>,
original_year: Option<i32>,
} }
impl From<&Card> for SearchCard { impl From<&Card> for SearchCard {
fn from(card: &Card) -> Self { fn from(card: &Card) -> Self {
Self { Self {
id: card.id, id: card.id,
card_type: card.card_type.to_lowercase(), card_type: card.card_type.to_lowercase(),
name: card.name.to_lowercase(), name: card.name.to_lowercase(),
text: card.text.to_lowercase(), text: card.text.to_lowercase(),
atk: card.atk, atk: card.atk,
def: card.def, def: card.def,
attribute: card.attribute.as_ref().map(|s| s.to_lowercase()), attribute: card.attribute.as_ref().map(|s| s.to_lowercase()),
r#type: card.r#type.to_lowercase(), r#type: card.r#type.to_lowercase(),
level: card.level, level: card.level,
link_rating: card.link_rating, link_rating: card.link_rating,
link_arrows: card.link_arrows.as_ref().map(|arrows| arrows.iter().map(|a| a.to_lowercase()).collect()), 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(), sets: card.card_sets.iter().filter_map(|s| s.set_code.split('-').next().map(str::to_lowercase)).collect(),
original_year: card
.card_sets
.iter()
.filter_map(|s| {
SETS_BY_NAME.get(&s.set_name.to_lowercase()).unwrap_or_else(|| panic!("Set {} not found", s.set_name)).tcg_date
})
.map(Date::year)
.min(),
} }
} }
} }
@ -64,6 +76,7 @@ pub fn build_filter(query: RawCardFilter) -> Result<CardFilter, String> {
} }
RawCardFilter(Field::Level, op, Value::Numerical(n)) => Box::new(move |card| op.filter_number(card.level, n)), RawCardFilter(Field::Level, op, Value::Numerical(n)) => Box::new(move |card| op.filter_number(card.level, n)),
RawCardFilter(Field::LinkRating, op, Value::Numerical(n)) => Box::new(move |card| op.filter_number(card.link_rating, n)), RawCardFilter(Field::LinkRating, op, Value::Numerical(n)) => Box::new(move |card| op.filter_number(card.link_rating, n)),
RawCardFilter(Field::Year, op, Value::Numerical(n)) => Box::new(move |card| op.filter_number(card.original_year, n)),
RawCardFilter(Field::Type, Operator::Equal, Value::String(s)) => Box::new(move |card| card.r#type == s), RawCardFilter(Field::Type, Operator::Equal, Value::String(s)) => Box::new(move |card| card.r#type == s),
RawCardFilter(Field::Type, Operator::NotEqual, Value::String(s)) => Box::new(move |card| card.r#type != s), RawCardFilter(Field::Type, Operator::NotEqual, Value::String(s)) => Box::new(move |card| card.r#type != s),
RawCardFilter(Field::Attribute, Operator::Equal, Value::String(s)) => Box::new(move |card| card.attribute.contains(&s)), RawCardFilter(Field::Attribute, Operator::Equal, Value::String(s)) => Box::new(move |card| card.attribute.contains(&s)),

@ -1,10 +1,11 @@
#![feature(option_result_contains, once_cell)] #![feature(option_result_contains, once_cell)]
use actix_web::{get, http::header, web, App, Either, HttpResponse, HttpServer}; use actix_web::{get, http::header, web, App, Either, HttpResponse, HttpServer};
use data::{Card, CardInfo}; use data::{Card, CardInfo, Set};
use filter::SearchCard; use filter::SearchCard;
use itertools::Itertools; use itertools::Itertools;
use serde::Deserialize; use serde::Deserialize;
use std::{collections::HashMap, fmt::Write, fs::File, io::BufReader, net::Ipv4Addr, sync::LazyLock, time::Instant}; use std::{collections::HashMap, fmt::Write, fs::File, io::BufReader, net::Ipv4Addr, sync::LazyLock, time::Instant};
use time::Date;
mod data; mod data;
mod filter; mod filter;
@ -13,13 +14,24 @@ mod parser;
const RESULT_LIMIT: usize = 100; const RESULT_LIMIT: usize = 100;
static CARDS: LazyLock<Vec<Card>> = LazyLock::new(|| { static CARDS: LazyLock<Vec<Card>> = LazyLock::new(|| {
serde_json::from_reader::<_, CardInfo>(BufReader::new(File::open("cards.json").expect("cards.json not found"))) let mut cards = serde_json::from_reader::<_, CardInfo>(BufReader::new(File::open("cards.json").expect("cards.json not found")))
.expect("Could not deserialize cards") .expect("Could not deserialize cards")
.data .data;
cards.iter_mut().for_each(|c| {
c.card_sets.sort_unstable_by_key(|s| SETS_BY_NAME.get(&s.set_name.to_lowercase()).and_then(|s| s.tcg_date).unwrap_or(Date::MAX))
});
cards
}); });
static CARDS_BY_ID: LazyLock<HashMap<usize, Card>> = static CARDS_BY_ID: LazyLock<HashMap<usize, Card>> =
LazyLock::new(|| CARDS.iter().map(|c| (c.id, Card { text: c.text.replace('\r', "").replace('\n', "<br/>"), ..c.clone() })).collect()); LazyLock::new(|| CARDS.iter().map(|c| (c.id, Card { text: c.text.replace('\r', "").replace('\n', "<br/>"), ..c.clone() })).collect());
static SEARCH_CARDS: LazyLock<Vec<SearchCard>> = LazyLock::new(|| CARDS.iter().map(SearchCard::from).collect()); static SEARCH_CARDS: LazyLock<Vec<SearchCard>> = LazyLock::new(|| CARDS.iter().map(SearchCard::from).collect());
static SETS_BY_NAME: LazyLock<HashMap<String, Set>> = LazyLock::new(|| {
serde_json::from_reader::<_, Vec<Set>>(BufReader::new(File::open("sets.json").expect("sets.json not found")))
.expect("Could not deserialize sets")
.into_iter()
.map(|s| (s.set_name.to_lowercase(), s))
.collect()
});
static IMG_HOST: LazyLock<String> = LazyLock::new(|| std::env::var("IMG_HOST").unwrap_or_else(|_| String::new())); static IMG_HOST: LazyLock<String> = LazyLock::new(|| std::env::var("IMG_HOST").unwrap_or_else(|_| String::new()));

@ -64,14 +64,15 @@ fn value(input: &str) -> IResult<&str, Value> {
pub enum Field { pub enum Field {
Atk = 1, Atk = 1,
Def = 2, Def = 2,
Level = 3, Level = 4,
LinkRating = 4, LinkRating = 6,
Set = 5, Year = 8,
Type = 6, Set = 10,
Attribute = 7, Type = 12,
Class = 8, Attribute = 14,
Name = 9, Class = 16,
Text = 10, Name = 18,
Text = 20,
} }
impl Display for Field { impl Display for Field {
@ -87,6 +88,7 @@ impl Display for Field {
Self::Def => "DEF", Self::Def => "DEF",
Self::LinkRating => "link rating", Self::LinkRating => "link rating",
Self::Set => "set", Self::Set => "set",
Self::Year => "year",
}) })
} }
} }
@ -105,6 +107,7 @@ impl FromStr for Field {
"lr" | "linkrating" => Self::LinkRating, "lr" | "linkrating" => Self::LinkRating,
"name" => Self::Name, "name" => Self::Name,
"set" | "s" => Self::Set, "set" | "s" => Self::Set,
"year" | "y" => Self::Year,
_ => Err(s.to_string())?, _ => Err(s.to_string())?,
}) })
} }