From ccf6e5641a6999342a4369d722c5b2dfad87e19c Mon Sep 17 00:00:00 2001 From: rakuja Date: Sun, 26 May 2024 16:12:59 +0200 Subject: [PATCH] feat: * introduce random shop * split raw query in sub methods * introduce dice structure --- src/db/data_providers/raw_query_builder.rs | 65 +++++++++++++++++++--- src/db/data_providers/shop_fetcher.rs | 45 ++++++++++++++- src/db/shop_proxy.rs | 8 +++ src/models/encounter_structs.rs | 2 + src/models/item/item_metadata/type_enum.rs | 18 +++++- src/models/mod.rs | 2 +- src/models/pf_version_enum.rs | 14 +++++ src/models/routers_validator_structs.rs | 21 ++++++- src/models/shop_structs.rs | 37 +++++++++++- src/routes/shop.rs | 47 ++++++++++++++-- src/services/encounter_service.rs | 11 +--- src/services/shop_service.rs | 43 ++++++++++++++ 12 files changed, 285 insertions(+), 28 deletions(-) diff --git a/src/db/data_providers/raw_query_builder.rs b/src/db/data_providers/raw_query_builder.rs index 357eb12..6c3c08d 100644 --- a/src/db/data_providers/raw_query_builder.rs +++ b/src/db/data_providers/raw_query_builder.rs @@ -1,8 +1,38 @@ use crate::models::creature::creature_filter_enum::CreatureFilter; +use crate::models::item::item_metadata::type_enum::ItemTypeEnum; +use crate::models::shop_structs::ShopFilterQuery; use log::debug; use std::collections::{HashMap, HashSet}; const ACCURACY_THRESHOLD: i64 = 50; + +pub fn prepare_filtered_get_items(shop_filter_query: &ShopFilterQuery) -> String { + let n_of_equipment = shop_filter_query.n_of_equipment; + let n_of_consumables = shop_filter_query.n_of_consumables; + let supported_pf_versions = + HashSet::from_iter(shop_filter_query.pathfinder_version.to_db_value()); + let min_level = shop_filter_query.min_level as i64; + let max_level = shop_filter_query.max_level as i64; + let equipment_query = prepare_item_subquery( + &ItemTypeEnum::Equipment, + n_of_equipment, + min_level, + max_level, + &supported_pf_versions, + ); + let consumable_query = prepare_item_subquery( + &ItemTypeEnum::Consumable, + n_of_consumables, + min_level, + max_level, + &supported_pf_versions, + ); + let query = format!( + "SELECT * FROM ITEM_TABLE WHERE id IN ( {equipment_query} ) OR id IN ({consumable_query} )" + ); + debug!("{}", query); + query +} pub fn prepare_filtered_get_creatures_core( key_value_filters: &HashMap>, ) -> String { @@ -40,7 +70,7 @@ pub fn prepare_filtered_get_creatures_core( simple_core_query.push_str(" AND ") } simple_core_query - .push_str(prepare_bounded_check(value, ACCURACY_THRESHOLD, 100).as_str()) + .push_str(prepare_bounded_or_check(value, ACCURACY_THRESHOLD, 100).as_str()) } _ => (), } @@ -57,9 +87,9 @@ pub fn prepare_filtered_get_creatures_core( query } -/// Prepares a 'bounded AND statement' aka checks if all the columns are in the bound given -/// (brute_percentage >= 0 AND brute_percentage <= 0) AND (sniper_percentage >= 0 ...) ... -fn prepare_bounded_check( +/// Prepares a 'bounded OR statement' aka checks if all the columns are in the bound given +/// (brute_percentage >= 0 AND brute_percentage <= 0) OR (sniper_percentage >= 0 ...) ... +fn prepare_bounded_or_check( column_names: &HashSet, lower_bound: i64, upper_bound: i64, @@ -69,13 +99,17 @@ fn prepare_bounded_check( if !bounded_query.is_empty() { bounded_query.push_str(" OR "); } - bounded_query.push_str( - format!("({column} >= {lower_bound} AND {column} <= {upper_bound})").as_str(), - ); + bounded_query + .push_str(prepare_bounded_check(column.as_str(), lower_bound, upper_bound).as_str()); } bounded_query } +/// Prepares a 'bounded statement' aka (x>=lb AND x<=ub) +fn prepare_bounded_check(column: &str, lower_bound: i64, upper_bound: i64) -> String { + format!("({column} >= {lower_bound} AND {column} <= {upper_bound})") +} + /// Prepares a query that gets all the ids linked with a given list of traits, example /// SELECT tcat.creature_id /// FROM TRAIT_CREATURE_ASSOCIATION_TABLE tcat @@ -139,3 +173,20 @@ fn prepare_in_statement_for_generic_type( } result_string } +fn prepare_item_subquery( + item_type: &ItemTypeEnum, + n_of_item: i64, + min_level: i64, + max_level: i64, + supported_pf_version: &HashSet, +) -> String { + let item_type_query = prepare_get_id_matching_item_type_query(item_type); + let initial_statement = "SELECT id FROM ITEM_TABLE"; + let filter_by_version = prepare_in_statement_for_generic_type("remaster", supported_pf_version); + let filter_by_level = prepare_bounded_check(&String::from("level"), min_level, max_level); + format!("{initial_statement} WHERE {filter_by_level} AND {filter_by_version} AND id IN ( {item_type_query} ) ORDER BY RANDOM() LIMIT {n_of_item}") +} + +fn prepare_get_id_matching_item_type_query(item_type: &ItemTypeEnum) -> String { + format!("SELECT id FROM ITEM_TABLE WHERE UPPER(item_type) = UPPER('{item_type}')") +} diff --git a/src/db/data_providers/shop_fetcher.rs b/src/db/data_providers/shop_fetcher.rs index 5f68bb3..d569a0e 100644 --- a/src/db/data_providers/shop_fetcher.rs +++ b/src/db/data_providers/shop_fetcher.rs @@ -1,9 +1,14 @@ use crate::db::data_providers::generic_fetcher::MyString; +use crate::db::data_providers::raw_query_builder::prepare_filtered_get_items; use crate::models::db::raw_trait::RawTrait; +use crate::models::item::item_metadata::type_enum::ItemTypeEnum; use crate::models::item::item_struct::Item; use crate::models::routers_validator_structs::PaginatedRequest; +use crate::models::shop_structs::ShopFilterQuery; use anyhow::Result; -use sqlx::{Pool, Sqlite}; +use log::debug; +use rand::Rng; +use sqlx::{query_as, Pool, Sqlite}; pub async fn fetch_item_by_id(conn: &Pool, item_id: i64) -> Result { let mut item: Item = @@ -64,3 +69,41 @@ async fn update_items_with_traits(conn: &Pool, mut items: Vec) -> } items } + +pub async fn fetch_items_with_filters( + conn: &Pool, + filters: &ShopFilterQuery, +) -> Result> { + let result: Vec = query_as(prepare_filtered_get_items(filters).as_str()) + .fetch_all(conn) + .await?; + let equipment: Vec = result + .iter() + .filter(|x| x.item_type == ItemTypeEnum::Equipment) + .cloned() + .collect(); + let consumables: Vec = result + .iter() + .filter(|x| x.item_type == ItemTypeEnum::Consumable) + .cloned() + .collect(); + + if result.len() as i64 >= filters.n_of_consumables + filters.n_of_equipment { + debug!("Result vector is the correct size, no more operations needed"); + return Ok(result); + } + debug!("Result vector is not the correct size, duplicating random elements.."); + // We clone, otherwise we increment the probability of the same item being copied n times + let mut item_vec = result.clone(); + for _ in 0..(equipment.len() as i64 - filters.n_of_equipment) { + if let Some(x) = equipment.get(rand::thread_rng().gen_range(0..equipment.len())) { + item_vec.push(x.clone()); + } + } + for _ in 0..(consumables.len() as i64 - filters.n_of_consumables) { + if let Some(x) = consumables.get(rand::thread_rng().gen_range(0..consumables.len())) { + item_vec.push(x.clone()); + } + } + Ok(result) +} diff --git a/src/db/shop_proxy.rs b/src/db/shop_proxy.rs index cffe11a..31eb57b 100644 --- a/src/db/shop_proxy.rs +++ b/src/db/shop_proxy.rs @@ -3,6 +3,7 @@ use crate::models::creature::creature_metadata::type_enum::CreatureTypeEnum; use crate::models::item::item_fields_enum::{FieldsUniqueValuesStruct, ItemField}; use crate::models::item::item_struct::Item; use crate::models::routers_validator_structs::{ItemFieldFilters, PaginatedRequest}; +use crate::models::shop_structs::ShopFilterQuery; use crate::AppState; use anyhow::Result; use cached::proc_macro::once; @@ -14,6 +15,13 @@ pub async fn get_item_by_id(app_state: &AppState, id: i64) -> Option { .ok() } +pub async fn get_filtered_items( + app_state: &AppState, + filters: &ShopFilterQuery, +) -> Result> { + shop_fetcher::fetch_items_with_filters(&app_state.conn, filters).await +} + pub async fn get_paginated_items( app_state: &AppState, filters: &ItemFieldFilters, diff --git a/src/models/encounter_structs.rs b/src/models/encounter_structs.rs index 9a9e20d..009a1ad 100644 --- a/src/models/encounter_structs.rs +++ b/src/models/encounter_structs.rs @@ -30,7 +30,9 @@ pub struct RandomEncounterData { pub creature_types: Option>, pub creature_roles: Option>, pub challenge: Option, + #[validate(range(min = 1, max = 30))] pub min_creatures: Option, + #[validate(range(min = 1, max = 30))] pub max_creatures: Option, #[validate(length(min = 1))] pub party_levels: Vec, diff --git a/src/models/item/item_metadata/type_enum.rs b/src/models/item/item_metadata/type_enum.rs index ceacd45..c36a936 100644 --- a/src/models/item/item_metadata/type_enum.rs +++ b/src/models/item/item_metadata/type_enum.rs @@ -1,11 +1,12 @@ use serde::{Deserialize, Serialize}; use sqlx::Type; +use std::fmt::{Display, Formatter}; use std::str::FromStr; -use strum::{Display, EnumIter}; +use strum::EnumIter; use utoipa::ToSchema; #[derive( - Serialize, Deserialize, ToSchema, Display, Eq, Hash, PartialEq, Ord, PartialOrd, Type, EnumIter, + Serialize, Deserialize, ToSchema, Eq, Hash, PartialEq, Ord, PartialOrd, Type, EnumIter, )] pub enum ItemTypeEnum { #[serde(alias = "consumable", alias = "CONSUMABLE")] @@ -33,3 +34,16 @@ impl FromStr for ItemTypeEnum { } } } + +impl Display for ItemTypeEnum { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + ItemTypeEnum::Consumable => { + write!(f, "consumable") + } + ItemTypeEnum::Equipment => { + write!(f, "equipment") + } + } + } +} diff --git a/src/models/mod.rs b/src/models/mod.rs index df22a30..f4f5499 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -6,4 +6,4 @@ pub mod pf_version_enum; pub mod response_data; pub mod routers_validator_structs; pub mod scales_struct; -mod shop_structs; +pub mod shop_structs; diff --git a/src/models/pf_version_enum.rs b/src/models/pf_version_enum.rs index e1b2ea9..18bf843 100644 --- a/src/models/pf_version_enum.rs +++ b/src/models/pf_version_enum.rs @@ -11,3 +11,17 @@ pub enum PathfinderVersionEnum { #[default] Any, } + +impl PathfinderVersionEnum { + pub fn to_db_value(&self) -> Vec { + match self { + // The db column is a boolean called "remaster" so we translate the enum to + // FALSE if legacy, TRUE if remaster and TRUE, FALSE if both + PathfinderVersionEnum::Legacy => vec![String::from("FALSE")], + PathfinderVersionEnum::Remaster => vec![String::from("TRUE")], + PathfinderVersionEnum::Any => { + vec![String::from("TRUE"), String::from("FALSE")] + } + } + } +} diff --git a/src/models/routers_validator_structs.rs b/src/models/routers_validator_structs.rs index 40bd7fd..f066f4f 100644 --- a/src/models/routers_validator_structs.rs +++ b/src/models/routers_validator_structs.rs @@ -5,8 +5,9 @@ use crate::models::creature::creature_metadata::size_enum::SizeEnum; use crate::models::creature::creature_metadata::type_enum::CreatureTypeEnum; use crate::models::item::item_metadata::type_enum::ItemTypeEnum; use crate::models::pf_version_enum::PathfinderVersionEnum; +use rand::Rng; use serde::{Deserialize, Serialize}; -use utoipa::IntoParams; +use utoipa::{IntoParams, ToSchema}; use validator::Validate; #[derive(Serialize, Deserialize, IntoParams, Validate)] @@ -86,3 +87,21 @@ impl Default for PaginatedRequest { } } } + +#[derive(Serialize, Deserialize, ToSchema, Validate, Eq, PartialEq, Hash, Clone)] +pub struct Dice { + #[validate(range(min = 1, max = 255))] + pub n_of_dices: u8, + #[validate(range(min = 2, max = 255))] + pub dice_size: u8, +} + +impl Dice { + pub fn roll(&self) -> i64 { + let mut roll_result = 0; + for _ in 0..self.n_of_dices { + roll_result += rand::thread_rng().gen_range(1..=self.dice_size) as i64 + } + roll_result + } +} diff --git a/src/models/shop_structs.rs b/src/models/shop_structs.rs index 4c14df7..9d58672 100644 --- a/src/models/shop_structs.rs +++ b/src/models/shop_structs.rs @@ -1,6 +1,39 @@ +use crate::models::pf_version_enum::PathfinderVersionEnum; +use crate::models::routers_validator_structs::Dice; use serde::{Deserialize, Serialize}; +use strum::EnumIter; use utoipa::ToSchema; use validator::Validate; -#[derive(Serialize, Deserialize, ToSchema, Validate)] -pub struct RandomShopData {} +#[derive( + Serialize, Deserialize, ToSchema, Default, EnumIter, Eq, PartialEq, Hash, Ord, PartialOrd, Clone, +)] +pub enum ShopTypeEnum { + Blacksmith, + Alchemist, + #[default] + General, +} + +#[derive(Serialize, Deserialize, ToSchema, Validate, Clone)] +pub struct RandomShopData { + #[validate(range(max = 30))] + pub min_level: Option, + #[validate(range(max = 30))] + pub max_level: Option, + #[validate(length(min = 1))] + pub equipment_dices: Vec, + #[validate(length(min = 1))] + pub consumable_dices: Vec, + pub shop_type: Option, + pub pathfinder_version: Option, +} + +pub struct ShopFilterQuery { + //pub shop_type: ShopTypeEnum, + pub min_level: u8, + pub max_level: u8, + pub n_of_equipment: i64, + pub n_of_consumables: i64, + pub pathfinder_version: PathfinderVersionEnum, +} diff --git a/src/routes/shop.rs b/src/routes/shop.rs index 4ac99f2..a0c4b51 100644 --- a/src/routes/shop.rs +++ b/src/routes/shop.rs @@ -1,26 +1,39 @@ use crate::models::item::item_metadata::type_enum::ItemTypeEnum; +use crate::models::item::item_struct::Item; use crate::models::response_data::ResponseItem; +use crate::models::routers_validator_structs::Dice; use crate::models::routers_validator_structs::{ItemFieldFilters, PaginatedRequest}; +use crate::models::shop_structs::RandomShopData; +use crate::models::shop_structs::ShopTypeEnum; use crate::services::shop_service; use crate::services::shop_service::ShopListingResponse; use crate::AppState; use actix_web::web::Query; -use actix_web::{error, get, web, Responder}; +use actix_web::{error, get, post, web, Responder}; use utoipa::OpenApi; pub fn init_endpoints(cfg: &mut web::ServiceConfig) { cfg.service( web::scope("/shop") .service(get_item) - .service(get_shop_listing), + .service(get_shop_listing) + .service(get_random_shop_listing), ); } pub fn init_docs(doc: &mut utoipa::openapi::OpenApi) { #[derive(OpenApi)] #[openapi( - paths(get_shop_listing, get_item), - components(schemas(ResponseItem, ItemTypeEnum, ShopListingResponse)) + paths(get_shop_listing, get_item, get_random_shop_listing), + components(schemas( + ResponseItem, + ItemTypeEnum, + ShopListingResponse, + Item, + RandomShopData, + Dice, + ShopTypeEnum + )) )] struct ApiDoc; @@ -50,6 +63,32 @@ pub async fn get_shop_listing( )) } +#[utoipa::path( + post, + path = "/shop/generator", + tag = "shop", + request_body( + content = RandomShopData, + content_type = "application/json", + ), + params( + + ), + responses( + (status=200, description = "Successful Response", body = RandomShopData), + (status=400, description = "Bad request.") + ), +)] +#[post("/generator")] +pub async fn get_random_shop_listing( + data: web::Data, + web::Json(body): web::Json, +) -> actix_web::Result { + Ok(web::Json( + shop_service::generate_random_shop_listing(&data, &body).await, + )) +} + #[utoipa::path( get, path = "/shop/item/{item_id}", diff --git a/src/services/encounter_service.rs b/src/services/encounter_service.rs index 3a252ed..7769825 100644 --- a/src/services/encounter_service.rs +++ b/src/services/encounter_service.rs @@ -5,7 +5,6 @@ use crate::models::creature::creature_struct::Creature; use crate::models::encounter_structs::{ EncounterChallengeEnum, EncounterParams, RandomEncounterData, }; -use crate::models::pf_version_enum::PathfinderVersionEnum; use crate::models::response_data::ResponseCreature; use crate::services::encounter_handler::encounter_calculator; use crate::services::encounter_handler::encounter_calculator::calculate_encounter_scaling_difficulty; @@ -266,15 +265,7 @@ fn build_filter_map(filter_enum: FilterStruct) -> HashMap vec![String::from("FALSE")].into_iter(), - PathfinderVersionEnum::Remaster => vec![String::from("TRUE")].into_iter(), - PathfinderVersionEnum::Any => { - vec![String::from("TRUE"), String::from("FALSE")].into_iter() - } - }), + HashSet::from_iter(filter_enum.pathfinder_version.to_db_value()), ); filter_map } diff --git a/src/services/shop_service.rs b/src/services/shop_service.rs index 7f27e5e..7a7ccf7 100644 --- a/src/services/shop_service.rs +++ b/src/services/shop_service.rs @@ -3,6 +3,7 @@ use crate::models::item::item_fields_enum::ItemField; use crate::models::item::item_struct::Item; use crate::models::response_data::ResponseItem; use crate::models::routers_validator_structs::{ItemFieldFilters, PaginatedRequest}; +use crate::models::shop_structs::{RandomShopData, ShopFilterQuery}; use crate::services::url_calculator::shop_next_url_calculator; use crate::AppState; use serde::{Deserialize, Serialize}; @@ -35,6 +36,48 @@ pub async fn get_shop_listing( ) } +pub async fn generate_random_shop_listing( + app_state: &AppState, + shop_data: &RandomShopData, +) -> ShopListingResponse { + let min_level = shop_data.min_level.unwrap_or(0); + let max_level = shop_data.max_level.unwrap_or(30); + + let _shop_type = shop_data.shop_type.clone().unwrap_or_default(); + let n_of_consumables: i64 = shop_data.consumable_dices.iter().map(|x| x.roll()).sum(); + let n_of_equipment: i64 = shop_data.equipment_dices.iter().map(|x| x.roll()).sum(); + + let pathfinder_version = shop_data.pathfinder_version.clone().unwrap_or_default(); + + match shop_proxy::get_filtered_items( + app_state, + &ShopFilterQuery { + //shop_type, + min_level, + max_level, + n_of_equipment, + n_of_consumables, + pathfinder_version, + }, + ) + .await + { + Ok(result) => { + let n_of_items = result.len(); + ShopListingResponse { + results: Some(result.into_iter().map(ResponseItem::from).collect()), + count: n_of_items, + next: None, + } + } + Err(_) => ShopListingResponse { + results: None, + count: 0, + next: None, + }, + } +} + pub async fn get_traits_list(app_state: &AppState) -> Vec { shop_proxy::get_all_possible_values_of_filter(app_state, ItemField::Traits).await }