diff --git a/src/data/load.rs b/src/data/load.rs index fd8ead2..b462fe3 100644 --- a/src/data/load.rs +++ b/src/data/load.rs @@ -1,10 +1,7 @@ //! This module contains functions to load **Set Bundles** from [Data Dragon](https://developer.riotgames.com/docs/lol). -use std::path::*; -use glob::{glob, GlobResult}; use itertools::Itertools; use crate::data::schema::Card; -use log::*; #[derive(Debug)] @@ -15,7 +12,7 @@ enum LoadingError { /// Load a single Set Bundle and create a [Vec] with the cards contained in it. -fn load_setbundle(path: PathBuf) -> Result, LoadingError> { +fn load_setbundle(path: std::path::PathBuf) -> Result, LoadingError> { let file = std::fs::File::open(path) .map_err(LoadingError::IO)?; let data = serde_json::de::from_reader::>(file) @@ -25,11 +22,11 @@ fn load_setbundle(path: PathBuf) -> Result, LoadingError> { /// Load a single Set Bundle (similarly to [load_setbundle]), but instead of returning a [Result], return an empty [Vec] in case of failure and log a [warn]ing. -fn load_setbundle_infallible(path: PathBuf) -> Vec { +fn load_setbundle_infallible(path: std::path::PathBuf) -> Vec { match load_setbundle(path) { Ok(v) => v, Err(e) => { - warn!("{:?}", e); + log::warn!("{:?}", e); vec![] } } @@ -38,9 +35,9 @@ fn load_setbundle_infallible(path: PathBuf) -> Vec { /// Load all Set Bundles matched by the passed glob, using [load_setbundle_infallible] and then concatenating the resulting [Vec]s. pub fn load_setbundles_infallible(pattern: &str) -> Vec { - glob(pattern) + glob::glob(pattern) .expect("a valid glob") - .filter_map(GlobResult::ok) + .filter_map(glob::GlobResult::ok) .map(load_setbundle_infallible) .concat() } diff --git a/src/data/schema.rs b/src/data/schema.rs index e519ec5..5d2ec49 100644 --- a/src/data/schema.rs +++ b/src/data/schema.rs @@ -1,94 +1,94 @@ /// A single Legends of Runeterra card as represented in the data files from [Data Dragon](https://developer.riotgames.com/docs/lor). -#[derive(serde::Serialize, serde::Deserialize, Debug)] +#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] #[serde(rename_all="camelCase")] pub struct Card { /// Localized names of the cards associated with this one. /// For some reason, might not match what is contained in `associated_card_refs`. - associated_cards: Vec, + pub associated_cards: Vec, /// `card_code`s of the cards associated with this one. - associated_card_refs: Vec, + pub associated_card_refs: Vec, /// Art assets of this card. - assets: Vec, + pub assets: Vec, /// Localized names of the regions this card belongs to. - regions: Vec, + pub regions: Vec, /// IDs of the regions this card belongs to. - region_refs: Vec, + pub region_refs: Vec, /// Base attack of the card. - attack: i8, + pub attack: i8, /// Base cost of the card. - cost: i8, + pub cost: i8, /// Base health of the card. - health: i8, + pub health: i8, /// Localized description of the card, in XML. - description: String, + pub description: String, /// Localized description of the card, in plain text. - description_raw: String, + pub description_raw: String, /// Localized level up text of the card, in XML. - levelup_description: String, + pub levelup_description: String, /// Localized level up text of the card, in plain text. - levelup_description_raw: String, + pub levelup_description_raw: String, /// Flavor text of the card, displayed when its image is inspected. - flavor_text: String, + pub flavor_text: String, /// Name of the artist who drew the card. - artist_name: String, + pub artist_name: String, /// Localized name of the card. - name: String, + pub name: String, /// Unique seven-character identifier of the card. - card_code: String, + pub card_code: String, /// List of keywords of this card, with their localized names. - keywords: Vec, + pub keywords: Vec, /// List of keywords of this card, with their internal names. - keyword_refs: Vec, + pub keyword_refs: Vec, /// Localized spell speed. - spell_speed: String, + pub spell_speed: String, /// [SpellSpeed] of the card. - spell_speed_ref: SpellSpeed, + pub spell_speed_ref: SpellSpeed, /// Localized rarity of the card. - rarity: String, + pub rarity: String, /// [CardRarity] of the card. - rarity_ref: CardRarity, + pub rarity_ref: CardRarity, /// The subtypes the card has, such as `PORO`. - subtypes: Vec, + pub subtypes: Vec, /// The [CardSupertype] the card belongs to, such as `Champion`. - supertype: CardSupertype, + pub supertype: CardSupertype, /// If `true`, the card can be found in chests, crafted, or used in decks. /// If `false`, the card is not available for direct use, as it is probably created by another card. - collectible: bool, + pub collectible: bool, /// The [CardSet] the card belongs to. - set: String, + pub set: String, #[serde(rename(serialize = "type", deserialize = "type"))] - card_type: CardType, + pub card_type: CardType, } /// An art asset associated with a given card. -#[derive(serde::Serialize, serde::Deserialize, Debug)] +#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] #[serde(rename_all="camelCase")] pub struct Asset { /// URL to the card art as it is displayed in-game. - game_absolute_path: String, + pub game_absolute_path: String, /// URL to the full-size card art as it is displayed when the card is inspected. - full_absolute_path: String, + pub full_absolute_path: String, } /// Possible card types. #[non_exhaustive] -#[derive(serde::Serialize, serde::Deserialize, Debug)] +#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] pub enum CardType { /// A spell. Spell, @@ -99,12 +99,14 @@ pub enum CardType { Ability, /// A landmark. Landmark, + /// A trap or boon. + Trap, } /// Possible card supertypes. #[non_exhaustive] -#[derive(serde::Serialize, serde::Deserialize, Debug)] +#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] pub enum CardSupertype { #[serde(alias = "")] None, @@ -115,7 +117,7 @@ pub enum CardSupertype { /// Possible card rarities. #[non_exhaustive] -#[derive(serde::Serialize, serde::Deserialize, Debug)] +#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] pub enum CardRarity { #[serde(alias = "NONE")] None, @@ -132,7 +134,7 @@ pub enum CardRarity { /// Possible spell speeds. #[non_exhaustive] -#[derive(serde::Serialize, serde::Deserialize, Debug)] +#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] pub enum SpellSpeed { /// Non-spell cards have this speed. #[serde(alias = "")] @@ -148,7 +150,7 @@ pub enum SpellSpeed { /// Release sets [Card]s may belong to. #[non_exhaustive] -#[derive(serde::Serialize, serde::Deserialize, Debug)] +#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] pub enum CardSet { #[serde(rename = "Set1")] Foundations, diff --git a/src/main.rs b/src/main.rs index 9a4ec7e..21b90ce 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,27 +1,52 @@ +#[macro_use] extern crate tantivy; +#[macro_use] extern crate log; + use teloxide::prelude::*; use teloxide::types::*; use log::*; +use tantivy::{IndexReader, ReloadPolicy}; mod data; +mod search; #[tokio::main] async fn main() { pretty_env_logger::init(); - let data = data::load::load_files(); - debug!("{:?}", data); + debug!("Loading Set Bundles..."); + let cards = data::load::load_setbundles_infallible("./data/*/en_us/data/*-en_us.json"); + + debug!("Creating Tantivy index..."); + let card_index = search::index::build_card_index(); + + debug!("Writing cards to the index..."); + search::index::write_cards_to_index(&card_index, &cards); + + debug!("Creating Tantivy reader..."); + let card_reader = search::index::build_reader(&card_index); + + let inline_query_closure = |query: InlineQuery, bot: AutoSend| async move { + bot.answer_inline_query(&query.id, handle_query(&query, &card_reader)).await; + respond(()) + }; info!("patched-porobot is starting..."); let bot = Bot::from_env().auto_send(); - let handler = Update::filter_inline_query().branch(dptree::endpoint(|query: InlineQuery, bot: AutoSend| async move { - let result = InlineQueryResult::Article(InlineQueryResultArticle::new("test", "Test", InputMessageContent::Text(InputMessageContentText::new("Qui è dove metterei la mia carta, se solo ne avessi una!")))); - - let response = bot.answer_inline_query(&query.id, vec![result]).await; - respond(()) - })); + let handler = Update::filter_inline_query().branch(dptree::endpoint(inline_query_closure)); Dispatcher::builder(bot, handler).enable_ctrlc_handler().build().dispatch().await; } + + +/// Handle a [InlineQuery] incoming from Telegram. +fn handle_query(query: &InlineQuery, reader: &IndexReader) -> Vec { + debug!("Creating Tantivy searcher..."); + let card_searcher = reader.searcher(); + + let result = InlineQueryResult::Article(InlineQueryResultArticle::new("test", "Test", InputMessageContent::Text(InputMessageContentText::new("Qui è dove metterei la mia carta, se solo ne avessi una!")))); + + vec![result] +} \ No newline at end of file diff --git a/src/search/index.rs b/src/search/index.rs new file mode 100644 index 0000000..ac7bb3b --- /dev/null +++ b/src/search/index.rs @@ -0,0 +1,41 @@ +use tantivy::*; +use crate::data::schema::Card; +use crate::search::schema; + + +/// Build a [tantivy] [Index] storing [Card]s as documents. +pub fn build_card_index() -> Index { + Index::create_in_ram(schema::build_card_schema()) +} + + +/// Build a [tantivy] [IndexWriter] from the given [Index] with some preset parameters. +/// +/// Currently allocates 4 MB. +pub fn build_writer(index: &Index) -> IndexWriter { + index.writer(4_000_000) + .expect("to be able to allocate a tantivy writer") +} + + +/// Write a [Vec] of [Card]s to a [tantivy] [Index], using a writer built with [build_writer]. +pub fn write_cards_to_index(index: &Index, cards: &Vec) -> () { + let schema = index.schema(); + let mut writer = build_writer(index); + + for card in cards { + let document = schema::card_to_document(&schema, card.to_owned()); + writer.add_document(document) + .expect("to be able to add a document to the index"); + } + + writer.commit() + .expect("to be able to commit the schema changes"); +} + + +/// Build a [tantivy] [IndexReader] from the given [Index] with some preset parameters. +pub fn build_reader(index: &Index) -> IndexReader { + index.reader_builder().reload_policy(ReloadPolicy::OnCommit).try_into() + .expect("to be able to allocate a tantivy reader") +} diff --git a/src/search/mod.rs b/src/search/mod.rs new file mode 100644 index 0000000..ec6b8d2 --- /dev/null +++ b/src/search/mod.rs @@ -0,0 +1,2 @@ +pub mod schema; +pub mod index; diff --git a/src/search/schema.rs b/src/search/schema.rs new file mode 100644 index 0000000..dbd6fa6 --- /dev/null +++ b/src/search/schema.rs @@ -0,0 +1,28 @@ +use tantivy::schema::*; +use crate::data::schema::Card; + + +/// Build a [tantivy] [Schema] storing [Card]s as documents. +/// +/// TODO: Allow search on all fields. +pub fn build_card_schema() -> Schema { + let mut schema_builder = Schema::builder(); + schema_builder.add_text_field("name", TEXT); + schema_builder.add_text_field("description", TEXT); + schema_builder.add_text_field("code", STRING | STORED); + schema_builder.build() +} + + +/// Convert a [Card] to a Tantivy [Document], using the specified [Schema] (which should come from [build_card_schema]). +pub fn card_to_document(schema: &Schema, card: Card) -> Document { + let name = schema.get_field("name").unwrap(); + let description = schema.get_field("description").unwrap(); + let code = schema.get_field("code").unwrap(); + + doc!( + name => card.name, + description => card.description_raw, + code => card.card_code, + ) +} \ No newline at end of file