Skip to content

Commit

Permalink
feat: Add item endpoint (#61)
Browse files Browse the repository at this point in the history
* introduce shop endpoint
* reorganize code structure
* introduce possibility to filter with pathfinder versions
* split raw query in sub methods
* introduce dice structure
* add total item and creature count for pagination
* fix ranged/melee bug
* update dependencies
* add safe guarantees around n of forged items range gen, now it is formally proved that it will not panic and isolated in a method to reduce redundancy
* allow upper and lower case alternative for encounter difficulty and pathfinder version
  • Loading branch information
RakuJa authored Jul 22, 2024
1 parent f27fed1 commit 2443ca4
Show file tree
Hide file tree
Showing 66 changed files with 2,681 additions and 710 deletions.
34 changes: 18 additions & 16 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "bybe"
version = "1.3.0"
version = "2.0.0"
authors = ["RakuJa"]

# Compiler info
Expand All @@ -21,31 +21,33 @@ build = "build/main.rs"
unsafe_code = "forbid"

[dependencies]
actix-web = "4.6.0"
actix-web = "4.8.0"
actix-cors = "0.7.0"
actix-web-validator = "5.0.1"
# Cannot be updated until actix-web updates validator dependency
validator = {version="0.16.1", features = ["derive"]}
validator = {version="0.18.1", features = ["derive"]}

utoipa = { version = "4.2.3", features = ["actix_extras"] }
utoipa-swagger-ui = { version = "7.0.1", features = ["actix-web"] }
utoipa = { version = "5.0.0-alpha.0", features = ["actix_extras"] }
utoipa-swagger-ui = { version = "7.1.1-alpha.0", features = ["actix-web"] }

sqlx = { version = "0.7.4", features = ["runtime-async-std", "sqlite"] }
cached = { version = "0.51.3", features = ["async"] }
cached = { version = "0.52.0", features = ["async"] }

anyhow = "1.0.86"
serde = { version = "1.0.202", features = ["derive"] }
serde_json = "1.0.117"
strum = {version="0.26.2", features = ["derive"]}
serde = { version = "1.0.204", features = ["derive"] }
serde_json = "1.0.120"
strum = {version="0.26.3", features = ["derive"]}
rand = "0.9.0-alpha.1"
counter = "0.5.7"
counter = "0.6.0"
ordered-float = { version = "4", features = ["serde"]}
num-traits = "0.2.19"
maplit = "1.0.2"

regex = "1.10.5"

dotenvy = "0.15.7"
regex = "1.10.4"

env_logger = "0.11.3"
log = "0.4.21"
maplit = "1.0.2"
num-traits = "0.2.19"
log = "0.4.22"
once_cell = "1.19.0"

[build-dependencies]
tokio = { version = "1", features = ["macros", "rt-multi-thread", "rt"] }
Expand Down
2 changes: 2 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ RUN apk add build-base

RUN apk add musl-dev

RUN apk add curl

# Build the project with optimizations
RUN cargo build --target x86_64-unknown-linux-musl --release

Expand Down
17 changes: 14 additions & 3 deletions build/creature_core_db_init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,25 @@ async fn create_temporary_table(conn: &Pool<Sqlite>) -> Result<()> {
ct.license,
ct.source,
ct.remaster,
CASE WHEN wt.creature_id IS NOT NULL AND UPPER(wt.wp_type)='MELEE' THEN TRUE ELSE FALSE END AS is_melee,
CASE WHEN wt.creature_id IS NOT NULL AND UPPER(wt.wp_type)='RANGED' THEN TRUE ELSE FALSE END AS is_ranged,
CASE WHEN ct.id IN (
SELECT creature_id FROM (
SELECT wcat.creature_id, base_item_id FROM ITEM_CREATURE_ASSOCIATION_TABLE wcat LEFT JOIN (
SELECT * FROM WEAPON_TABLE w1 WHERE UPPER(w1.weapon_type) = 'MELEE'
) wt ON base_item_id = wcat.item_id
)
) THEN TRUE ELSE FALSE END AS is_melee,
CASE WHEN ct.id IN (
SELECT creature_id FROM (
SELECT wcat.creature_id, base_item_id FROM ITEM_CREATURE_ASSOCIATION_TABLE wcat LEFT JOIN (
SELECT * FROM WEAPON_TABLE w1 WHERE UPPER(w1.weapon_type) = 'RANGED'
) wt ON base_item_id = wcat.item_id
)
) THEN TRUE ELSE FALSE END AS is_ranged,
CASE WHEN st.creature_id IS NOT NULL THEN TRUE ELSE FALSE END AS is_spell_caster,
CASE WHEN ct.aon_id IS NOT NULL THEN CONCAT('https://2e.aonprd.com/', CAST(UPPER(COALESCE(UPPER(ct.cr_type) , 'MONSTER')) AS TEXT), 's' , '.aspx?ID=', CAST(ct.aon_id AS TEXT)) ELSE NULL END AS archive_link,
COALESCE(ct.cr_type , 'Monster') AS cr_type,
COALESCE(ct.family , '-') AS family
FROM CREATURE_TABLE ct
LEFT JOIN WEAPON_TABLE wt ON ct.id = wt.creature_id
LEFT JOIN SPELL_TABLE st ON ct.id = st.creature_id
GROUP BY ct.id;
"
Expand Down
183 changes: 126 additions & 57 deletions src/db/proxy.rs → src/db/bestiary_proxy.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
use crate::models::creature::Creature;
use crate::models::creature::creature_struct::Creature;
use std::collections::{HashMap, HashSet};

use crate::db::data_providers::fetcher;
use crate::db::data_providers::fetcher::{
fetch_traits_associated_with_creatures, fetch_unique_values_of_field,
};
use crate::models::creature_component::creature_core::CreatureCoreData;
use crate::models::creature_component::fields_unique_values_struct::FieldsUniqueValuesStruct;
use crate::models::creature_fields_enum::CreatureField;
use crate::models::creature_filter_enum::CreatureFilter;
use crate::models::creature_metadata::alignment_enum::AlignmentEnum;
use crate::models::creature_metadata::type_enum::CreatureTypeEnum;
use crate::models::creature_metadata::variant_enum::CreatureVariant;
use crate::db::data_providers::creature_fetcher::fetch_traits_associated_with_creatures;
use crate::db::data_providers::{creature_fetcher, generic_fetcher};
use crate::models::bestiary_structs::{BestiaryPaginatedRequest, CreatureSortEnum};
use crate::models::creature::creature_component::creature_core::CreatureCoreData;
use crate::models::creature::creature_filter_enum::{CreatureFilter, FieldsUniqueValuesStruct};
use crate::models::creature::creature_metadata::alignment_enum::AlignmentEnum;
use crate::models::creature::creature_metadata::creature_role::CreatureRoleEnum;
use crate::models::creature::creature_metadata::type_enum::CreatureTypeEnum;
use crate::models::creature::creature_metadata::variant_enum::CreatureVariant;
use crate::models::pf_version_enum::PathfinderVersionEnum;
use crate::models::response_data::OptionalData;
use crate::models::routers_validator_structs::{FieldFilters, PaginatedRequest};
use crate::models::routers_validator_structs::{CreatureFieldFilters, OrderEnum};
use crate::AppState;
use anyhow::Result;
use cached::proc_macro::once;
Expand All @@ -25,7 +24,7 @@ pub async fn get_creature_by_id(
variant: &CreatureVariant,
optional_data: &OptionalData,
) -> Option<Creature> {
let creature = fetcher::fetch_creature_by_id(&app_state.conn, optional_data, id)
let creature = creature_fetcher::fetch_creature_by_id(&app_state.conn, optional_data, id)
.await
.ok()?;
Some(creature.convert_creature_to_variant(variant))
Expand All @@ -48,24 +47,74 @@ pub async fn get_elite_creature_by_id(

pub async fn get_paginated_creatures(
app_state: &AppState,
filters: &FieldFilters,
pagination: &PaginatedRequest,
filters: &CreatureFieldFilters,
pagination: &BestiaryPaginatedRequest,
) -> Result<(u32, Vec<Creature>)> {
let list = get_list(app_state, CreatureVariant::Base).await;

let filtered_list: Vec<Creature> = list
let mut filtered_list: Vec<Creature> = list
.into_iter()
.filter(|x| Creature::is_passing_filters(x, filters))
.collect();

let total_creature_count = filtered_list.len();

filtered_list.sort_by(|a, b| {
let cmp = match pagination
.bestiary_sort_data
.sort_by
.clone()
.unwrap_or_default()
{
CreatureSortEnum::Id => a.core_data.essential.id.cmp(&b.core_data.essential.id),
CreatureSortEnum::Name => a.core_data.essential.name.cmp(&b.core_data.essential.name),
CreatureSortEnum::Level => a
.core_data
.essential
.level
.cmp(&b.core_data.essential.level),
CreatureSortEnum::Trait => a
.core_data
.traits
.join(", ")
.cmp(&b.core_data.traits.join(", ")),
CreatureSortEnum::Size => a.core_data.essential.size.cmp(&b.core_data.essential.size),
CreatureSortEnum::Type => a
.core_data
.essential
.cr_type
.cmp(&b.core_data.essential.cr_type),
CreatureSortEnum::Hp => a.core_data.essential.hp.cmp(&b.core_data.essential.hp),
CreatureSortEnum::Rarity => a
.core_data
.essential
.rarity
.cmp(&b.core_data.essential.rarity),
CreatureSortEnum::Family => a
.core_data
.essential
.family
.cmp(&b.core_data.essential.family),
};
match pagination
.bestiary_sort_data
.order_by
.clone()
.unwrap_or_default()
{
OrderEnum::Ascending => cmp,
OrderEnum::Descending => cmp.reverse(),
}
});

let curr_slice: Vec<Creature> = filtered_list
.iter()
.skip(pagination.cursor as usize)
.take(pagination.page_size as usize)
.skip(pagination.paginated_request.cursor as usize)
.take(pagination.paginated_request.page_size as usize)
.cloned()
.collect();

Ok((curr_slice.len() as u32, curr_slice))
Ok((total_creature_count as u32, curr_slice))
}

pub async fn get_creatures_passing_all_filters(
Expand All @@ -82,7 +131,8 @@ pub async fn get_creatures_passing_all_filters(
let modified_filters =
prepare_filters_for_db_communication(key_value_filters, fetch_weak, fetch_elite);
for core in
fetcher::fetch_creatures_core_data_with_filters(&app_state.conn, &modified_filters).await?
creature_fetcher::fetch_creatures_core_data_with_filters(&app_state.conn, &modified_filters)
.await?
{
// We have fetched creature with level +1 if weak is allowed or level-1 if elite is allowed
// (or both). Now we catalogue correctly giving them the elite or weak variant, this does not
Expand All @@ -106,21 +156,27 @@ pub async fn get_creatures_passing_all_filters(
Ok(creature_vec)
}

pub async fn get_keys(app_state: &AppState, field: CreatureField) -> Vec<String> {
pub async fn get_all_possible_values_of_filter(
app_state: &AppState,
field: CreatureFilter,
) -> Vec<String> {
let runtime_fields_values = get_all_keys(app_state).await;
let mut x = match field {
CreatureField::Size => runtime_fields_values.list_of_sizes,
CreatureField::Rarity => runtime_fields_values.list_of_rarities,
CreatureField::Ranged => vec![true.to_string(), false.to_string()],
CreatureField::Melee => vec![true.to_string(), false.to_string()],
CreatureField::SpellCaster => vec![true.to_string(), false.to_string()],
CreatureField::Family => runtime_fields_values.list_of_families,
CreatureField::Traits => runtime_fields_values.list_of_traits,
CreatureField::Sources => runtime_fields_values.list_of_sources,
CreatureField::Alignment => AlignmentEnum::iter().map(|x| x.to_string()).collect(),
CreatureField::Level => runtime_fields_values.list_of_levels,
CreatureField::CreatureTypes => CreatureTypeEnum::iter().map(|x| x.to_string()).collect(),
_ => vec![],
CreatureFilter::Size => runtime_fields_values.list_of_sizes,
CreatureFilter::Rarity => runtime_fields_values.list_of_rarities,
CreatureFilter::Ranged => vec![true.to_string(), false.to_string()],
CreatureFilter::Melee => vec![true.to_string(), false.to_string()],
CreatureFilter::SpellCaster => vec![true.to_string(), false.to_string()],
CreatureFilter::Family => runtime_fields_values.list_of_families,
CreatureFilter::Traits => runtime_fields_values.list_of_traits,
CreatureFilter::Sources => runtime_fields_values.list_of_sources,
CreatureFilter::Alignment => AlignmentEnum::iter().map(|x| x.to_string()).collect(),
CreatureFilter::Level => runtime_fields_values.list_of_levels,
CreatureFilter::CreatureTypes => CreatureTypeEnum::iter().map(|x| x.to_string()).collect(),
CreatureFilter::CreatureRoles => CreatureRoleEnum::iter().map(|x| x.to_string()).collect(),
CreatureFilter::PathfinderVersion => PathfinderVersionEnum::iter()
.map(|x| x.to_string())
.collect(),
};
x.sort();
x
Expand All @@ -130,39 +186,52 @@ pub async fn get_keys(app_state: &AppState, field: CreatureField) -> Vec<String>
#[once(sync_writes = true)]
async fn get_all_keys(app_state: &AppState) -> FieldsUniqueValuesStruct {
FieldsUniqueValuesStruct {
list_of_levels: fetch_unique_values_of_field(&app_state.conn, "CREATURE_CORE", "level")
.await
.unwrap_or_default(),
list_of_families: fetch_unique_values_of_field(&app_state.conn, "CREATURE_CORE", "family")
.await
.unwrap(),
list_of_levels: generic_fetcher::fetch_unique_values_of_field(
&app_state.conn,
"CREATURE_CORE",
"level",
)
.await
.unwrap_or_default(),
list_of_families: generic_fetcher::fetch_unique_values_of_field(
&app_state.conn,
"CREATURE_CORE",
"family",
)
.await
.unwrap(),
list_of_traits: fetch_traits_associated_with_creatures(&app_state.conn)
.await
.unwrap_or_default(),
list_of_sources: fetch_unique_values_of_field(&app_state.conn, "CREATURE_CORE", "source")
.await
.unwrap_or_default(),
list_of_sizes: fetch_unique_values_of_field(&app_state.conn, "CREATURE_CORE", "size")
.await
.unwrap_or_default(),
list_of_rarities: fetch_unique_values_of_field(&app_state.conn, "CREATURE_CORE", "rarity")
.await
.unwrap_or_default(),
list_of_sources: generic_fetcher::fetch_unique_values_of_field(
&app_state.conn,
"CREATURE_CORE",
"source",
)
.await
.unwrap_or_default(),
list_of_sizes: generic_fetcher::fetch_unique_values_of_field(
&app_state.conn,
"CREATURE_CORE",
"size",
)
.await
.unwrap_or_default(),
list_of_rarities: generic_fetcher::fetch_unique_values_of_field(
&app_state.conn,
"CREATURE_CORE",
"rarity",
)
.await
.unwrap_or_default(),
}
}

/// Gets all the creature core data from the DB. It will not fetch data outside of variant and core.
/// It will cache the result.
#[once(sync_writes = true, result = true)]
async fn get_all_creatures_from_db(app_state: &AppState) -> Result<Vec<CreatureCoreData>> {
fetcher::fetch_creatures_core_data(
&app_state.conn,
&PaginatedRequest {
cursor: 0,
page_size: -1,
},
)
.await
creature_fetcher::fetch_creatures_core_data(&app_state.conn, 0, -1).await
}

/// Infallible method, it will expose a vector representing the values fetched from db or empty vec
Expand Down
Loading

0 comments on commit 2443ca4

Please sign in to comment.