From 34188ec671bac83372afea498a64b0c0ff4d98f3 Mon Sep 17 00:00:00 2001 From: RakuJa Date: Thu, 16 Nov 2023 16:58:45 +0100 Subject: [PATCH 1/3] feat!: Implement SQLite db (#28) * refactor Dockerfile using alpine image; * introduce SQLite, remove redis; * introduce sqlx, without compile time guarantees on queries; * replace cached with mini-moka cache. More granular, cached structs and not function calls and returns; * remove unused parameters in router (sort, order) to avoid useless caching. Add cache for runtime fields values; * update README.md; * chore: order creature list by name by default. --- Cargo.toml | 5 +- Dockerfile | 29 +++- README.md | 29 ++-- src/db/db_cache.rs | 106 +++----------- src/db/db_communicator.rs | 175 ++++++++---------------- src/db/db_proxy.rs | 117 ++++------------ src/db/mod.rs | 2 +- src/main.rs | 49 ++++++- src/models/creature.rs | 1 + src/models/creature_metadata_enums.rs | 95 +++++++------ src/models/creature_sort_enums.rs | 24 ---- src/models/mod.rs | 1 - src/models/routers_validator_structs.rs | 9 -- src/routes/bestiary.rs | 77 +++++++---- src/routes/encounter.rs | 8 +- src/routes/health.rs | 15 +- src/services/bestiary_service.rs | 61 ++++----- src/services/encounter_service.rs | 11 +- src/services/url_calculator.rs | 15 +- 19 files changed, 350 insertions(+), 479 deletions(-) delete mode 100644 src/models/creature_sort_enums.rs diff --git a/Cargo.toml b/Cargo.toml index b80c5a4..62352c2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,13 +14,12 @@ validator = {version="0.16.1", features = ["derive"]} utoipa = { version = "4", features = ["actix_extras"] } utoipa-swagger-ui = { version = "4", features = ["actix-web"] } -redis = {version = "0.23.0-beta.1", features = ["json"]} -cached = "0.46.0" +sqlx = { version = "0.7.2", features = ["runtime-async-std", "sqlite"] } +mini-moka = "0.10.2" anyhow = "1.0.75" serde = { version = "1", features = ["derive"] } serde_json = "1" -regex = "1.10.2" strum = {version="0.25", features = ["derive"]} rand = "0.8.5" counter = "0.5.7" diff --git a/Dockerfile b/Dockerfile index 9c8cf1f..290fc7e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Stage 1: Build the Rust project -FROM rust:latest as builder +FROM rust:1.73-alpine as builder # Set the working directory in the container WORKDIR /app @@ -7,20 +7,37 @@ WORKDIR /app # Copy the project files into the container COPY . . +# Install all the required libraries +# GCC +RUN apk add build-base + +RUN apk add musl-dev +RUN cargo install cross + +# cross needs docker to work +RUN apk add --update docker openrc +RUN rc-update add docker boot + +# Static binary magic +#RUN rustup target add aarch64-unknown-linux-musl +#RUN rustup toolchain install stable-aarch64-unknown-linux-musl + # Build the project with optimizations -RUN cargo build --release +RUN cargo build --target x86_64-unknown-linux-musl --release # Stage 2: Create a minimal runtime image -FROM debian:bookworm-slim +FROM alpine:latest + +# Adding sqlite, cannot do it before +RUN apk add sqlite # Set the working directory in the container WORKDIR /app # Copy the built binary from the previous stage -COPY --from=builder /app/target/release/bybe . +COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/bybe . # Expose the port that your Actix-Web application will listen on EXPOSE 25566 - # Command to run your application when the container starts -CMD ["./bybe"] +ENTRYPOINT ["./bybe"] diff --git a/README.md b/README.md index af9881f..1862894 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ ![Rust](https://img.shields.io/badge/Rust-664666?style=for-the-badge&logo=rust&logoColor=red) ![Actix-web](https://img.shields.io/badge/actix-web?style=for-the-badge&logoColor=black&labelColor=pink&color=black ) -![Redis](https://img.shields.io/badge/redis-%23DD0031.svg?style=for-the-badge&logo=redis&logoColor=white) +![SQLite](https://img.shields.io/badge/sqlite-%2307405e.svg?style=for-the-badge&logo=sqlite&logoColor=white) [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT) @@ -23,13 +23,13 @@ Built using: - [Rust](https://www.rust-lang.org/tools/install) -- [Redis](https://redis.io/download/) +- [SQLite](https://www.sqlite.org/download.html) ## Installation guide - Local 1. Install [Rust](https://www.rust-lang.org/tools/install) on your machine. -2. Install [Redis](https://redis.io/download/) and populate the database. -3Clone this repository: +2. Populate the SQLite database. +3. Clone this repository: ``` git clone https://github.com/RakuJa/BYBE @@ -41,14 +41,14 @@ git clone https://github.com/RakuJa/BYBE ``` cargo build ``` - -5. Run the backend in development mode: +6. Set DATABASE_URL variable to SQLite db path +7. Run the backend in development mode: ``` cargo run ``` -6. To instead deploy the production build, run: +8. To instead deploy the production build, run: ``` cargo build --release @@ -60,23 +60,18 @@ cargo run ## Installation guide using Docker -1) Install Docker on your local machine -2) Download redis on your local machine: -``` -docker pull redis -``` -3) Clone the repository or download the ZIP +1. Install Docker on your local machine +2. Clone the repository or download the ZIP ``` git clone https://github.com/RakuJa/BYBE ``` -4) Go to the local BYBE project folder +3. Go to the local BYBE project folder -5) Build docker image of bybe using +4. Build docker image of bybe using ``` docker build -t bybe . ``` - -6) Run the image +5. Run the image ``` docker run -p 25566:25566 --name bybe-container bybe ``` diff --git a/src/db/db_cache.rs b/src/db/db_cache.rs index 83226c7..81ce106 100644 --- a/src/db/db_cache.rs +++ b/src/db/db_cache.rs @@ -1,36 +1,7 @@ use crate::models::creature::Creature; -use cached::proc_macro::cached; +use crate::AppState; -#[derive(Default, Hash, Eq, PartialEq, Clone)] -pub struct SortedVectorsByField { - pub unordered_creatures: Vec, - - pub order_by_id_ascending: Vec, - pub order_by_id_descending: Vec, - - pub order_by_name_ascending: Vec, - pub order_by_name_descending: Vec, - - pub order_by_hp_ascending: Vec, - pub order_by_hp_descending: Vec, - - pub order_by_level_ascending: Vec, - pub order_by_level_descending: Vec, - - pub order_by_family_ascending: Vec, - pub order_by_family_descending: Vec, - - pub order_by_alignment_ascending: Vec, - pub order_by_alignment_descending: Vec, - - pub order_by_size_ascending: Vec, - pub order_by_size_descending: Vec, - - pub order_by_rarity_ascending: Vec, - pub order_by_rarity_descending: Vec, -} - -#[derive(Default, Hash, Eq, PartialEq, Clone)] +#[derive(Default, Eq, PartialEq, Clone)] pub struct RuntimeFieldsValues { pub list_of_ids: Vec, pub list_of_levels: Vec, @@ -42,10 +13,24 @@ pub struct RuntimeFieldsValues { pub list_of_creature_types: Vec, } -#[cached(time = 604800, sync_writes = true)] -pub fn from_db_data_to_filter_cache(data: Vec) -> RuntimeFieldsValues { +pub fn from_db_data_to_filter_cache( + app_state: &AppState, + data: Vec, +) -> RuntimeFieldsValues { let mut fields_values_cache = RuntimeFieldsValues::default(); - // The right structure would be an hashset, but it does not implement hash.. + let cache = &app_state.runtime_fields_cache.clone(); + if let Some(runtime_fields) = cache.get(&0) { + return RuntimeFieldsValues { + list_of_ids: runtime_fields.list_of_ids.clone(), + list_of_levels: runtime_fields.list_of_levels.clone(), + list_of_families: runtime_fields.list_of_families.clone(), + list_of_traits: runtime_fields.list_of_traits.clone(), + list_of_alignments: runtime_fields.list_of_alignments.clone(), + list_of_sizes: runtime_fields.list_of_sizes.clone(), + list_of_rarities: runtime_fields.list_of_rarities.clone(), + list_of_creature_types: runtime_fields.list_of_creature_types.clone(), + }; + } for curr_creature in data { let id = curr_creature.id.to_string(); let lvl = curr_creature.level.to_string(); @@ -95,56 +80,7 @@ pub fn from_db_data_to_filter_cache(data: Vec) -> RuntimeFieldsValues .push(creature_type); } } - fields_values_cache -} - -#[cached(time = 604800, sync_writes = true)] -pub fn from_db_data_to_sorted_vectors(unordered_creatures: Vec) -> SortedVectorsByField { - let mut sorted_cache = SortedVectorsByField::default(); - - let mut sort_stage = unordered_creatures.clone(); - - sorted_cache.unordered_creatures = unordered_creatures.clone(); - - sort_stage.sort_by_key(|cr| cr.id); - sorted_cache.order_by_id_ascending = sort_stage.clone(); - sort_stage.reverse(); - sorted_cache.order_by_id_descending = sort_stage.clone(); - - sort_stage.sort_by_key(|cr| cr.name.clone()); - sorted_cache.order_by_name_ascending = sort_stage.clone(); - sort_stage.reverse(); - sorted_cache.order_by_name_descending = sort_stage.clone(); - - sort_stage.sort_by_key(|cr| cr.hp); - sorted_cache.order_by_hp_ascending = sort_stage.clone(); - sort_stage.reverse(); - sorted_cache.order_by_hp_descending = sort_stage.clone(); + cache.insert(0, fields_values_cache.clone()); - sort_stage.sort_by_key(|cr| cr.level); - sorted_cache.order_by_level_ascending = sort_stage.clone(); - sort_stage.reverse(); - sorted_cache.order_by_level_descending = sort_stage.clone(); - - sort_stage.sort_by_key(|cr| cr.family.clone()); - sorted_cache.order_by_family_ascending = sort_stage.clone(); - sort_stage.reverse(); - sorted_cache.order_by_family_descending = sort_stage.clone(); - - sort_stage.sort_by_key(|cr| cr.alignment.clone()); - sorted_cache.order_by_alignment_ascending = sort_stage.clone(); - sort_stage.reverse(); - sorted_cache.order_by_alignment_descending = sort_stage.clone(); - - sort_stage.sort_by_key(|cr| cr.size.clone()); - sorted_cache.order_by_size_ascending = sort_stage.clone(); - sort_stage.reverse(); - sorted_cache.order_by_size_descending = sort_stage.clone(); - - sort_stage.sort_by_key(|cr| cr.rarity.clone()); - sorted_cache.order_by_rarity_ascending = sort_stage.clone(); - sort_stage.reverse(); - sorted_cache.order_by_rarity_descending = sort_stage.clone(); - - sorted_cache + fields_values_cache } diff --git a/src/db/db_communicator.rs b/src/db/db_communicator.rs index ebd318a..365a7ea 100644 --- a/src/db/db_communicator.rs +++ b/src/db/db_communicator.rs @@ -1,156 +1,99 @@ use crate::models::creature::Creature; use crate::models::creature_metadata_enums::{ - creature_type_to_storage_string, AlignmentEnum, CreatureTypeEnum, RarityEnum, SizeEnum, + AlignmentEnum, CreatureTypeEnum, RarityEnum, SizeEnum, }; use crate::services::url_calculator::generate_archive_link; use log::warn; -use redis::{ - from_redis_value, Commands, Connection, ConnectionLike, FromRedisValue, JsonCommands, - RedisError, RedisResult, Value, -}; -use regex::Regex; use serde::{Deserialize, Serialize}; -use std::env; +use sqlx::{Error, FromRow, Pool, Sqlite}; +use std::str::FromStr; -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, FromRow)] pub struct RawCreature { + id: i32, + aon_id: i32, name: String, hp: i16, level: i8, - alignment: AlignmentEnum, - size: SizeEnum, + alignment: String, + size: String, family: Option, - rarity: RarityEnum, + rarity: String, is_melee: i8, is_ranged: i8, is_spell_caster: i8, - sources: String, - traits: String, - creature_type: CreatureTypeEnum, + creature_type: String, } -impl FromRedisValue for RawCreature { - fn from_redis_value(v: &Value) -> RedisResult { - let json_str: String = from_redis_value(v)?; - let vec_of_raw_strings: serde_json::error::Result> = - serde_json::from_str(&json_str); - match vec_of_raw_strings { - Ok(mut raw) => { - // raw is a vec of one element, we can pop it and forget - let x: serde_json::error::Result = - serde_json::from_str(&(raw.pop().unwrap_or(String::from("")))); - x - } - Err(err) => Err(err), - } - .map_err(redis::RedisError::from) - } +#[derive(Serialize, Deserialize, FromRow)] +pub struct CreatureTrait { + name: String, } -fn from_raw_vec_to_creature(raw_vec: Vec, id_vec: Vec) -> Vec { - raw_vec - .iter() - .zip(id_vec.iter()) - .map(|(raw, identifier)| from_raw_to_creature(raw, identifier)) - .collect() +#[derive(Serialize, Deserialize, FromRow)] +pub struct CreatureSource { + name: String, } -fn extract_vec_from_raw_string(raw_vector: &str) -> Vec { - // Extracts a vec of string from a json string representing a vector - // ex "['hi', 'man']" => ['hi', 'man'] - // complex string ex "oneword's secondword" are stored in double quotes - // simple strings in '' - let double_quotes_re = Regex::new("\"([^\"]+)\"").unwrap(); - let single_quotes_re = Regex::new(r"'([^']+)'").unwrap(); - - let resulting_vector: Vec = double_quotes_re - .captures_iter(raw_vector) - .filter(|capture| capture.len() >= 2) - .map(|capture| capture[1].to_string()) - .collect(); - if !resulting_vector.is_empty() { - resulting_vector - } else { - let resulting_vector: Vec = single_quotes_re - .captures_iter(raw_vector) - .map(|capture| capture[1].to_string()) - .collect(); - resulting_vector +async fn from_raw_vec_to_creature(conn: &Pool, raw_vec: Vec) -> Vec { + let mut creature_list = Vec::new(); + for el in raw_vec { + creature_list.push(from_raw_to_creature(conn, &el).await); } + creature_list } -fn from_raw_to_creature(raw: &RawCreature, identifier: &str) -> Creature { - let creature_type = raw.creature_type.clone(); - let key_prefix_string = &format!("{}{}", creature_type_to_storage_string(&creature_type), ":"); - let id = identifier - .strip_prefix(key_prefix_string) - .map(|s| s.parse::().unwrap_or(0)) - .unwrap_or(0); - let sources_list = extract_vec_from_raw_string(&raw.sources); - let traits_list = extract_vec_from_raw_string(&raw.traits); - let archive_link = generate_archive_link(id, &creature_type); +async fn from_raw_to_creature(conn: &Pool, raw: &RawCreature) -> Creature { + let creature_type = CreatureTypeEnum::from_str(raw.creature_type.as_str()).unwrap_or_default(); + let alignment_enum = AlignmentEnum::from_str(raw.alignment.as_str()).unwrap_or_default(); + let size_enum = SizeEnum::from_str(raw.size.as_str()).unwrap_or_default(); + let rarity_enum = RarityEnum::from_str(raw.rarity.as_str()).unwrap_or_default(); + let archive_link = generate_archive_link(raw.aon_id, &creature_type); + + let sources = sqlx::query_as::<_, CreatureSource>( + &format!("SELECT * FROM source_table INTERSECT SELECT source_id FROM SOURCE_ASSOCIATION_TABLE WHERE creature_id == {}", raw.id) + ).fetch_all(conn).await.unwrap_or_default(); + + let traits = sqlx::query_as::<_, CreatureTrait>( + &format!("SELECT * FROM trait_table INTERSECT SELECT trait_id FROM TRAIT_ASSOCIATION_TABLE WHERE creature_id == {}", raw.id) + ).fetch_all(conn).await.unwrap_or_default(); Creature { - id, + id: raw.id, + aon_id: raw.aon_id, name: raw.name.clone(), hp: raw.hp, level: raw.level, - alignment: raw.alignment.clone(), - size: raw.size.clone(), + alignment: alignment_enum, + size: size_enum, family: raw.family.clone(), - rarity: raw.rarity.clone(), + rarity: rarity_enum, is_melee: raw.is_melee != 0, is_ranged: raw.is_ranged != 0, is_spell_caster: raw.is_spell_caster != 0, - sources: sources_list, - traits: traits_list, + sources: sources + .into_iter() + .map(|curr_source| curr_source.name) + .collect(), + traits: traits + .into_iter() + .map(|curr_trait| curr_trait.name) + .collect(), creature_type, archive_link, } } -fn get_redis_url() -> String { - let redis_password = env::var("REDIS_KEY").unwrap_or_else(|_| "".to_string()); - - let redis_ip = env::var("REDIS_IP").unwrap_or_else(|_| "localhost".to_string()); - let redis_port = env::var("REDIS_PORT").unwrap_or_else(|_| "6379".to_string()); - - format!("redis://:{}@{}:{}", redis_password, redis_ip, redis_port) -} - -fn get_connection() -> RedisResult { - let client = redis::Client::open(get_redis_url())?; - let conn = client.get_connection()?; - Ok(conn) -} - -pub fn get_creatures_by_ids(ids: Vec) -> Result, RedisError> { - let mut conn = get_connection()?; - let raw_results: RedisResult> = conn.json_get(ids.clone(), "$"); - // Convert each RawJsonString to RawCreature and collect the results into a Vec - let mut raw_creatures = Vec::new(); - match raw_results { - Ok(creature_list) => { - for raw in creature_list { - raw_creatures.push(raw); - } +pub async fn fetch_creatures(conn: &Pool) -> Result, Error> { + let creatures = sqlx::query_as::<_, RawCreature>( + "SELECT * FROM CREATURE_TABLE ORDER BY name" + ) + .fetch_all(conn) + .await; + match creatures { + Ok(creature_list) => Ok(from_raw_vec_to_creature(conn, creature_list).await), + Err(err) => { + warn!("Error converting data from db {}", err); + Err(err) } - Err(err) => warn!("Error converting data from db {}", err), - } - - Ok(from_raw_vec_to_creature(raw_creatures, ids)) -} - -pub fn fetch_and_parse_all_keys(pattern: &str) -> Result, RedisError> { - let mut conn = get_connection()?; - let mut parse_pattern = pattern.to_owned(); - if !pattern.ends_with('*') { - parse_pattern.push('*') } - - let keys: Vec = conn.scan_match(parse_pattern)?.collect(); - Ok(keys) -} - -pub fn is_redis_up() -> RedisResult { - Ok(get_connection()?.is_open()) } diff --git a/src/db/db_proxy.rs b/src/db/db_proxy.rs index a5e8ba4..2a9a54d 100644 --- a/src/db/db_proxy.rs +++ b/src/db/db_proxy.rs @@ -2,25 +2,24 @@ use crate::db::db_communicator; use crate::models::creature::{check_creature_pass_filters, Creature}; use std::collections::{HashMap, HashSet}; -use crate::db::db_cache::{from_db_data_to_filter_cache, from_db_data_to_sorted_vectors}; +use crate::db::db_cache::from_db_data_to_filter_cache; use crate::models::creature_fields_enum::CreatureField; use crate::models::creature_filter_enum::CreatureFilter; -use crate::models::creature_sort_enums::{OrderEnum, SortEnum}; -use crate::models::routers_validator_structs::{FieldFilters, PaginatedRequest, SortData}; +use crate::models::routers_validator_structs::{FieldFilters, PaginatedRequest}; +use crate::AppState; use anyhow::Result; -use cached::proc_macro::cached; -pub fn get_creature_by_id(id: i32) -> Option { - let list = get_list(None, None); +pub async fn get_creature_by_id(app_state: &AppState, id: i32) -> Option { + let list = get_list(app_state).await; list.iter().find(|creature| creature.id == id).cloned() } -pub fn get_paginated_creatures( - sort: &SortData, +pub async fn get_paginated_creatures( + app_state: &AppState, filters: &FieldFilters, pagination: &PaginatedRequest, ) -> Result<(u32, Vec)> { - let list = get_list(sort.sort_key, sort.order_by); + let list = get_list(app_state).await; let filtered_list: Vec = list .into_iter() @@ -37,10 +36,11 @@ pub fn get_paginated_creatures( Ok((curr_slice.len() as u32, curr_slice)) } -pub fn fetch_creatures_passing_all_filters( +pub async fn fetch_creatures_passing_all_filters( + app_state: &AppState, key_value_filters: HashMap>, ) -> Result> { - let creature_list = get_list(None, None); + let creature_list = get_list(app_state).await; let mut intersection = HashSet::from_iter(creature_list.iter().cloned()); key_value_filters .iter() @@ -104,10 +104,9 @@ fn fetch_creatures_passing_single_filter( } } -#[cached(time = 604800, sync_writes = true)] -pub fn get_keys(field: CreatureField) -> Vec { - if let Some(db_data) = fetch_data_from_database() { - let runtime_fields_values = from_db_data_to_filter_cache(db_data); +pub async fn get_keys(app_state: &AppState, field: CreatureField) -> Vec { + if let Some(db_data) = fetch_data_from_database(app_state).await { + let runtime_fields_values = from_db_data_to_filter_cache(app_state, db_data); let mut x = match field { CreatureField::Id => runtime_fields_values.list_of_ids, CreatureField::Size => runtime_fields_values.list_of_sizes, @@ -129,89 +128,27 @@ pub fn get_keys(field: CreatureField) -> Vec { vec![] } -#[cached(time = 604800, sync_writes = true)] -fn fetch_data_from_database() -> Option> { - if let Some(monster_vec) = fetch_creatures("monster:") { - if let Some(mut npc_vec) = fetch_creatures("npc:") { - let mut creature_vec = monster_vec; - creature_vec.append(&mut npc_vec); - return Some(creature_vec); - } +async fn fetch_data_from_database(app_state: &AppState) -> Option> { + if let Some(creature_vec) = fetch_creatures(app_state).await { + return Some(creature_vec); } None } -fn fetch_creatures(pattern: &str) -> Option> { - if let Ok(keys) = db_communicator::fetch_and_parse_all_keys(pattern) { - if let Ok(creatures) = db_communicator::get_creatures_by_ids(keys) { - return Some(creatures); - } +async fn fetch_creatures(app_state: &AppState) -> Option> { + let cache = &app_state.creature_cache.clone(); + if let Some(creatures) = cache.get(&0) { + return Some(creatures); + } else if let Ok(creatures) = db_communicator::fetch_creatures(&app_state.conn).await { + cache.insert(0, creatures.clone()); + return Some(creatures); } None } -#[cached(time = 604800, sync_writes = true)] -fn get_list(sort_field: Option, order_by: Option) -> Vec { - if let Some(db_data) = fetch_data_from_database() { - let sorted_vec_cache = from_db_data_to_sorted_vectors(db_data); - let x = match (sort_field.unwrap_or_default(), order_by.unwrap_or_default()) { - (SortEnum::Id, OrderEnum::Ascending) => { - sorted_vec_cache.order_by_id_ascending.to_owned() - } - (SortEnum::Id, OrderEnum::Descending) => { - sorted_vec_cache.order_by_id_descending.to_owned() - } - - (SortEnum::Hp, OrderEnum::Ascending) => { - sorted_vec_cache.order_by_hp_ascending.to_owned() - } - (SortEnum::Hp, OrderEnum::Descending) => { - sorted_vec_cache.order_by_hp_descending.to_owned() - } - - (SortEnum::Family, OrderEnum::Ascending) => { - sorted_vec_cache.order_by_family_ascending.to_owned() - } - (SortEnum::Family, OrderEnum::Descending) => { - sorted_vec_cache.order_by_family_descending.to_owned() - } - - (SortEnum::Alignment, OrderEnum::Ascending) => { - sorted_vec_cache.order_by_alignment_ascending.to_owned() - } - (SortEnum::Alignment, OrderEnum::Descending) => { - sorted_vec_cache.order_by_alignment_descending.to_owned() - } - - (SortEnum::Level, OrderEnum::Ascending) => { - sorted_vec_cache.order_by_level_ascending.to_owned() - } - (SortEnum::Level, OrderEnum::Descending) => { - sorted_vec_cache.order_by_level_descending.to_owned() - } - - (SortEnum::Name, OrderEnum::Ascending) => { - sorted_vec_cache.order_by_name_ascending.to_owned() - } - (SortEnum::Name, OrderEnum::Descending) => { - sorted_vec_cache.order_by_name_descending.to_owned() - } - - (SortEnum::Rarity, OrderEnum::Ascending) => { - sorted_vec_cache.order_by_rarity_ascending.to_owned() - } - (SortEnum::Rarity, OrderEnum::Descending) => { - sorted_vec_cache.order_by_rarity_descending.to_owned() - } - - (SortEnum::Size, OrderEnum::Ascending) => { - sorted_vec_cache.order_by_size_ascending.to_owned() - } - (SortEnum::Size, OrderEnum::Descending) => { - sorted_vec_cache.order_by_rarity_descending.to_owned() - } - }; - return x; +async fn get_list(app_state: &AppState) -> Vec { + if let Some(db_data) = fetch_data_from_database(app_state).await { + return db_data; } vec![] } diff --git a/src/db/mod.rs b/src/db/mod.rs index a529e2f..6b48a43 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -1,3 +1,3 @@ -mod db_cache; +pub mod db_cache; pub mod db_communicator; pub mod db_proxy; diff --git a/src/main.rs b/src/main.rs index 722283f..8f962d3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,11 +4,16 @@ extern crate lazy_static; mod routes; +use crate::db::db_cache::RuntimeFieldsValues; +use crate::models::creature::Creature; use crate::routes::{bestiary, encounter, health}; use actix_cors::Cors; use actix_web::http::header::{CacheControl, CacheDirective}; -use actix_web::{get, middleware, App, HttpResponse, HttpServer, Responder}; +use actix_web::{get, middleware, web, App, HttpResponse, HttpServer, Responder}; +use mini_moka::sync::Cache; +use sqlx::{sqlite::SqlitePoolOptions, Pool, Sqlite}; use std::env; +use std::time::Duration; use utoipa::OpenApi; use utoipa_swagger_ui::SwaggerUi; @@ -16,12 +21,23 @@ mod db; mod models; mod services; +#[derive(Clone)] +pub struct AppState { + conn: Pool, + creature_cache: Cache>, + runtime_fields_cache: Cache, +} + #[utoipa::path(get, path = "/")] #[get("/")] async fn index() -> impl Responder { HttpResponse::Ok().body("Hello, world!") } +fn get_service_db_url() -> String { + env::var("DATABASE_URL").expect("Error fetching database URL") +} + fn get_service_ip() -> String { env::var("SERVICE_IP").unwrap_or_else(|_| "0.0.0.0".to_string()) } @@ -42,8 +58,34 @@ fn init_docs(openapi: &mut utoipa::openapi::OpenApi) { #[actix_web::main] async fn main() -> std::io::Result<()> { env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); + // get env vars + let db_url = get_service_db_url(); let service_ip = get_service_ip(); let service_port = get_service_port(); + + // establish connection to database + let pool = SqlitePoolOptions::new() + .max_connections(5) + .connect(&db_url) + .await + .expect("Error building connection pool"); + + let cr_cache = Cache::builder() + // Time to live (TTL): 1 week + .time_to_live(Duration::from_secs(604800)) + // Time to idle (TTI): 1 week + // .time_to_idle(Duration::from_secs( 5 * 60)) + // Create the cache. + .build(); + + let fields_cache = Cache::builder() + // Time to live (TTL): 1 week + .time_to_live(Duration::from_secs(604800)) + // Time to idle (TTI): 1 week + // .time_to_idle(Duration::from_secs( 5 * 60)) + // Create the cache. + .build(); + log::info!( "starting HTTP server at http://{}:{}", service_ip.as_str(), @@ -80,6 +122,11 @@ async fn main() -> std::io::Result<()> { .service( SwaggerUi::new("/swagger-ui/{_:.*}").url("/api-docs/openapi.json", openapi.clone()), ) + .app_data(web::Data::new(AppState { + conn: pool.clone(), + creature_cache: cr_cache.clone(), + runtime_fields_cache: fields_cache.clone(), + })) }) .bind((get_service_ip(), get_service_port()))? .run() diff --git a/src/models/creature.rs b/src/models/creature.rs index 318cefc..d470dfd 100644 --- a/src/models/creature.rs +++ b/src/models/creature.rs @@ -8,6 +8,7 @@ use utoipa::ToSchema; #[derive(Serialize, Deserialize, Clone, ToSchema, Eq, Hash, PartialEq)] pub struct Creature { pub id: i32, + pub aon_id: i32, pub name: String, pub hp: i16, pub level: i8, diff --git a/src/models/creature_metadata_enums.rs b/src/models/creature_metadata_enums.rs index 9ae8f48..a55aa0a 100644 --- a/src/models/creature_metadata_enums.rs +++ b/src/models/creature_metadata_enums.rs @@ -1,4 +1,5 @@ use serde::{Deserialize, Serialize}; +use std::str::FromStr; use strum::{Display, EnumString}; use utoipa::ToSchema; @@ -64,17 +65,7 @@ pub enum AlignmentEnum { } #[derive( - Serialize, - Deserialize, - ToSchema, - Display, - Eq, - Hash, - PartialEq, - Ord, - PartialOrd, - Default, - EnumString, + Serialize, Deserialize, ToSchema, Display, Eq, Hash, PartialEq, Ord, PartialOrd, Default, )] pub enum RarityEnum { #[default] @@ -89,17 +80,7 @@ pub enum RarityEnum { } #[derive( - Serialize, - Deserialize, - ToSchema, - Display, - Eq, - Hash, - PartialEq, - Ord, - PartialOrd, - Default, - EnumString, + Serialize, Deserialize, ToSchema, Display, Eq, Hash, PartialEq, Ord, PartialOrd, Default, )] pub enum SizeEnum { #[serde(alias = "tiny", alias = "TINY")] @@ -117,17 +98,7 @@ pub enum SizeEnum { Gargantuan, } #[derive( - Serialize, - Deserialize, - ToSchema, - Display, - Eq, - Hash, - PartialEq, - Ord, - PartialOrd, - Default, - EnumString, + Serialize, Deserialize, ToSchema, Display, Eq, Hash, PartialEq, Ord, PartialOrd, Default, )] pub enum CreatureTypeEnum { #[default] @@ -141,6 +112,19 @@ pub enum CreatureTypeEnum { Npc, } +impl FromStr for CreatureTypeEnum { + type Err = (); + fn from_str(s: &str) -> Result { + match s { + "Monster" => Ok(CreatureTypeEnum::Monster), + "NPC" => Ok(CreatureTypeEnum::Npc), + "Npc" => Ok(CreatureTypeEnum::Npc), + "npc" => Ok(CreatureTypeEnum::Npc), + _ => Ok(CreatureTypeEnum::Monster), + } + } +} + impl Clone for AlignmentEnum { fn clone(&self) -> AlignmentEnum { match self { @@ -159,6 +143,23 @@ impl Clone for AlignmentEnum { } } +impl FromStr for RarityEnum { + type Err = (); + fn from_str(s: &str) -> Result { + match s { + "COMMON" => Ok(RarityEnum::Common), + "common" => Ok(RarityEnum::Common), + "UNCOMMON" => Ok(RarityEnum::Uncommon), + "uncommon" => Ok(RarityEnum::Uncommon), + "RARE" => Ok(RarityEnum::Rare), + "rare" => Ok(RarityEnum::Rare), + "UNIQUE" => Ok(RarityEnum::Unique), + "unique" => Ok(RarityEnum::Unique), + _ => Err(()), + } + } +} + impl Clone for RarityEnum { fn clone(&self) -> RarityEnum { match self { @@ -183,6 +184,27 @@ impl Clone for SizeEnum { } } +impl FromStr for SizeEnum { + type Err = (); + fn from_str(s: &str) -> Result { + match s { + "TINY" => Ok(SizeEnum::Tiny), + "tiny" => Ok(SizeEnum::Tiny), + "SMALL" => Ok(SizeEnum::Small), + "small" => Ok(SizeEnum::Small), + "MEDIUM" => Ok(SizeEnum::Medium), + "medium" => Ok(SizeEnum::Medium), + "LARGE" => Ok(SizeEnum::Large), + "large" => Ok(SizeEnum::Large), + "HUGE" => Ok(SizeEnum::Huge), + "huge" => Ok(SizeEnum::Huge), + "GARGANTUAN" => Ok(SizeEnum::Gargantuan), + "gargantuan" => Ok(SizeEnum::Gargantuan), + _ => Err(()), + } + } +} + impl Clone for CreatureTypeEnum { fn clone(&self) -> CreatureTypeEnum { match self { @@ -198,10 +220,3 @@ pub fn creature_type_to_url_string(creature_type: &CreatureTypeEnum) -> &str { CreatureTypeEnum::Npc => "NPCs", } } - -pub fn creature_type_to_storage_string(creature_type: &CreatureTypeEnum) -> &str { - match creature_type { - CreatureTypeEnum::Monster => "monster", - CreatureTypeEnum::Npc => "npc", - } -} diff --git a/src/models/creature_sort_enums.rs b/src/models/creature_sort_enums.rs deleted file mode 100644 index 1a5cd08..0000000 --- a/src/models/creature_sort_enums.rs +++ /dev/null @@ -1,24 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use strum::Display; -use utoipa::ToSchema; - -#[derive(Serialize, Deserialize, ToSchema, Display, Copy, Clone, Default, Hash, Eq, PartialEq)] -pub enum SortEnum { - #[default] - Id, - Name, - Hp, - Level, - Family, - Alignment, - Size, - Rarity, -} - -#[derive(Serialize, Deserialize, ToSchema, Display, Copy, Clone, Default, Hash, Eq, PartialEq)] -pub enum OrderEnum { - #[default] - Ascending, - Descending, -} diff --git a/src/models/mod.rs b/src/models/mod.rs index 04851d2..5771a26 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -2,7 +2,6 @@ pub mod creature; pub mod creature_fields_enum; pub mod creature_filter_enum; pub mod creature_metadata_enums; -pub mod creature_sort_enums; pub mod encounter_structs; pub mod party_member; pub mod routers_validator_structs; diff --git a/src/models/routers_validator_structs.rs b/src/models/routers_validator_structs.rs index 23eacad..43e482d 100644 --- a/src/models/routers_validator_structs.rs +++ b/src/models/routers_validator_structs.rs @@ -1,5 +1,4 @@ use crate::models::creature_metadata_enums::{AlignmentEnum, RarityEnum, SizeEnum}; -use crate::models::creature_sort_enums::{OrderEnum, SortEnum}; use serde::{Deserialize, Serialize}; use utoipa::IntoParams; use validator::Validate; @@ -20,14 +19,6 @@ pub struct FieldFilters { pub is_spell_caster_filter: Option, } -#[derive(Serialize, Deserialize, IntoParams, Validate, Copy, Clone)] -pub struct SortData { - #[param(inline)] - pub sort_key: Option, - #[param(inline)] - pub order_by: Option, -} - #[derive(Serialize, Deserialize, IntoParams, Validate)] pub struct PaginatedRequest { pub cursor: u32, diff --git a/src/routes/bestiary.rs b/src/routes/bestiary.rs index fbe4f77..e6942db 100644 --- a/src/routes/bestiary.rs +++ b/src/routes/bestiary.rs @@ -1,8 +1,11 @@ use crate::models::creature::Creature; -use crate::models::creature_metadata_enums::{AlignmentEnum, RarityEnum, SizeEnum}; -use crate::models::routers_validator_structs::{FieldFilters, PaginatedRequest, SortData}; +use crate::models::creature_metadata_enums::{ + AlignmentEnum, CreatureTypeEnum, RarityEnum, SizeEnum, +}; +use crate::models::routers_validator_structs::{FieldFilters, PaginatedRequest}; use crate::services::bestiary_service; use crate::services::bestiary_service::BestiaryResponse; +use crate::AppState; use actix_web::{error, get, web, Responder, Result}; use actix_web_validator::Query; use utoipa::OpenApi; @@ -38,7 +41,14 @@ pub fn init_docs(doc: &mut utoipa::openapi::OpenApi) { get_elite_creature, get_weak_creature, ), - components(schemas(BestiaryResponse, Creature, AlignmentEnum, RarityEnum, SizeEnum)) + components(schemas( + BestiaryResponse, + Creature, + AlignmentEnum, + RarityEnum, + SizeEnum, + CreatureTypeEnum + )) )] struct ApiDoc; @@ -50,7 +60,7 @@ pub fn init_docs(doc: &mut utoipa::openapi::OpenApi) { path = "/bestiary/list", tag = "bestiary", params( - SortData, FieldFilters, PaginatedRequest + FieldFilters, PaginatedRequest ), responses( (status=200, description = "Successful Response", body = BestiaryResponse), @@ -59,15 +69,13 @@ pub fn init_docs(doc: &mut utoipa::openapi::OpenApi) { )] #[get("/list")] pub async fn get_bestiary( - sort: Query, + data: web::Data, filters: Query, pagination: Query, ) -> Result { - Ok(web::Json(bestiary_service::get_bestiary( - &sort.0, - &filters.0, - &pagination.0, - ))) + Ok(web::Json( + bestiary_service::get_bestiary(&data, &filters.0, &pagination.0).await, + )) } #[utoipa::path( @@ -83,8 +91,8 @@ pub async fn get_bestiary( ), )] #[get("/families")] -pub async fn get_families_list() -> Result { - Ok(web::Json(bestiary_service::get_families_list())) +pub async fn get_families_list(data: web::Data) -> Result { + Ok(web::Json(bestiary_service::get_families_list(&data).await)) } #[utoipa::path( @@ -100,8 +108,8 @@ pub async fn get_families_list() -> Result { ), )] #[get("/traits")] -pub async fn get_traits_list() -> Result { - Ok(web::Json(bestiary_service::get_traits_list())) +pub async fn get_traits_list(data: web::Data) -> Result { + Ok(web::Json(bestiary_service::get_traits_list(&data).await)) } #[utoipa::path( @@ -117,8 +125,8 @@ pub async fn get_traits_list() -> Result { ), )] #[get("/rarities")] -pub async fn get_rarities_list() -> Result { - Ok(web::Json(bestiary_service::get_rarities_list())) +pub async fn get_rarities_list(data: web::Data) -> Result { + Ok(web::Json(bestiary_service::get_rarities_list(&data).await)) } #[utoipa::path( @@ -134,8 +142,8 @@ pub async fn get_rarities_list() -> Result { ), )] #[get("/sizes")] -pub async fn get_sizes_list() -> Result { - Ok(web::Json(bestiary_service::get_sizes_list())) +pub async fn get_sizes_list(data: web::Data) -> Result { + Ok(web::Json(bestiary_service::get_sizes_list(&data).await)) } #[utoipa::path( @@ -151,8 +159,10 @@ pub async fn get_sizes_list() -> Result { ), )] #[get("/alignments")] -pub async fn get_alignments_list() -> Result { - Ok(web::Json(bestiary_service::get_alignments_list())) +pub async fn get_alignments_list(data: web::Data) -> Result { + Ok(web::Json( + bestiary_service::get_alignments_list(&data).await, + )) } #[utoipa::path( @@ -168,8 +178,10 @@ pub async fn get_alignments_list() -> Result { ), )] #[get("/creature_types")] -pub async fn get_creature_types_list() -> Result { - Ok(web::Json(bestiary_service::get_creature_types_list())) +pub async fn get_creature_types_list(data: web::Data) -> Result { + Ok(web::Json( + bestiary_service::get_creature_types_list(&data).await, + )) } #[utoipa::path( @@ -185,9 +197,12 @@ pub async fn get_creature_types_list() -> Result { ), )] #[get("/base/{creature_id}")] -pub async fn get_creature(creature_id: web::Path) -> Result { +pub async fn get_creature( + data: web::Data, + creature_id: web::Path, +) -> Result { Ok(web::Json( - bestiary_service::get_creature(sanitize_id(&creature_id)?).await, + bestiary_service::get_creature(&data, sanitize_id(&creature_id)?).await, )) } @@ -204,9 +219,12 @@ pub async fn get_creature(creature_id: web::Path) -> Result) -> Result { +pub async fn get_elite_creature( + data: web::Data, + creature_id: web::Path, +) -> Result { Ok(web::Json( - bestiary_service::get_elite_creature(sanitize_id(&creature_id)?).await, + bestiary_service::get_elite_creature(&data, sanitize_id(&creature_id)?).await, )) } @@ -223,9 +241,12 @@ pub async fn get_elite_creature(creature_id: web::Path) -> Result) -> Result { +pub async fn get_weak_creature( + data: web::Data, + creature_id: web::Path, +) -> Result { Ok(web::Json( - bestiary_service::get_weak_creature(sanitize_id(&creature_id)?).await, + bestiary_service::get_weak_creature(&data, sanitize_id(&creature_id)?).await, )) } diff --git a/src/routes/encounter.rs b/src/routes/encounter.rs index d1fac84..abdf8f2 100644 --- a/src/routes/encounter.rs +++ b/src/routes/encounter.rs @@ -4,6 +4,7 @@ use crate::models::encounter_structs::{ use crate::services::encounter_service; use crate::services::encounter_service::EncounterInfoResponse; use crate::services::encounter_service::RandomEncounterGeneratorResponse; +use crate::AppState; use actix_web::{post, web, Responder, Result}; use utoipa::OpenApi; @@ -73,9 +74,10 @@ pub async fn get_encounter_info( )] #[post("/generator")] pub async fn get_generated_random_encounter( + data: web::Data, web::Json(body): web::Json, ) -> Result { - Ok(web::Json(encounter_service::generate_random_encounter( - body, - ))) + Ok(web::Json( + encounter_service::generate_random_encounter(&data, body).await, + )) } diff --git a/src/routes/health.rs b/src/routes/health.rs index 2304f88..a61da00 100644 --- a/src/routes/health.rs +++ b/src/routes/health.rs @@ -1,4 +1,4 @@ -use crate::db::db_communicator::is_redis_up; +use crate::AppState; use actix_web::web::Json; use actix_web::{get, web}; use maplit::hashmap; @@ -34,14 +34,15 @@ pub fn init_docs(doc: &mut utoipa::openapi::OpenApi) { ), )] #[get("/health")] -pub async fn get_health() -> Json { - let is_redis_up = is_redis_up().unwrap_or(false); +pub async fn get_health(data: web::Data) -> Json { + let conn = &data.conn; + let is_db_up = !conn.is_closed(); Json(HealthResponse { - ready: is_redis_up.to_string(), + ready: is_db_up.to_string(), dependencies: vec![hashmap! { - "name".to_string() => "redis database".to_string(), - "ready".to_string() => is_redis_up.to_string(), - "live".to_string() => is_redis_up.to_string(), + "name".to_string() => "SQLite database".to_string(), + "ready".to_string() => is_db_up.to_string(), + "live".to_string() => is_db_up.to_string(), "type".to_string() => "REQUIRED".to_string(), }], }) diff --git a/src/services/bestiary_service.rs b/src/services/bestiary_service.rs index 5ce6755..1f931eb 100644 --- a/src/services/bestiary_service.rs +++ b/src/services/bestiary_service.rs @@ -2,8 +2,9 @@ use crate::db::db_proxy; use crate::db::db_proxy::get_creature_by_id; use crate::models::creature::Creature; use crate::models::creature_fields_enum::CreatureField; -use crate::models::routers_validator_structs::{FieldFilters, PaginatedRequest, SortData}; +use crate::models::routers_validator_structs::{FieldFilters, PaginatedRequest}; use crate::services::url_calculator::{add_boolean_query, next_url_calculator}; +use crate::AppState; use anyhow::Result; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -16,61 +17,63 @@ pub struct BestiaryResponse { next: Option, } -pub async fn get_creature(id: i32) -> HashMap> { - hashmap! {String::from("results") => get_creature_by_id(id)} +pub async fn get_creature(app_state: &AppState, id: i32) -> HashMap> { + hashmap! {String::from("results") => get_creature_by_id(app_state, id).await} } -pub async fn get_elite_creature(id: i32) -> HashMap> { - hashmap! {String::from("results") => update_creature(id, 1)} +pub async fn get_elite_creature( + app_state: &AppState, + id: i32, +) -> HashMap> { + hashmap! {String::from("results") => update_creature(app_state, id, 1).await} } -pub async fn get_weak_creature(id: i32) -> HashMap> { - hashmap! {String::from("results") => update_creature(id, -1)} +pub async fn get_weak_creature(app_state: &AppState, id: i32) -> HashMap> { + hashmap! {String::from("results") => update_creature(app_state, id, -1).await} } -pub fn get_bestiary( - sort_field: &SortData, +pub async fn get_bestiary( + app_state: &AppState, field_filter: &FieldFilters, pagination: &PaginatedRequest, ) -> BestiaryResponse { convert_result_to_bestiary_response( - sort_field, field_filter, pagination, - db_proxy::get_paginated_creatures(sort_field, field_filter, pagination), + db_proxy::get_paginated_creatures(app_state, field_filter, pagination).await, ) } -pub fn get_families_list() -> Vec { - db_proxy::get_keys(CreatureField::Family) +pub async fn get_families_list(app_state: &AppState) -> Vec { + db_proxy::get_keys(app_state, CreatureField::Family).await } -pub fn get_traits_list() -> Vec { - db_proxy::get_keys(CreatureField::Traits) +pub async fn get_traits_list(app_state: &AppState) -> Vec { + db_proxy::get_keys(app_state, CreatureField::Traits).await } -pub fn get_rarities_list() -> Vec { - db_proxy::get_keys(CreatureField::Rarity) +pub async fn get_rarities_list(app_state: &AppState) -> Vec { + db_proxy::get_keys(app_state, CreatureField::Rarity).await } -pub fn get_sizes_list() -> Vec { - db_proxy::get_keys(CreatureField::Size) +pub async fn get_sizes_list(app_state: &AppState) -> Vec { + db_proxy::get_keys(app_state, CreatureField::Size).await } -pub fn get_alignments_list() -> Vec { - db_proxy::get_keys(CreatureField::Alignment) +pub async fn get_alignments_list(app_state: &AppState) -> Vec { + db_proxy::get_keys(app_state, CreatureField::Alignment).await } -pub fn get_creature_types_list() -> Vec { - db_proxy::get_keys(CreatureField::CreatureTypes) +pub async fn get_creature_types_list(app_state: &AppState) -> Vec { + db_proxy::get_keys(app_state, CreatureField::CreatureTypes).await } fn hp_increase_by_level() -> HashMap { hashmap! { 1 => 10, 2=> 15, 5=> 20, 20=> 30 } } -fn update_creature(id: i32, level_delta: i8) -> Option { - match get_creature_by_id(id) { +async fn update_creature(app_state: &AppState, id: i32, level_delta: i8) -> Option { + match get_creature_by_id(app_state, id).await { Some(mut creature) => { let hp_increase = hp_increase_by_level(); let desired_key = hp_increase @@ -95,7 +98,6 @@ fn update_creature(id: i32, level_delta: i8) -> Option { } fn convert_result_to_bestiary_response( - sort_field: &SortData, field_filters: &FieldFilters, pagination: &PaginatedRequest, result: Result<(u32, Vec)>, @@ -108,12 +110,7 @@ fn convert_result_to_bestiary_response( results: Some(cr), count: cr_length, next: if cr_length >= pagination.page_size as usize { - Some(next_url_calculator( - sort_field, - field_filters, - pagination, - res.0, - )) + Some(next_url_calculator(field_filters, pagination, res.0)) } else { None }, diff --git a/src/services/encounter_service.rs b/src/services/encounter_service.rs index e636763..870f8cb 100644 --- a/src/services/encounter_service.rs +++ b/src/services/encounter_service.rs @@ -9,6 +9,7 @@ use crate::models::encounter_structs::{ }; use crate::services::encounter_handler::encounter_calculator; use crate::services::encounter_handler::encounter_calculator::calculate_encounter_scaling_difficulty; +use crate::AppState; use anyhow::{ensure, Result}; use counter::Counter; use log::warn; @@ -48,11 +49,12 @@ pub fn get_encounter_info(enc_params: EncounterParams) -> EncounterInfoResponse } } -pub fn generate_random_encounter( +pub async fn generate_random_encounter( + app_state: &AppState, enc_data: RandomEncounterData, ) -> RandomEncounterGeneratorResponse { let party_levels = enc_data.party_levels.clone(); - let encounter_data = calculate_random_encounter(enc_data, party_levels); + let encounter_data = calculate_random_encounter(app_state, enc_data, party_levels).await; match encounter_data { Err(error) => { warn!( @@ -74,7 +76,8 @@ pub fn generate_random_encounter( } // Private method, does not handle failure. For that we use a public method -fn calculate_random_encounter( +async fn calculate_random_encounter( + app_state: &AppState, enc_data: RandomEncounterData, party_levels: Vec, ) -> Result { @@ -102,7 +105,7 @@ fn calculate_random_encounter( enc_data.creature_types, unique_levels, ); - let filtered_creatures = fetch_creatures_passing_all_filters(filter_map)?; + let filtered_creatures = fetch_creatures_passing_all_filters(app_state, filter_map).await?; ensure!( !filtered_creatures.is_empty(), "No creatures have been fetched" diff --git a/src/services/url_calculator.rs b/src/services/url_calculator.rs index a42a681..a6fdd36 100644 --- a/src/services/url_calculator.rs +++ b/src/services/url_calculator.rs @@ -1,25 +1,16 @@ use crate::models::creature_metadata_enums::{creature_type_to_url_string, CreatureTypeEnum}; -use crate::models::routers_validator_structs::{FieldFilters, PaginatedRequest, SortData}; +use crate::models::routers_validator_structs::{FieldFilters, PaginatedRequest}; pub fn next_url_calculator( - sort_field: &SortData, field_filters: &FieldFilters, pagination: &PaginatedRequest, next_cursor: u32, ) -> String { - let base_url = "https://bybe.fly.dev/bestiary/list/"; //"0.0.0.0:25566/list/" - let sort_query = format!( - "?sort_key={}&order_by={}", - sort_field.sort_key.unwrap_or_default(), - sort_field.order_by.unwrap_or_default() - ); + let base_url = "https://backbybe.fly.dev/bestiary/list/"; //"0.0.0.0:25566/list/" let filter_query = filter_query_calculator(field_filters); let pagination_query = format!("&cursor={}&page_size={}", next_cursor, pagination.page_size); - format!( - "{}{}{}{}", - base_url, sort_query, filter_query, pagination_query - ) + format!("{}{}{}", base_url, filter_query, pagination_query) } pub fn generate_archive_link(id: i32, creature_type: &CreatureTypeEnum) -> String { From 7953c746f52962899c26b365f994ffdb6e25f1a8 Mon Sep 17 00:00:00 2001 From: RakuJa Date: Thu, 16 Nov 2023 16:59:53 +0100 Subject: [PATCH 2/3] chore: Add valid rust actions --- .github/workflows/clippy.yml | 14 ++++++++++++ .github/workflows/fmt.yml | 18 +++++++++++++++ .github/workflows/sonarcloud.yml | 38 -------------------------------- 3 files changed, 32 insertions(+), 38 deletions(-) create mode 100644 .github/workflows/clippy.yml create mode 100644 .github/workflows/fmt.yml delete mode 100644 .github/workflows/sonarcloud.yml diff --git a/.github/workflows/clippy.yml b/.github/workflows/clippy.yml new file mode 100644 index 0000000..9cbf9bd --- /dev/null +++ b/.github/workflows/clippy.yml @@ -0,0 +1,14 @@ +on: push +name: Clippy check + +# Make sure CI fails on all warnings, including Clippy lints +env: + RUSTFLAGS: "-Dwarnings" + +jobs: + clippy_check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Run Clippy + run: cargo clippy --all-targets --all-features diff --git a/.github/workflows/fmt.yml b/.github/workflows/fmt.yml new file mode 100644 index 0000000..d727d69 --- /dev/null +++ b/.github/workflows/fmt.yml @@ -0,0 +1,18 @@ +on: pull_request + +name: Rustfmt + +jobs: + format: + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@nightly + with: + components: rustfmt + - uses: mbrobbel/rustfmt-check@master + with: + token: ${{ secrets.GITHUB_TOKEN }} + mode: review \ No newline at end of file diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml deleted file mode 100644 index fe3b387..0000000 --- a/.github/workflows/sonarcloud.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: SonarCloud analysis - -on: - push: - branches: [ "master" ] - pull_request: - branches: [ "master" ] - workflow_dispatch: - -permissions: - pull-requests: read # allows SonarCloud to decorate PRs with analysis results - -jobs: - Analysis: - runs-on: ubuntu-latest - - steps: - - name: Analyze with SonarCloud - - # You can pin the exact commit or the version. - # uses: SonarSource/sonarcloud-github-action@de2e56b42aa84d0b1c5b622644ac17e505c9a049 - uses: SonarSource/sonarcloud-github-action@de2e56b42aa84d0b1c5b622644ac17e505c9a049 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} # Generate a token on Sonarcloud.io, add it to the secrets of this repo with the name SONAR_TOKEN (Settings > Secrets > Actions > add new repository secret) - with: - # Additional arguments for the sonarcloud scanner - args: - # Unique keys of your project and organization. You can find them in SonarCloud > Information (bottom-left menu) - # mandatory - -Dsonar.projectKey=RakuJa_BYBE - -Dsonar.organization=rakuja - #-Dsonar.projectBaseDir=app - # Comma-separated paths to directories containing main source files. - #-Dsonar.sources= # optional, default is project base directory - #-Dsonar.tests= # optional. For more info about Code Coverage, please refer to https://docs.sonarcloud.io/enriching/test-coverage/overview/ - # Adds more detail to both client and server-side analysis logs, activating DEBUG mode for the scanner, and adding client-side environment variables and system properties to the server-side log of analysis report processing. - #-Dsonar.verbose= # optional, default is false From 7785ed43f483468d7962be34fb5c852c1bb9fa25 Mon Sep 17 00:00:00 2001 From: RakuJa Date: Thu, 16 Nov 2023 17:19:57 +0100 Subject: [PATCH 3/3] feat: Implement source endpoint (#30) --- src/db/db_cache.rs | 10 ++++++++++ src/db/db_communicator.rs | 4 +--- src/db/db_proxy.rs | 1 + src/routes/bestiary.rs | 19 +++++++++++++++++++ src/services/bestiary_service.rs | 4 ++++ 5 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/db/db_cache.rs b/src/db/db_cache.rs index 81ce106..5aed42a 100644 --- a/src/db/db_cache.rs +++ b/src/db/db_cache.rs @@ -7,6 +7,7 @@ pub struct RuntimeFieldsValues { pub list_of_levels: Vec, pub list_of_families: Vec, pub list_of_traits: Vec, + pub list_of_sources: Vec, pub list_of_alignments: Vec, pub list_of_sizes: Vec, pub list_of_rarities: Vec, @@ -25,6 +26,7 @@ pub fn from_db_data_to_filter_cache( list_of_levels: runtime_fields.list_of_levels.clone(), list_of_families: runtime_fields.list_of_families.clone(), list_of_traits: runtime_fields.list_of_traits.clone(), + list_of_sources: runtime_fields.list_of_sources.clone(), list_of_alignments: runtime_fields.list_of_alignments.clone(), list_of_sizes: runtime_fields.list_of_sizes.clone(), list_of_rarities: runtime_fields.list_of_rarities.clone(), @@ -62,6 +64,14 @@ pub fn from_db_data_to_filter_cache( } }); + curr_creature.sources.iter().for_each(|single_source| { + if !fields_values_cache.list_of_sources.contains(single_source) { + fields_values_cache + .list_of_sources + .push(single_source.to_string()) + } + }); + if !fields_values_cache.list_of_alignments.contains(&alignment) { fields_values_cache.list_of_alignments.push(alignment); } diff --git a/src/db/db_communicator.rs b/src/db/db_communicator.rs index 365a7ea..7437159 100644 --- a/src/db/db_communicator.rs +++ b/src/db/db_communicator.rs @@ -84,9 +84,7 @@ async fn from_raw_to_creature(conn: &Pool, raw: &RawCreature) -> Creatur } pub async fn fetch_creatures(conn: &Pool) -> Result, Error> { - let creatures = sqlx::query_as::<_, RawCreature>( - "SELECT * FROM CREATURE_TABLE ORDER BY name" - ) + let creatures = sqlx::query_as::<_, RawCreature>("SELECT * FROM CREATURE_TABLE ORDER BY name") .fetch_all(conn) .await; match creatures { diff --git a/src/db/db_proxy.rs b/src/db/db_proxy.rs index 2a9a54d..309293e 100644 --- a/src/db/db_proxy.rs +++ b/src/db/db_proxy.rs @@ -116,6 +116,7 @@ pub async fn get_keys(app_state: &AppState, field: CreatureField) -> Vec 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 => runtime_fields_values.list_of_alignments, CreatureField::Level => runtime_fields_values.list_of_levels, CreatureField::CreatureTypes => runtime_fields_values.list_of_creature_types, diff --git a/src/routes/bestiary.rs b/src/routes/bestiary.rs index e6942db..668ac15 100644 --- a/src/routes/bestiary.rs +++ b/src/routes/bestiary.rs @@ -19,6 +19,7 @@ pub fn init_endpoints(cfg: &mut web::ServiceConfig) { .service(get_creature) .service(get_families_list) .service(get_traits_list) + .service(get_sources_list) .service(get_rarities_list) .service(get_creature_types_list) .service(get_sizes_list) @@ -33,6 +34,7 @@ pub fn init_docs(doc: &mut utoipa::openapi::OpenApi) { get_bestiary, get_families_list, get_traits_list, + get_sources_list, get_rarities_list, get_sizes_list, get_alignments_list, @@ -112,6 +114,23 @@ pub async fn get_traits_list(data: web::Data) -> Result) -> Result { + Ok(web::Json(bestiary_service::get_sources_list(&data).await)) +} + #[utoipa::path( get, path = "/bestiary/rarities", diff --git a/src/services/bestiary_service.rs b/src/services/bestiary_service.rs index 1f931eb..5e4b886 100644 --- a/src/services/bestiary_service.rs +++ b/src/services/bestiary_service.rs @@ -52,6 +52,10 @@ pub async fn get_traits_list(app_state: &AppState) -> Vec { db_proxy::get_keys(app_state, CreatureField::Traits).await } +pub async fn get_sources_list(app_state: &AppState) -> Vec { + db_proxy::get_keys(app_state, CreatureField::Sources).await +} + pub async fn get_rarities_list(app_state: &AppState) -> Vec { db_proxy::get_keys(app_state, CreatureField::Rarity).await }