From 7bd717e4db5da20e41f834b36de679b33a173a2f Mon Sep 17 00:00:00 2001 From: Felipe Contreras Salinas Date: Sat, 27 Dec 2025 01:48:17 -0300 Subject: [PATCH] download index Signed-off-by: Felipe Contreras Salinas --- Cargo.lock | 52 ++++++-- Cargo.toml | 18 ++- src/card.rs | 45 ------- src/cli.rs | 19 +-- src/constants.rs | 4 + src/directories.rs | 47 ++++++++ src/downloader/card_info.rs | 231 ------------------------------------ src/downloader/mod.rs | 3 - src/editions.rs | 14 ++- src/logging.rs | 45 ++++--- src/main.rs | 39 +++--- src/malie/client.rs | 86 ++++++++++++++ src/malie/mod.rs | 3 + src/malie/models.rs | 33 ++++++ 14 files changed, 301 insertions(+), 338 deletions(-) delete mode 100644 src/card.rs create mode 100644 src/constants.rs create mode 100644 src/directories.rs delete mode 100644 src/downloader/card_info.rs delete mode 100644 src/downloader/mod.rs create mode 100644 src/malie/client.rs create mode 100644 src/malie/models.rs diff --git a/Cargo.lock b/Cargo.lock index 84d78ee..fe89780 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -985,6 +985,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -1056,8 +1065,11 @@ dependencies = [ "fluent-templates", "reqwest", "serde", + "serde_json", "strum", "tokio", + "tokio-stream", + "tokio-util", "tracing", "tracing-subscriber", ] @@ -1394,14 +1406,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.147" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "6af14725505314343e673e9ecb7cd7e8a36aa9791eb936235a3567cc31447ae4" dependencies = [ "itoa", "memchr", - "ryu", "serde", + "serde_core", + "zmij", ] [[package]] @@ -1653,11 +1666,22 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-util" -version = "0.7.14" +version = "0.7.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" dependencies = [ "bytes", "futures-core", @@ -1749,12 +1773,13 @@ dependencies = [ ] [[package]] -name = "tracing-serde" +name = "tracing-log" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" dependencies = [ - "serde", + "log", + "once_cell", "tracing-core", ] @@ -1765,15 +1790,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" dependencies = [ "matchers", + "nu-ansi-term", "once_cell", "regex-automata", - "serde", - "serde_json", "sharded-slab", "thread_local", "tracing", "tracing-core", - "tracing-serde", + "tracing-log", ] [[package]] @@ -2322,3 +2346,9 @@ dependencies = [ "quote", "syn", ] + +[[package]] +name = "zmij" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4af59da1029247450b54ba43e0b62c8e376582464bbe5504dd525fe521e7e8fd" diff --git a/Cargo.toml b/Cargo.toml index 4452029..677b4be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ clap = { version = "4.5.53", features = ["derive"] } directories = "6.0.0" fluent-templates = "0.13.2" reqwest = { version = "0.12.26", default-features = false, features = [ - "brotli", + "brotli", "http2", "gzip", "json", @@ -21,12 +21,20 @@ serde = { version = "1.0.228", default-features = false, features = [ "derive", "std", ] } +serde_json = "1.0.147" strum = { version = "0.27.2", features = ["derive"] } -tokio = { version = "1.48.0", default-features = false, features = ["fs", "rt-multi-thread", "macros"] } +tokio = { version = "1.48.0", default-features = false, features = [ + "fs", + "rt-multi-thread", + "macros", +] } +tokio-util = { version = "0.7.17", default-features = false, features = ["io"] } +tokio-stream = { version = "0.1.17", default-features = false } tracing = "0.1.44" tracing-subscriber = { version = "0.3.22", default-features = false, features = [ - "fmt", - "json", + "ansi", "env-filter", + "fmt", + "tracing", + "tracing-log", ] } - diff --git a/src/card.rs b/src/card.rs deleted file mode 100644 index 7998e4b..0000000 --- a/src/card.rs +++ /dev/null @@ -1,45 +0,0 @@ -//! Card info - -#[derive(Debug)] -pub struct CardInfo { - pub slug: String, - pub inner: InnerCardInfo, -} - -#[derive(Debug)] -pub struct InnerCardInfo { - pub name: String, - pub kind: CardKind, - pub card_type: CardType, - pub acespec: bool, - pub tagteam: bool, - pub future: bool, - pub ancient: bool, - pub specific_info: SpecificInfo, -} - -#[derive(Debug)] -pub enum CardKind { - Pokemon, - Trainer, - Energy, -} - -#[derive(Debug)] -pub enum CardType { - Basic, - Stage1, - Stage2, - Item, - Tool, - Supporter, - Stadium, - Special, -} - -#[derive(Debug)] -pub enum SpecificInfo { - PokemonInfo {}, - TrainerInfo { effect: Vec }, - EnergyInfo {}, -} diff --git a/src/cli.rs b/src/cli.rs index 7717baa..d759d4b 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,15 +1,18 @@ //! CLI parameters -use clap::Parser; +use clap::{Parser, Subcommand}; #[derive(Debug, Parser)] #[command(version, about, long_about = None)] pub struct Args { - /// Edition code - pub code: String, - /// Card number within the edition - pub number: u8, - ///Override the slug for the card - #[arg(short, long)] - pub slug: Option, + #[command(subcommand)] + pub command: Command, +} + +#[derive(Debug, Subcommand, PartialEq)] +pub enum Command { + /// Downloads the card data + DownloadData, + /// Terminal User Interface + Tui, } diff --git a/src/constants.rs b/src/constants.rs new file mode 100644 index 0000000..01521fe --- /dev/null +++ b/src/constants.rs @@ -0,0 +1,4 @@ +//! Application wide constants. + +pub const APP_NAME: &str = "ptcg-tools"; +pub const SNAKE_CASE_APP_NAME: &str = "ptcg_tools"; diff --git a/src/directories.rs b/src/directories.rs new file mode 100644 index 0000000..a57d8db --- /dev/null +++ b/src/directories.rs @@ -0,0 +1,47 @@ +//! User directories handling +use anyhow::{Result, anyhow}; +use camino::Utf8PathBuf; +use directories::ProjectDirs; + +use crate::constants::APP_NAME; + +/// Returns the path to the user data directory. +/// +/// Post condition: this function ensures the directory is already created when returning. +pub async fn data_directory() -> Result { + let user_directory = ProjectDirs::from("cl", "bstr", APP_NAME) + .ok_or_else(|| anyhow!("failed to get ProjectDirs"))? + .data_dir() + .to_path_buf(); + let user_directory = Utf8PathBuf::try_from(user_directory)?; + tokio::fs::create_dir_all(&user_directory).await?; + Ok(user_directory) +} + +/// Returns the path to the user data cache directory. +/// +/// Post condition: this function ensures the directory is already created when returning. +pub async fn data_cache_directory() -> Result { + let user_directory = ProjectDirs::from("cl", "bstr", APP_NAME) + .ok_or_else(|| anyhow!("failed to get ProjectDirs"))? + .cache_dir() + .to_path_buf(); + let user_directory = Utf8PathBuf::try_from(user_directory)?; + let user_directory = user_directory.join("data"); + tokio::fs::create_dir_all(&user_directory).await?; + Ok(user_directory) +} + +/// Returns the path to the user data cache directory. +/// +/// Post condition: this function ensures the directory is already created when returning. +pub async fn image_cache_directory() -> Result { + let user_directory = ProjectDirs::from("cl", "bstr", APP_NAME) + .ok_or_else(|| anyhow!("failed to get ProjectDirs"))? + .cache_dir() + .to_path_buf(); + let user_directory = Utf8PathBuf::try_from(user_directory)?; + let user_directory = user_directory.join("data"); + tokio::fs::create_dir_all(&user_directory).await?; + Ok(user_directory) +} diff --git a/src/downloader/card_info.rs b/src/downloader/card_info.rs deleted file mode 100644 index 65121b2..0000000 --- a/src/downloader/card_info.rs +++ /dev/null @@ -1,231 +0,0 @@ -//! Download card information. - -use anyhow::{Result, anyhow}; -use reqwest::Client; -use scraper::{Html, Selector}; - -use crate::card::{CardInfo, CardKind, CardType, InnerCardInfo, SpecificInfo}; -use crate::editions::EditionCode; -use crate::lang::Language; - -pub async fn download_card_info( - client: Client, - lang: Language, - code: EditionCode, - number: u8, - override_slug: Option<&str>, -) -> Result { - let url = format!( - "{}/{}/{number}/", - base_url(lang), - code.edition_num().to_lowercase() - ); - let response = client.get(url).send().await?; - response.error_for_status_ref()?; - let (mut slug, inner) = parse_html(lang, code, response.text().await?)?; - if let Some(override_slug) = override_slug { - slug = override_slug.into() - } - Ok(CardInfo { slug, inner }) -} - -fn base_url(lang: Language) -> &'static str { - match lang { - Language::Es => "https://www.pokemon.com/el/jcc-pokemon/cartas-pokemon/series", - Language::En => todo!(), - } -} - -fn parse_html(lang: Language, code: EditionCode, html: String) -> Result<(String, InnerCardInfo)> { - let html = Html::parse_document(&html); - let card = html - .select(&selector("div.full-card-information")?) - .next() - .ok_or(anyhow!("Couldn't find card info"))?; - let name = card - .select(&selector("h1")?) - .next() - .ok_or(anyhow!("Failed to get card name"))? - .inner_html(); - let (kind, card_type) = parse_card_type( - lang, - card.select(&selector("div.card-type > h2")?) - .next() - .ok_or(anyhow!("Failed to get card type"))? - .inner_html(), - )?; - let slug = match kind { - CardKind::Pokemon => slugify_pokemon(lang, code, &name), - CardKind::Trainer | CardKind::Energy => slugify_unique(lang, &name), - }; - let specific_info = match kind { - CardKind::Pokemon => SpecificInfo::PokemonInfo {}, - CardKind::Trainer => { - let effect = card - .select(&selector("div.ability > pre > p")?) - .map(|e| e.inner_html()) - .collect(); - SpecificInfo::TrainerInfo { effect } - } - CardKind::Energy => SpecificInfo::EnergyInfo {}, - }; - - Ok(( - slug, - InnerCardInfo { - name, - kind, - card_type, - acespec: false, - tagteam: false, - future: false, - ancient: false, - specific_info, - }, - )) -} - -fn selector(sel: &str) -> Result { - Selector::parse(sel).map_err(|_| anyhow!("failed to parse selector")) -} - -fn parse_card_type(lang: Language, text: String) -> Result<(CardKind, CardType)> { - let kind = if text.contains(trainer_pattern(lang)) || text.contains(tool_pattern(lang)) { - Ok(CardKind::Trainer) - } else if text.contains("Pokémon") { - Ok(CardKind::Pokemon) - } else if text.contains(energy_pattern(lang)) { - Ok(CardKind::Energy) - } else { - Err(anyhow!( - "Failed to get card kind (Pokemon, Trainer or Energy)" - )) - }?; - - let card_type = match kind { - CardKind::Pokemon => { - if text.contains(basic_pattern(lang)) { - Ok(CardType::Basic) - } else if text.contains(stage1_pattern(lang)) { - Ok(CardType::Stage1) - } else if text.contains(stage2_pattern(lang)) { - Ok(CardType::Stage2) - } else { - Err(anyhow!("Failed to get Pokemon type: {text}")) - } - } - CardKind::Trainer => { - if text.contains(item_pattern(lang)) { - Ok(CardType::Item) - } else if text.contains(tool_pattern(lang)) { - Ok(CardType::Tool) - } else if text.contains(stadium_pattern(lang)) { - Ok(CardType::Stadium) - } else if text.contains(supporter_pattern(lang)) { - Ok(CardType::Supporter) - } else { - Err(anyhow!("Failed to get Trainer type")) - } - } - CardKind::Energy => { - if text.contains(basic_pattern(lang)) { - Ok(CardType::Basic) - } else if text.contains(special_pattern(lang)) { - Ok(CardType::Special) - } else { - Err(anyhow!("Failed to get Pokemon type")) - } - } - }?; - - Ok((kind, card_type)) -} - -fn trainer_pattern(lang: Language) -> &'static str { - match lang { - Language::Es => "Entrenador", - Language::En => todo!(), - } -} - -fn energy_pattern(lang: Language) -> &'static str { - match lang { - Language::Es => "Energía", - Language::En => todo!(), - } -} - -fn basic_pattern(lang: Language) -> &'static str { - match lang { - Language::Es => "Básic", - Language::En => todo!(), - } -} - -fn stage1_pattern(lang: Language) -> &'static str { - match lang { - Language::Es => "Fase 1", - Language::En => todo!(), - } -} - -fn stage2_pattern(lang: Language) -> &'static str { - match lang { - Language::Es => "Fase 2", - Language::En => todo!(), - } -} - -fn item_pattern(lang: Language) -> &'static str { - match lang { - Language::Es => "Objeto", - Language::En => todo!(), - } -} - -fn tool_pattern(lang: Language) -> &'static str { - match lang { - Language::Es => "Herramienta", - Language::En => todo!(), - } -} - -fn supporter_pattern(lang: Language) -> &'static str { - match lang { - Language::Es => "Partidario", - Language::En => todo!(), - } -} - -fn stadium_pattern(lang: Language) -> &'static str { - match lang { - Language::Es => "Estadio", - Language::En => todo!(), - } -} - -fn special_pattern(lang: Language) -> &'static str { - match lang { - Language::Es => "Especial", - Language::En => todo!(), - } -} - -fn slugify_pokemon(lang: Language, code: EditionCode, name: &str) -> String { - format!("{}-{code}-{lang}", slugify(name)) -} - -fn slugify_unique(lang: Language, name: &str) -> String { - format!("{}-{lang}", slugify(name)) -} - -fn slugify(name: &str) -> String { - name.to_lowercase() - .replace("'s", "") - .replace(" ", "-") - .replace("á", "a") - .replace("é", "e") - .replace("í", "i") - .replace("ó", "o") - .replace("ú", "u") -} diff --git a/src/downloader/mod.rs b/src/downloader/mod.rs deleted file mode 100644 index 6fdf7d8..0000000 --- a/src/downloader/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -//! Data downloaders - -pub mod card_info; diff --git a/src/editions.rs b/src/editions.rs index 0031888..7926423 100644 --- a/src/editions.rs +++ b/src/editions.rs @@ -1,5 +1,6 @@ //! Editions information +use serde::Deserialize; use strum::{Display, EnumString}; pub enum EditionBlock { @@ -8,10 +9,12 @@ pub enum EditionBlock { Sm, Ssh, Sv, + Meg, } -#[derive(Clone, Copy, Display, Debug, Hash, PartialEq, Eq, EnumString)] +#[derive(Clone, Copy, Display, Debug, Hash, PartialEq, Eq, EnumString, Deserialize)] #[strum(ascii_case_insensitive, serialize_all = "lowercase")] +#[serde(rename_all = "UPPERCASE")] pub enum EditionCode { /// Sword and Shield Ssh, @@ -39,6 +42,10 @@ pub enum EditionCode { Ssp, /// Prismatic Evolutions Pre, + /// Mega Evolution + Meg, + /// Phantasmal Flames + Pfl, } impl EditionCode { @@ -57,6 +64,8 @@ impl EditionCode { EditionCode::Scr => "SV07", EditionCode::Ssp => "SV08", EditionCode::Pre => "SV8pt5", + EditionCode::Meg => "MEG1", + EditionCode::Pfl => "MEG2", } } @@ -75,6 +84,8 @@ impl EditionCode { EditionCode::Scr => "stellar-crown", EditionCode::Ssp => "surging-sparks", EditionCode::Pre => "prismatic-evolutions", + EditionCode::Meg => "mega-evolution", + EditionCode::Pfl => "phantasmal-flames", } } @@ -93,6 +104,7 @@ impl EditionCode { | EditionCode::Scr | EditionCode::Ssp | EditionCode::Pre => EditionBlock::Sv, + EditionCode::Meg | EditionCode::Pfl => EditionBlock::Meg, } } } diff --git a/src/logging.rs b/src/logging.rs index c619960..f1cd836 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -1,21 +1,28 @@ -use anyhow::{Result, anyhow}; -use camino::Utf8PathBuf; -use directories::ProjectDirs; +use anyhow::{Context, Result}; use tracing_subscriber::Layer; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; -const APP_NAME: &str = "ptcg-tools"; +use crate::constants::{APP_NAME, SNAKE_CASE_APP_NAME}; +use crate::directories::data_directory; -/// Sets up logging for the application. Since the mode we will need logging for is a TUI, we can't -/// print our logs into stdout, so we will log into a file instead. -pub async fn initialize_logging() -> Result<()> { - let user_directory = ProjectDirs::from("cl", "bstr", APP_NAME) - .ok_or_else(|| anyhow!("failed to get ProjectDirs"))? - .data_dir() - .to_path_buf(); - let user_directory = Utf8PathBuf::try_from(user_directory)?; - tokio::fs::create_dir_all(&user_directory).await?; +pub enum LogMode { + File, + Print, +} + +/// Sets up logging for the application. +pub async fn initialize_logging(mode: LogMode) -> Result<()> { + match mode { + LogMode::File => initialize_file_logging().await, + LogMode::Print => initialize_print_logging(), + } +} + +async fn initialize_file_logging() -> Result<()> { + let user_directory = data_directory() + .await + .context("While initializing logging")?; let log_path = user_directory.join(format!("{APP_NAME}.log")); let log_file = std::fs::File::create(log_path)?; let file_subscriber = tracing_subscriber::fmt::layer() @@ -25,8 +32,18 @@ pub async fn initialize_logging() -> Result<()> { .with_target(false) .with_ansi(false) .with_filter(tracing_subscriber::filter::EnvFilter::from(format!( - "{APP_NAME}=debug" + "{SNAKE_CASE_APP_NAME}=debug" ))); tracing_subscriber::registry().with(file_subscriber).init(); Ok(()) } + +fn initialize_print_logging() -> Result<()> { + tracing_subscriber::registry() + .with(tracing_subscriber::filter::EnvFilter::from(format!( + "{SNAKE_CASE_APP_NAME}=debug" + ))) + .with(tracing_subscriber::fmt::layer()) + .init(); + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index 5636400..86bb5ae 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,9 @@ -use std::str::FromStr; - -use anyhow::{Context, Result}; +use anyhow::Result; use clap::Parser; -pub mod card; pub mod cli; -pub mod downloader; +pub mod constants; +pub mod directories; pub mod editions; pub mod lang; pub mod logging; @@ -14,20 +12,21 @@ pub mod malie; #[tokio::main] async fn main() -> Result<()> { let args = cli::Args::parse(); - logging::initialize_logging().await?; - let client = reqwest::Client::new(); - let edition = - editions::EditionCode::from_str(&args.code).context("Couldn't parse edition code")?; - let number = args.number; - let slug = args.slug.as_deref(); - let card_info = downloader::card_info::download_card_info( - client.clone(), - lang::Language::Es, - edition, - number, - slug, - ) - .await?; - println!("{card_info:?}"); + let log_mode = if args.command == cli::Command::Tui { + logging::LogMode::File + } else { + logging::LogMode::Print + }; + logging::initialize_logging(log_mode).await?; + match args.command { + cli::Command::DownloadData => download_data().await?, + cli::Command::Tui => todo!(), + } + Ok(()) +} + +async fn download_data() -> Result<()> { + let client = malie::client::Client::new().await?; + client.download_all_data().await?; Ok(()) } diff --git a/src/malie/client.rs b/src/malie/client.rs new file mode 100644 index 0000000..b2b7062 --- /dev/null +++ b/src/malie/client.rs @@ -0,0 +1,86 @@ +//! Client to download data from malie.io + +use anyhow::{Context, Result, anyhow}; +use camino::Utf8PathBuf; +use tokio::fs::File; +use tokio_stream::StreamExt; +use tokio_util::io::StreamReader; +use tracing::debug; + +use super::models::Index; +use crate::directories::data_cache_directory; + +/// Client to download data from mallie.io +pub struct Client { + client: reqwest::Client, + data_cache_directory: Utf8PathBuf, +} + +const TCGL_BASE_URL: &str = "https://cdn.malie.io/file/malie-io/tcgl/export"; + +impl Client { + /// Create a new `Client` + pub async fn new() -> Result { + Ok(Self { + client: reqwest::Client::new(), + data_cache_directory: data_cache_directory().await?, + }) + } + + pub async fn download_all_data(&self) -> Result<()> { + self.download_tcgl_index_json().await?; + let index = self.load_tcgl_index().await?; + println!("{index:?}"); + Ok(()) + } + + pub async fn download_tcgl_index_json(&self) -> Result<()> { + let file_path = self.data_cache_directory.join("tcgl_index.json"); + let url = format!("{TCGL_BASE_URL}/index.json"); + self.download_if_not_exists(file_path, &url).await?; + Ok(()) + } + + async fn load_tcgl_index(&self) -> Result { + let file_path = self.data_cache_directory.join("tcgl_index.json"); + let index = tokio::fs::read_to_string(&file_path) + .await + .with_context(|| format!("Failed to read {file_path}"))?; + let index: Index = + serde_json::from_str(&index).with_context(|| format!("Couldn't parse {file_path}"))?; + Ok(index) + } + + async fn download_if_not_exists(&self, file_path: Utf8PathBuf, url: &str) -> Result<()> { + if let Ok(true) = tokio::fs::try_exists(&file_path).await { + debug!("Found {}, skipping download", &file_path); + return Ok(()); + } + + let response = self.client.get(url).send().await?; + if !response.status().is_success() { + return Err(anyhow!( + "Error {} when downloading: {}", + response.status(), + url + )); + } + + let mut file = File::create_new(&file_path) + .await + .with_context(|| format!("Couldn't create file {file_path}"))?; + tokio::io::copy_buf( + &mut StreamReader::new( + response + .bytes_stream() + .map(|result| result.map_err(std::io::Error::other)), + ), + &mut file, + ) + .await + .with_context(|| format!("While writing to file {file_path}"))?; + file.sync_all().await?; + + Ok(()) + } +} diff --git a/src/malie/mod.rs b/src/malie/mod.rs index 574e877..3122192 100644 --- a/src/malie/mod.rs +++ b/src/malie/mod.rs @@ -1 +1,4 @@ //! Module to interact with the PTCG data from malie.io + +pub mod client; +pub mod models; diff --git a/src/malie/models.rs b/src/malie/models.rs new file mode 100644 index 0000000..a44f7a8 --- /dev/null +++ b/src/malie/models.rs @@ -0,0 +1,33 @@ +//! Models for malie.io exports + +use std::collections::HashMap; + +use serde::Deserialize; + +use crate::editions::EditionCode; + +pub type Index = HashMap>; + +#[derive(Debug, Deserialize, Eq, PartialEq, Hash)] +pub enum Lang { + #[serde(rename = "de-DE")] + De, + #[serde(rename = "en-US")] + En, + #[serde(rename = "es-ES")] + Es, + #[serde(rename = "es-419")] + EsLa, + #[serde(rename = "it-IT")] + It, + #[serde(rename = "fr-FR")] + Fr, + #[serde(rename = "pt-BR")] + Pt, +} + +#[derive(Debug, Deserialize)] +pub struct Edition { + path: String, + abbr: Option, +}