diff --git a/CHANGELOG.md b/CHANGELOG.md index c387468..351bae2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,20 @@ - `Released` type has been changed to a struct with an inner tuple value, though to get the regions, you use the functions: - `Released::japan()`, `Released::global()`, and `Released::china()` for example. -## Fixes ⚒️ +### Fixes ⚒️ - Applied a change to the `Student::position` function, was passing in the `Student::armor_type` for some reason... oops! + +## 0.5.0 - 2024-04-01 + +### Additions ✨ + +Added the new `blocking` feature. It is not enabled by default, so you must require it if you wish to use it! + +- This uses reqwest's `blocking` feature to handle all requests in a non-asynchronous way. + +### Changes 📝 + +- Changed how some internal deserialization and hashing works in the crate. + +### Fixes ⚒️ diff --git a/Cargo.toml b/Cargo.toml index 0819534..687f039 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "blue_archive" -version = "0.4.0" +version = "0.5.0" edition = "2021" license-file = "LICENSE" description = "A Blue Archive api wrapper for Rust, based off of SchaleDB's data: https://github.com/lonqie/SchaleDB" @@ -33,3 +33,11 @@ strum_macros = "0.26" # futures = "0.3" # chrono = { version = "0.4", features = ["serde"] } + +[features] +blocking = ["reqwest/blocking"] + +[[example]] +name = "get_all_students" +path = "examples/blocking/get_all_students.rs" +required-features = ["blocking"] diff --git a/examples/blocking/get_all_students.rs b/examples/blocking/get_all_students.rs new file mode 100644 index 0000000..7297af6 --- /dev/null +++ b/examples/blocking/get_all_students.rs @@ -0,0 +1,14 @@ +use anyhow::Result; +use blue_archive::Language; + +pub fn main() -> Result<()> { + // You can obtain the students in a blocking context now. + let students = blue_archive::blocking::get_all_students(Language::English)?; + println!("# of students: {}", students.len()); + // You may also use filtering and the StudentFetcher as you would usually do, though with blocking you will have to use the `new_blocking` function. + let fetched = blue_archive::StudentFetcher::new_blocking(Language::Japanese)?; + if let Some(student) = fetched.get_random_student() { + println!("Randomized Student: [{student}]") + } + Ok(()) +} diff --git a/examples/fetch_currency.rs b/examples/fetch_currency.rs index 9f0ba60..5a8d255 100644 --- a/examples/fetch_currency.rs +++ b/examples/fetch_currency.rs @@ -12,7 +12,7 @@ async fn main() -> anyhow::Result<()> { println!("Pyroxenes"); println!("--------------------------"); println!( - "{:?}", + "{:#?}", blue_archive::fetch_currency_by_name("Pyroxenes", Language::English) .await? .unwrap() diff --git a/examples/fetch_equipment.rs b/examples/fetch_equipment.rs index ba77b9a..89a619c 100644 --- a/examples/fetch_equipment.rs +++ b/examples/fetch_equipment.rs @@ -1,9 +1,17 @@ -use blue_archive::Language; +use blue_archive::{types::equipment::EquipmentCategory, Language}; #[tokio::main] async fn main() -> anyhow::Result<()> { for equipment in blue_archive::fetch_all_equipment(Language::English).await? { println!("{}", equipment.name) } + + let hats = blue_archive::fetch_equipment_by_category(Language::English, EquipmentCategory::Hat) + .await?; + + for hat in &hats { + println!("[{}] -> {}", hat.id, hat.name) + } + Ok(()) } diff --git a/src/api/blocking/currency.rs b/src/api/blocking/currency.rs new file mode 100644 index 0000000..a2bb98c --- /dev/null +++ b/src/api/blocking/currency.rs @@ -0,0 +1,59 @@ +use std::borrow::Borrow; + +use crate::types::currency::Currency; + +use crate::Language; + +use super::{ + internal::{get_response, Endpoint}, + BlueArchiveError, Client, Result, +}; + +/** + Fetches all existing **[`Currency`]** currently in the database. + + # Examples + ``` + use blue_archive::Language; + + fn main() -> anyhow::Result<()> { + println!( + "Total Currencies: [{}]", + blue_archive::blocking::get_all_currencies(Language::English).len() + ); + Ok(()) + } + ``` +*/ +fn get_all_currencies(language: impl Borrow) -> Result, BlueArchiveError> { + Ok( + get_response(&Endpoint::Currency, language.borrow(), &Client::new())? + .json::>()?, + ) +} + +/** + Fetches a specific **[`Currency`]** that matches with a provided **`name`** argument. + + # Examples + ``` + use blue_archive::Language; + + fn main() -> anyhow::Result<()> { + let pyroxenes_now = blue_archive::blocking::get_currency_by_name("Pyroxenes", Language::English) + .unwrap(); + println!("Pyroxenes"); + println!("--------------------------"); + println!("{:?}", pyroxenes_now); + Ok(()) + } + ``` +*/ +pub fn get_currency_by_name( + name: impl AsRef, + language: impl Borrow, +) -> Result, BlueArchiveError> { + Ok(get_all_currencies(language)? + .into_iter() + .find(|currency| currency.name.to_lowercase() == name.as_ref().to_lowercase())) +} diff --git a/src/api/blocking/enemy.rs b/src/api/blocking/enemy.rs new file mode 100644 index 0000000..474a5f9 --- /dev/null +++ b/src/api/blocking/enemy.rs @@ -0,0 +1,32 @@ +use std::borrow::Borrow; + +use crate::types::enemy::Enemy; + +use super::{ + internal::{get_response, Endpoint}, + BlueArchiveError, Client, Result, +}; + +use crate::Language; + +/// Fetches all [`Enemy`]'s that are currently in the database. +pub fn get_all_enemies(language: impl Borrow) -> Result, BlueArchiveError> { + Ok( + get_response(&Endpoint::Enemies, language.borrow(), &Client::new())? + .json::>()?, + ) +} + +/** + Fetches a specific **[`Enemy`]** that matches with a provided **`name`** argument. + + +*/ +pub fn get_enemy_by_name( + language: impl Borrow, + name: impl AsRef, +) -> Result, BlueArchiveError> { + Ok(get_all_enemies(language)? + .into_iter() + .find(|enemy| enemy.name.to_lowercase() == name.as_ref().to_lowercase())) +} diff --git a/src/api/blocking/equipment.rs b/src/api/blocking/equipment.rs new file mode 100644 index 0000000..c9f3aed --- /dev/null +++ b/src/api/blocking/equipment.rs @@ -0,0 +1,41 @@ +use std::borrow::Borrow; + +use crate::types::equipment::{Equipment, EquipmentCategory}; + +use crate::Language; + +use super::{ + internal::{get_response, Endpoint}, + BlueArchiveError, Client, Result, +}; + +/** Fetches all equipment in the database. */ +pub fn get_all_equipment( + language: impl Borrow, +) -> Result, BlueArchiveError> { + Ok( + get_response(&Endpoint::Equipment, language.borrow(), &Client::new())? + .json::>()?, + ) +} + +/** Fetches all equipment that is equal to the given **`name`**. */ +pub fn get_equipment_by_name( + language: impl Borrow, + name: impl AsRef, +) -> Result, BlueArchiveError> { + Ok(get_all_equipment(language)? + .into_iter() + .find(|equipment| equipment.name.to_lowercase() == name.as_ref().to_lowercase())) +} + +/** Fetches all equipment that is equal to the given **[`EquipmentCategory`]**. */ +pub fn get_equipment_by_category( + language: impl Borrow, + category: EquipmentCategory, +) -> Result, BlueArchiveError> { + Ok(get_all_equipment(language)? + .into_iter() + .filter(|equipment| equipment.category == category) + .collect::>()) +} diff --git a/src/api/blocking/mod.rs b/src/api/blocking/mod.rs new file mode 100644 index 0000000..d7dc17c --- /dev/null +++ b/src/api/blocking/mod.rs @@ -0,0 +1,14 @@ +pub mod currency; +pub mod enemy; +pub mod equipment; +pub mod raid; +pub mod student; +pub mod summon; + +use anyhow::Result; + +use super::internal; +use crate::BlueArchiveError; +use reqwest::blocking::Client; + +pub use self::{currency::*, enemy::*, equipment::*, raid::*, student::*, summon::*}; diff --git a/src/api/blocking/raid.rs b/src/api/blocking/raid.rs new file mode 100644 index 0000000..e3ac86f --- /dev/null +++ b/src/api/blocking/raid.rs @@ -0,0 +1,17 @@ +//! Functions primarily for geting [`RaidData`]. + +use std::borrow::Borrow; + +use anyhow::Result; + +use crate::{types::RaidData, Language}; + +use super::{ + internal::{get_response, Endpoint}, + BlueArchiveError, Client, +}; + +/// Fetches **[`RaidData`]**, which contains information related to raids in Blue Archive. +pub fn get_raid_data(language: impl Borrow) -> Result { + Ok(get_response(&Endpoint::Raids, language.borrow(), &Client::new())?.json::()?) +} diff --git a/src/api/blocking/student.rs b/src/api/blocking/student.rs new file mode 100644 index 0000000..1ce7dda --- /dev/null +++ b/src/api/blocking/student.rs @@ -0,0 +1,106 @@ +//! Functions primarily for geting [`Student`] data. + +use std::borrow::Borrow; + +use rand::seq::IteratorRandom; + +use crate::{ + filter::student::StudentFilterOptions, + types::{students::student::StudentImageData, Student}, + Language, +}; + +use super::{ + internal::{get_response, Endpoint}, + BlueArchiveError, Client, Result, +}; + +/// Fetches all students with extra data, which includes the images of the **[`Students`][`Student`]** among other things. +pub fn get_all_students(language: impl Borrow) -> Result, BlueArchiveError> { + let client = Client::new(); + let mut students = + get_response(&Endpoint::Students, language.borrow(), &client)?.json::>()?; + + students + .iter_mut() + .for_each(|student| student.image = StudentImageData::new(student)); + + Ok(students) +} + +/** + Fetches a **[`Student`]** by a `name` from a set of names. + + ## Different Methods + - Searching with an associated tag, such as **`Iori (Swimsuit)`** + - It is recommended to use the last name and an associated tag if you are looking for a **[`Student`]** with a different appearance. + - Searching via. the **last name (`surname`)**. + - Searching via. the **first name**. + - Searching via. the **first name and last name together**, and vise versa (e.g. Asuna Ichinose/Ichinose Asuna). + + # Examples + ``` + use anyhow::Result; + + use blue_archive::Language; + + fn main() -> Result<()> { + // Fetching asuna is relatively simple... + let asuna = blue_archive::blocking::get_student_by_name("Asuna", Language::English)?.unwrap(); + println!("{}", asuna); + Ok(()) + } + ``` +*/ +pub fn get_student_by_name( + name: impl AsRef, + language: impl Borrow, +) -> Result, BlueArchiveError> { + let mut matched_student = None; + + for student in get_all_students(language)? { + let lowercased = name.as_ref().to_lowercase(); + let maybe_student = (lowercased == student.name.to_lowercase() + || lowercased == student.first_name.to_lowercase() + || lowercased == student.last_name.to_lowercase() + || lowercased == student.full_name_last().to_lowercase() + || lowercased == student.full_name_first().to_lowercase()) + .then_some(student); + if let Some(student) = maybe_student { + matched_student = Some(student); + break; + } + } + + Ok(matched_student) +} + +/// Attempts to get a random **[`Student`]**. +/// +/// If geting the data fails, then it will return a [`BlueArchiveError`], as the data would not have any **[`Students`][`Student`]**. +pub fn get_random_student( + language: impl Borrow, +) -> Result, BlueArchiveError> { + Ok(get_all_students(language)? + .into_iter() + .choose(&mut rand::thread_rng())) +} + +/// Attempts to get a random amount of **[`Students`][`Student`]** depending on the specified **`amount`**. +/// +/// Depending on the **`amount`** inserted, if it exceeds the total length of **[`Students`][`Student`]** from the data, it will just return everything. +/// +/// If geting the data fails, then it will return a [`BlueArchiveError`], as the data would not have any **[`Students`][`Student`]**. +pub fn get_random_students( + language: impl Borrow, + amount: usize, +) -> Result, BlueArchiveError> { + Ok(get_all_students(language)? + .into_iter() + .choose_multiple(&mut rand::thread_rng(), amount)) +} + +/// Returns **[`StudentFilterOptions`]** to be used with the provided **[`Vec`]** for filtering. +pub fn filter(students: &Vec) -> StudentFilterOptions { + StudentFilterOptions::new(students) +} diff --git a/src/api/blocking/summon.rs b/src/api/blocking/summon.rs new file mode 100644 index 0000000..222f09c --- /dev/null +++ b/src/api/blocking/summon.rs @@ -0,0 +1,18 @@ +//! Functions primarily for geting [`Summon`] data. + +use std::borrow::Borrow; + +use crate::{types::Summon, Language}; + +use super::{ + internal::{get_response, Endpoint}, + BlueArchiveError, Client, +}; + +/// Fetches all **[`Summons`][`Summon`]** from the data. +pub fn get_all_summons(language: impl Borrow) -> Result, BlueArchiveError> { + Ok( + get_response(&Endpoint::Summons, language.borrow(), &Client::new())? + .json::>()?, + ) +} diff --git a/src/api/mod.rs b/src/api/mod.rs index 4a701e1..2704424 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,4 +1,6 @@ //! The main module where obtaining the data happens. +#[cfg(feature = "blocking")] +pub mod blocking; pub mod currency; pub mod enemy; pub mod equipment; @@ -50,4 +52,19 @@ pub(crate) mod internal { ); Ok(client.get(url).send().await?.error_for_status()?) } + + #[cfg(feature = "blocking")] + pub(crate) fn get_response( + endpoint: &Endpoint, + language: &Language, + client: &reqwest::blocking::Client, + ) -> Result { + let url = format!( + "{}/{}/{}.json", + DATA_URI, + language.id(), + endpoint.to_string().to_lowercase() + ); + Ok(client.get(url).send()?.error_for_status()?) + } } diff --git a/src/fetcher/student.rs b/src/fetcher/student.rs index 9b181d9..5fce2b0 100644 --- a/src/fetcher/student.rs +++ b/src/fetcher/student.rs @@ -1,5 +1,7 @@ //! Contains the **[`StudentFetcher`]** structure. +use std::borrow::Borrow; + use crate::{filter::student::StudentFilterOptions, types::Student, BlueArchiveError, Language}; use anyhow::Result; @@ -14,14 +16,20 @@ pub struct StudentFetcher { impl StudentFetcher { /// Creates a new **[`StudentFetcher`]** by fetching **[`Student`]** data. /// Has a chance to fail as it attempts to fetch all students. - pub async fn new( - language: impl std::borrow::Borrow, - ) -> Result { + pub async fn new(language: impl Borrow) -> Result { Ok(Self { students: crate::fetch_all_students(language).await?, }) } + #[cfg(feature = "blocking")] + /// Creates a new student fetcher using the **[`crate::blocking`]** module. + pub fn new_blocking(language: impl Borrow) -> Result { + Ok(Self { + students: crate::blocking::get_all_students(language)?, + }) + } + /** Gets a **[`Student`]** by a `name` from a set of names. diff --git a/src/lib.rs b/src/lib.rs index a40e6f6..745f010 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,10 +35,15 @@ pub mod filter; pub(crate) mod serialization; pub mod types; +#[cfg(feature = "blocking")] +pub use api::blocking; + pub use api::{currency::*, enemy::*, equipment::*, raid::*, student::*, summon::*}; + pub use enums::{ Armor, BulletType, Club, Language, Position, School, Squad, TacticalRole, WeaponType, }; + pub use errors::BlueArchiveError; pub use fetcher::StudentFetcher; pub use filter::student::StudentFilter; diff --git a/src/serialization/mod.rs b/src/serialization/mod.rs index 9219038..8b65018 100644 --- a/src/serialization/mod.rs +++ b/src/serialization/mod.rs @@ -3,6 +3,5 @@ use serde::{Deserialize, Deserializer}; pub(crate) fn deserialize_html_encoded_string<'de, D: Deserializer<'de>>( deserializer: D, ) -> Result { - let buffer = String::deserialize(deserializer)?; - Ok(html_escape::decode_html_entities(&buffer).into()) + Ok(html_escape::decode_html_entities(&String::deserialize(deserializer)?).into()) } diff --git a/src/types/mod.rs b/src/types/mod.rs index fa3c8d5..1073210 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -17,7 +17,7 @@ pub use summons::Summon; /// **A Blue Archive ID**. /// /// Basically wraps around a [`u32`], and exists for representation of an identifier that can be filtered and have extra functionality. -#[derive(Debug, PartialEq, Clone)] +#[derive(Debug, PartialEq, Eq, Hash, Clone)] pub struct ID(u32); impl ID { @@ -52,7 +52,7 @@ impl<'de> Deserialize<'de> for ID { } } -#[derive(Debug, Deserialize, Serialize, PartialEq, Clone)] +#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)] pub enum SkillKind { #[serde(alias = "weaponpassive")] WeaponPassive, @@ -72,7 +72,7 @@ pub enum SkillKind { Unknown, } -#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] #[serde(rename_all = "PascalCase")] #[serde(tag = "Type")] pub enum Effect { @@ -252,13 +252,13 @@ pub enum Effect { Unknown, } -#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] #[serde(untagged)] pub enum ScaleValue { D1(Vec), D2(Vec>), } -#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] #[serde(rename_all = "PascalCase")] pub struct Restriction { pub property: String, @@ -267,14 +267,14 @@ pub struct Restriction { pub value: RestrictValue, } -#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] #[serde(untagged)] pub enum RestrictValue { String(String), I32(i32), } -#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] #[serde(rename_all = "PascalCase")] pub struct Frames { pub attack_enter_duration: u8, @@ -286,7 +286,7 @@ pub struct Frames { pub attack_reload_duration: u8, } -#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] pub enum CriticalCheck { Check, Always, diff --git a/src/types/students/student.rs b/src/types/students/student.rs index 9480484..66f4956 100644 --- a/src/types/students/student.rs +++ b/src/types/students/student.rs @@ -13,7 +13,7 @@ use crate::{ use super::Height; -#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] #[serde(rename_all = "PascalCase")] pub struct Student { /// The **[`ID`]** of the student. @@ -122,6 +122,12 @@ pub struct Student { pub image: StudentImageData, } +impl std::hash::Hash for Student { + fn hash(&self, state: &mut H) { + self.id.hash(state); + } +} + impl Student { /// Gets the full name of a student, with the **surname (`family_name`)** coming first. pub fn full_name_last(&self) -> String { @@ -235,7 +241,7 @@ impl std::fmt::Display for Student { /// A [`Student`] specific skill. /// /// A great portion of it is raw data that has not been fully deserialized and represented. -#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] #[serde(rename_all = "PascalCase")] pub struct Skill { #[serde(alias = "SkillType")] @@ -249,7 +255,7 @@ pub struct Skill { } /// A [`Student`] specific summon. -#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] #[serde(rename_all = "PascalCase")] pub struct Summon { pub id: ID, @@ -263,7 +269,7 @@ pub struct Summon { /// There is an issue where Gear in data is represented as `"gear": {}`, therefore this is a mitigation against that. /// If you have a better implementation of handling this, as in allowing for me to represent the data as an `Option`, please send a PR. /// todo: Could use #[serde(skip_serializing_if = "...")] -#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] #[serde(untagged)] pub enum GearKind { Present(Gear), @@ -279,7 +285,7 @@ impl GearKind { } } -#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] #[serde(rename_all = "PascalCase")] pub struct Gear { /// Whether a specific gear was **[`Released`]** or not in a specific region. @@ -301,11 +307,11 @@ impl Gear { } /// There is an issue where Gear in data is represented as `"gear": {}`, therefore this is a mitigation against that. /// If you have a better implementation of handling this, as in allowing for me to represent the data as an `Option<...>`, please send a PR. -#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] #[serde(rename_all = "PascalCase")] pub struct Empty {} -#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] #[serde(rename_all = "PascalCase")] pub struct Weapon { /// The name of the weapon. @@ -327,7 +333,7 @@ pub struct Weapon { } /// The level-up type of a **[`Weapon`]**. -#[derive(Debug, strum_macros::Display, Serialize, Deserialize, PartialEq, Clone)] +#[derive(Debug, strum_macros::Display, Serialize, Deserialize, PartialEq, Eq, Clone)] pub enum LevelUpType { Standard, Premature, @@ -337,7 +343,7 @@ pub enum LevelUpType { } /// Image data related to a **[`Student`]**. -#[derive(Debug, Default, PartialEq, Clone)] +#[derive(Debug, Default, PartialEq, Eq, Clone)] pub struct StudentImageData { /// The portrait associated with this **[`Student`]**. pub portrait: Portrait, @@ -364,7 +370,7 @@ impl StudentImageData { } /// Contains portrait data of a **[`Student`]**. -#[derive(Debug, Default, PartialEq, Clone)] +#[derive(Debug, Default, PartialEq, Eq, Clone)] pub struct Portrait { /// The full body image url associated with this **[`Student`]**. pub full_body_url: String,