From c6ec8467c18227fe2fdb3b9ae31572b006e3824d Mon Sep 17 00:00:00 2001 From: Joey Hines Date: Thu, 30 Dec 2021 21:48:33 -0600 Subject: [PATCH] report_out_of_stock command + new query system + report_out_of_stock can be used by a when an item is not in stock in a shop + Added new query system + Impl for both LocationDB and Player for now + Goal is to increase code re-use without a million functions for different queries + Should be expanded to more models + I Should really just rip out the DB and make it a generic thing + Clippy + fmt --- geoffrey_api/src/commands/add_item.rs | 17 ++- geoffrey_api/src/commands/delete.rs | 14 +- geoffrey_api/src/commands/edit.rs | 16 +- geoffrey_api/src/commands/info.rs | 14 +- geoffrey_api/src/commands/mod.rs | 3 + geoffrey_api/src/commands/remove_item.rs | 25 +-- .../src/commands/report_out_of_stock.rs | 71 +++++++++ geoffrey_api/src/commands/restock.rs | 27 ++-- geoffrey_api/src/commands/set_portal.rs | 16 +- geoffrey_bot/src/bot/commands/remove_item.rs | 4 +- geoffrey_bot/src/bot/commands/restock.rs | 4 +- geoffrey_db/src/database.rs | 43 ++++-- geoffrey_db/src/error.rs | 9 ++ geoffrey_db/src/lib.rs | 23 +++ geoffrey_db/src/query/location_query.rs | 144 ++++++++++++++++++ geoffrey_db/src/query/mod.rs | 33 ++++ geoffrey_db/src/query/player_query.rs | 101 ++++++++++++ ...stock_params.rs => item_command_params.rs} | 6 +- geoffrey_models/src/models/parameters/mod.rs | 3 +- .../models/parameters/remove_item_params.rs | 19 --- .../src/models/response/api_error.rs | 4 + 21 files changed, 510 insertions(+), 86 deletions(-) create mode 100644 geoffrey_api/src/commands/report_out_of_stock.rs create mode 100644 geoffrey_db/src/query/location_query.rs create mode 100644 geoffrey_db/src/query/mod.rs create mode 100644 geoffrey_db/src/query/player_query.rs rename geoffrey_models/src/models/parameters/{restock_params.rs => item_command_params.rs} (76%) delete mode 100644 geoffrey_models/src/models/parameters/remove_item_params.rs diff --git a/geoffrey_api/src/commands/add_item.rs b/geoffrey_api/src/commands/add_item.rs index a3079e1..8bdd82c 100644 --- a/geoffrey_api/src/commands/add_item.rs +++ b/geoffrey_api/src/commands/add_item.rs @@ -3,9 +3,10 @@ use crate::config::GeoffreyAPIConfig; use crate::context::Context; use crate::helper::validate_string_parameter; use crate::Result; -use geoffrey_db::helper::{find_location_by_name_type, load_location}; +use geoffrey_db::helper::load_location; +use geoffrey_db::query::QueryBuilder; use geoffrey_models::models::item::ItemListing; -use geoffrey_models::models::locations::{Location, LocationDataDb, LocationType}; +use geoffrey_models::models::locations::{Location, LocationDataDb, LocationDb, LocationType}; use geoffrey_models::models::parameters::add_item_params::AddItemParams; use geoffrey_models::models::player::Player; use geoffrey_models::models::response::api_error::GeoffreyAPIError; @@ -33,8 +34,16 @@ impl Command for AddItem { fn run_command(ctx: Arc, req: &Self::Req, user: Option) -> Result { let user = user.unwrap(); - let mut shop = - find_location_by_name_type(&ctx.db, &req.shop, user.id.unwrap(), LocationType::Shop)?; + let query = QueryBuilder::::default() + .with_type(LocationType::Shop) + .with_owner(user.id.unwrap()) + .with_name(&req.shop); + + let mut shop = ctx + .db + .run_query(query)? + .pop() + .ok_or(GeoffreyAPIError::EntryNotFound)?; if let LocationDataDb::Shop(shop_data) = &mut shop.loc_data { shop_data.item_listings.insert(ItemListing::new( diff --git a/geoffrey_api/src/commands/delete.rs b/geoffrey_api/src/commands/delete.rs index 07964d5..4a4dc65 100644 --- a/geoffrey_api/src/commands/delete.rs +++ b/geoffrey_api/src/commands/delete.rs @@ -1,7 +1,8 @@ use crate::commands::{Command, RequestType}; use crate::context::Context; use crate::Result; -use geoffrey_db::helper::{find_location_by_name, load_location}; +use geoffrey_db::helper::load_location; +use geoffrey_db::query::QueryBuilder; use geoffrey_models::models::locations::{Location, LocationDb}; use geoffrey_models::models::parameters::delete_params::DeleteParams; use geoffrey_models::models::player::Player; @@ -30,7 +31,16 @@ impl Command for Delete { fn run_command(ctx: Arc, req: &Self::Req, user: Option) -> Result { let user = user.unwrap(); - let location = find_location_by_name(&ctx.db, &req.location, user.id.unwrap())?; + let query = QueryBuilder::::default() + .with_owner(user.id.unwrap()) + .with_name(&req.location); + + let location = ctx + .db + .run_query(query)? + .pop() + .ok_or(GeoffreyAPIError::EntryNotFound)?; + let location = load_location(&ctx.db, &location).map_err(GeoffreyAPIError::from)?; ctx.db.remove::(location.id)?; diff --git a/geoffrey_api/src/commands/edit.rs b/geoffrey_api/src/commands/edit.rs index 855ee52..8bcbd22 100644 --- a/geoffrey_api/src/commands/edit.rs +++ b/geoffrey_api/src/commands/edit.rs @@ -1,10 +1,12 @@ use crate::commands::{Command, RequestType}; use crate::context::Context; use crate::Result; -use geoffrey_db::helper::{find_location_by_name, load_location}; -use geoffrey_models::models::locations::Location; +use geoffrey_db::helper::load_location; +use geoffrey_db::query::QueryBuilder; +use geoffrey_models::models::locations::{Location, LocationDb}; use geoffrey_models::models::parameters::edit_params::EditParams; use geoffrey_models::models::player::Player; +use geoffrey_models::models::response::api_error::GeoffreyAPIError; use geoffrey_models::models::CommandLevel; use std::sync::Arc; @@ -29,7 +31,15 @@ impl Command for Edit { fn run_command(ctx: Arc, req: &Self::Req, user: Option) -> Result { let user = user.unwrap(); - let mut location = find_location_by_name(&ctx.db, &req.loc_name, user.id.unwrap())?; + let query = QueryBuilder::::default() + .with_owner(user.id.unwrap()) + .with_name(&req.loc_name); + + let mut location = ctx + .db + .run_query(query)? + .pop() + .ok_or(GeoffreyAPIError::EntryNotFound)?; if let Some(new_pos) = &req.new_pos { location.position = *new_pos; diff --git a/geoffrey_api/src/commands/info.rs b/geoffrey_api/src/commands/info.rs index 73b715d..7144a73 100644 --- a/geoffrey_api/src/commands/info.rs +++ b/geoffrey_api/src/commands/info.rs @@ -2,6 +2,7 @@ use crate::commands::{Command, RequestType}; use crate::context::Context; use crate::Result; use geoffrey_db::helper::load_location; +use geoffrey_db::query::QueryBuilder; use geoffrey_models::models::locations::{Location, LocationDb}; use geoffrey_models::models::parameters::info_params::InfoParams; use geoffrey_models::models::player::Player; @@ -28,17 +29,12 @@ impl Command for InfoCommand { } fn run_command(ctx: Arc, req: &Self::Req, _: Option) -> Result { - let query = req.location_name.to_lowercase(); + let query = QueryBuilder::::default().with_name_regex(&req.location_name)?; - let location: LocationDb = ctx + let location = ctx .db - .filter(|_, loc: &LocationDb| { - let name = loc.name.to_lowercase(); - - name == query - }) - .map_err(GeoffreyAPIError::from)? - .next() + .run_query(query)? + .pop() .ok_or(GeoffreyAPIError::EntryNotFound)?; Ok(load_location(&ctx.db, &location)?) diff --git a/geoffrey_api/src/commands/mod.rs b/geoffrey_api/src/commands/mod.rs index 4c73b9d..265bbc1 100644 --- a/geoffrey_api/src/commands/mod.rs +++ b/geoffrey_api/src/commands/mod.rs @@ -7,6 +7,7 @@ use crate::commands::info::InfoCommand; use crate::commands::link::LinkCommand; use crate::commands::register::Register; use crate::commands::remove_item::RemoveItem; +use crate::commands::report_out_of_stock::ReportOutOfStock; use crate::commands::restock::Restock; use crate::commands::selling::Selling; use crate::commands::set_portal::SetPortal; @@ -37,6 +38,7 @@ pub mod info; pub mod link; pub mod register; pub mod remove_item; +pub mod report_out_of_stock; pub mod restock; pub mod selling; pub mod set_portal; @@ -147,6 +149,7 @@ pub fn command_filter( .or(create_command_filter::(ctx.clone())) .or(create_command_filter::(ctx.clone())) .or(create_command_filter::(ctx.clone())) + .or(create_command_filter::(ctx.clone())) .or(create_command_filter::(ctx)), ) } diff --git a/geoffrey_api/src/commands/remove_item.rs b/geoffrey_api/src/commands/remove_item.rs index 033c0b7..80fadd4 100644 --- a/geoffrey_api/src/commands/remove_item.rs +++ b/geoffrey_api/src/commands/remove_item.rs @@ -1,9 +1,10 @@ use crate::commands::{Command, RequestType}; use crate::context::Context; use crate::Result; -use geoffrey_db::helper::{find_location_by_name_type, load_location}; -use geoffrey_models::models::locations::{Location, LocationDataDb, LocationType}; -use geoffrey_models::models::parameters::remove_item_params::RemoveItemParameters; +use geoffrey_db::helper::load_location; +use geoffrey_db::query::QueryBuilder; +use geoffrey_models::models::locations::{Location, LocationDataDb, LocationDb, LocationType}; +use geoffrey_models::models::parameters::item_command_params::ItemCommandParams; use geoffrey_models::models::player::Player; use geoffrey_models::models::response::api_error::GeoffreyAPIError; use geoffrey_models::models::CommandLevel; @@ -12,7 +13,7 @@ use std::sync::Arc; pub struct RemoveItem {} impl Command for RemoveItem { - type Req = RemoveItemParameters; + type Req = ItemCommandParams; type Resp = Location; fn command_name() -> String { @@ -30,12 +31,16 @@ impl Command for RemoveItem { fn run_command(ctx: Arc, req: &Self::Req, user: Option) -> Result { let user = user.unwrap(); - let mut shop = find_location_by_name_type( - &ctx.db, - &req.shop_name, - user.id.unwrap(), - LocationType::Shop, - )?; + let query = QueryBuilder::::default() + .with_type(LocationType::Shop) + .with_name(&req.shop_name) + .with_owner(user.id.unwrap()); + + let mut shop = ctx + .db + .run_query(query)? + .pop() + .ok_or(GeoffreyAPIError::EntryNotFound)?; if let LocationDataDb::Shop(shop_data) = &mut shop.loc_data { shop_data diff --git a/geoffrey_api/src/commands/report_out_of_stock.rs b/geoffrey_api/src/commands/report_out_of_stock.rs new file mode 100644 index 0000000..96a7324 --- /dev/null +++ b/geoffrey_api/src/commands/report_out_of_stock.rs @@ -0,0 +1,71 @@ +use crate::commands::{Command, RequestType}; +use crate::context::Context; +use crate::Result; +use geoffrey_db::helper::load_location; +use geoffrey_db::query::QueryBuilder; +use geoffrey_models::models::item::ItemListing; +use geoffrey_models::models::locations::{Location, LocationDataDb, LocationDb, LocationType}; +use geoffrey_models::models::parameters::item_command_params::ItemCommandParams; +use geoffrey_models::models::player::Player; +use geoffrey_models::models::response::api_error::GeoffreyAPIError; +use geoffrey_models::models::CommandLevel; +use std::collections::HashSet; +use std::sync::Arc; + +pub struct ReportOutOfStock {} + +impl Command for ReportOutOfStock { + type Req = ItemCommandParams; + type Resp = Location; + + fn command_name() -> String { + "report_out_of_stock".to_string() + } + + fn request_type() -> RequestType { + RequestType::POST + } + + fn command_level() -> CommandLevel { + CommandLevel::REGISTERED + } + + fn run_command(ctx: Arc, req: &Self::Req, user: Option) -> Result { + let user = user.unwrap(); + + let query = QueryBuilder::::default() + .with_type(LocationType::Shop) + .with_name(&req.shop_name); + + let mut shop = ctx + .db + .run_query(query)? + .pop() + .ok_or(GeoffreyAPIError::EntryNotFound)?; + + if let LocationDataDb::Shop(shop_data) = &mut shop.loc_data { + let filter = regex::Regex::new(&req.item_name) + .map_err(|e| GeoffreyAPIError::InvalidRegex(e.to_string()))?; + + let updated_items: HashSet = shop_data + .item_listings + .iter() + .map(|item| { + let mut item = item.clone(); + if filter.is_match(&item.item.name) { + item.report_out_of_stock(user.id.unwrap()) + } + + item + }) + .collect(); + + shop_data.item_listings = updated_items; + let shop = ctx.db.insert(shop)?; + + load_location(&ctx.db, &shop).map_err(|err| err.into()) + } else { + Err(GeoffreyAPIError::EntryNotFound) + } + } +} diff --git a/geoffrey_api/src/commands/restock.rs b/geoffrey_api/src/commands/restock.rs index 5c13271..1581e80 100644 --- a/geoffrey_api/src/commands/restock.rs +++ b/geoffrey_api/src/commands/restock.rs @@ -1,10 +1,11 @@ use crate::commands::{Command, RequestType}; use crate::context::Context; use crate::Result; -use geoffrey_db::helper::{find_location_by_name_type, load_location}; +use geoffrey_db::helper::load_location; +use geoffrey_db::query::QueryBuilder; use geoffrey_models::models::item::ItemListing; -use geoffrey_models::models::locations::{Location, LocationDataDb, LocationType}; -use geoffrey_models::models::parameters::restock_params::RestockParameters; +use geoffrey_models::models::locations::{Location, LocationDataDb, LocationDb, LocationType}; +use geoffrey_models::models::parameters::item_command_params::ItemCommandParams; use geoffrey_models::models::player::Player; use geoffrey_models::models::response::api_error::GeoffreyAPIError; use geoffrey_models::models::CommandLevel; @@ -14,7 +15,7 @@ use std::sync::Arc; pub struct Restock {} impl Command for Restock { - type Req = RestockParameters; + type Req = ItemCommandParams; type Resp = Location; fn command_name() -> String { @@ -32,16 +33,20 @@ impl Command for Restock { fn run_command(ctx: Arc, req: &Self::Req, user: Option) -> Result { let user = user.unwrap(); - let mut shop = find_location_by_name_type( - &ctx.db, - &req.shop_name, - user.id.unwrap(), - LocationType::Shop, - )?; + let query = QueryBuilder::::default() + .with_type(LocationType::Shop) + .with_name(&req.shop_name) + .with_owner(user.id.unwrap()); + + let mut shop = ctx + .db + .run_query(query)? + .pop() + .ok_or(GeoffreyAPIError::EntryNotFound)?; if let LocationDataDb::Shop(shop_data) = &mut shop.loc_data { let filter = regex::Regex::new(&req.item_name) - .map_err(|_| GeoffreyAPIError::ParameterInvalid("item_name".to_string()))?; + .map_err(|e| GeoffreyAPIError::InvalidRegex(e.to_string()))?; let updated_items: HashSet = shop_data .item_listings diff --git a/geoffrey_api/src/commands/set_portal.rs b/geoffrey_api/src/commands/set_portal.rs index bd9cf1c..6dea219 100644 --- a/geoffrey_api/src/commands/set_portal.rs +++ b/geoffrey_api/src/commands/set_portal.rs @@ -1,10 +1,12 @@ use crate::commands::{Command, RequestType}; use crate::context::Context; use crate::Result; -use geoffrey_db::helper::{find_location_by_name, load_location}; -use geoffrey_models::models::locations::Location; +use geoffrey_db::helper::load_location; +use geoffrey_db::query::QueryBuilder; +use geoffrey_models::models::locations::{Location, LocationDb}; use geoffrey_models::models::parameters::set_portal_params::SetPortalParams; use geoffrey_models::models::player::Player; +use geoffrey_models::models::response::api_error::GeoffreyAPIError; use geoffrey_models::models::CommandLevel; use std::sync::Arc; @@ -29,7 +31,15 @@ impl Command for SetPortal { fn run_command(ctx: Arc, req: &Self::Req, user: Option) -> Result { let user = user.unwrap(); - let mut location = find_location_by_name(&ctx.db, &req.loc_name, user.id.unwrap())?; + let query = QueryBuilder::::default() + .with_name(&req.loc_name) + .with_owner(user.id.unwrap()); + + let mut location = ctx + .db + .run_query(query)? + .pop() + .ok_or(GeoffreyAPIError::EntryNotFound)?; location.portal = Some(req.portal.clone()); diff --git a/geoffrey_bot/src/bot/commands/remove_item.rs b/geoffrey_bot/src/bot/commands/remove_item.rs index 0b4cd0b..9a28363 100644 --- a/geoffrey_bot/src/bot/commands/remove_item.rs +++ b/geoffrey_bot/src/bot/commands/remove_item.rs @@ -5,7 +5,7 @@ use serenity::model::interactions::application_command::{ }; use geoffrey_models::models::locations::Location; -use geoffrey_models::models::parameters::remove_item_params::RemoveItemParameters; +use geoffrey_models::models::parameters::item_command_params::ItemCommandParams; use crate::bot::arg_parse::option_to_string; use crate::bot::commands::{BotCommand, CommandError}; @@ -18,7 +18,7 @@ pub struct RemoveItemCommand; #[async_trait] impl BotCommand for RemoveItemCommand { - type ApiParams = RemoveItemParameters; + type ApiParams = ItemCommandParams; type ApiResp = Location; fn command_name() -> String { diff --git a/geoffrey_bot/src/bot/commands/restock.rs b/geoffrey_bot/src/bot/commands/restock.rs index 2e84ebf..52b34c3 100644 --- a/geoffrey_bot/src/bot/commands/restock.rs +++ b/geoffrey_bot/src/bot/commands/restock.rs @@ -10,7 +10,7 @@ use crate::bot::arg_parse::option_to_string; use crate::bot::commands::{BotCommand, CommandError}; use crate::bot::formatters::display_loc_full; use crate::bot::lang::PLAYER_DOES_NOT_HAVE_MATCHING_SHOP; -use geoffrey_models::models::parameters::restock_params::RestockParameters; +use geoffrey_models::models::parameters::item_command_params::ItemCommandParams; use geoffrey_models::models::response::api_error::GeoffreyAPIError; use serenity::builder::CreateApplicationCommand; @@ -18,7 +18,7 @@ pub struct RestockCommand; #[async_trait] impl BotCommand for RestockCommand { - type ApiParams = RestockParameters; + type ApiParams = ItemCommandParams; type ApiResp = Location; fn command_name() -> String { diff --git a/geoffrey_db/src/database.rs b/geoffrey_db/src/database.rs index 219bcf8..6c0a2e4 100644 --- a/geoffrey_db/src/database.rs +++ b/geoffrey_db/src/database.rs @@ -1,5 +1,6 @@ use crate::error::{GeoffreyDBError, Result}; use crate::migration::do_migration; +use crate::query::QueryBuilder; use geoffrey_models::models::db_metadata::DBMetadata; use geoffrey_models::GeoffreyDatabaseModel; use sled::IVec; @@ -113,6 +114,31 @@ impl Database { })) } + pub fn run_query(&self, query_builder: QueryBuilder) -> Result> + where + T: GeoffreyDatabaseModel, + { + let result: Vec = self + .filter(|id, loc: &T| { + for query in &query_builder.queries { + let res = query(id, loc); + + if !res { + return false; + } + } + + true + })? + .collect(); + + if result.is_empty() { + Err(GeoffreyDBError::NotFound) + } else { + Ok(result) + } + } + pub fn remove(&self, id: u64) -> Result where T: GeoffreyDatabaseModel, @@ -145,29 +171,14 @@ impl Database { #[cfg(test)] mod tests { - use crate::database::Database; + use crate::test::{cleanup, DB, LOCK}; use geoffrey_models::models::locations::shop::Shop; use geoffrey_models::models::locations::{LocationDataDb, LocationDb}; use geoffrey_models::models::player::{Player, UserID}; use geoffrey_models::models::{Dimension, Position}; use geoffrey_models::GeoffreyDatabaseModel; - use lazy_static::lazy_static; - use std::path::Path; - use std::sync::Mutex; use std::time::Instant; - lazy_static! { - static ref DB: Database = Database::new(Path::new("../test_database")).unwrap(); - static ref LOCK: Mutex<()> = Mutex::default(); - } - - fn cleanup() { - DB.clear_tree::().unwrap(); - DB.clear_tree::().unwrap(); - DB.db.clear().unwrap(); - DB.db.flush().unwrap(); - } - #[test] fn test_insert() { let _lock = LOCK.lock().unwrap(); diff --git a/geoffrey_db/src/error.rs b/geoffrey_db/src/error.rs index 3df03ac..fcca2b6 100644 --- a/geoffrey_db/src/error.rs +++ b/geoffrey_db/src/error.rs @@ -8,6 +8,7 @@ pub enum GeoffreyDBError { SerdeJsonError(serde_json::Error), NotUnique, NotFound, + RegexError(regex::Error), } impl std::error::Error for GeoffreyDBError {} @@ -19,6 +20,7 @@ impl std::fmt::Display for GeoffreyDBError { GeoffreyDBError::SerdeJsonError(e) => write!(f, "Serde JSON Error: {}", e), GeoffreyDBError::NotUnique => write!(f, "Entry is not unique."), GeoffreyDBError::NotFound => write!(f, "Entry was not found."), + GeoffreyDBError::RegexError(e) => write!(f, "Regex Error: {}", e), } } } @@ -35,6 +37,12 @@ impl From for GeoffreyDBError { } } +impl From for GeoffreyDBError { + fn from(e: regex::Error) -> Self { + Self::RegexError(e) + } +} + impl From for GeoffreyAPIError { fn from(e: GeoffreyDBError) -> Self { match e { @@ -42,6 +50,7 @@ impl From for GeoffreyAPIError { GeoffreyDBError::SerdeJsonError(_) => GeoffreyAPIError::DatabaseError(e.to_string()), GeoffreyDBError::NotUnique => GeoffreyAPIError::EntryNotUnique, GeoffreyDBError::NotFound => GeoffreyAPIError::EntryNotFound, + GeoffreyDBError::RegexError(_) => GeoffreyAPIError::InvalidRegex(e.to_string()), } } } diff --git a/geoffrey_db/src/lib.rs b/geoffrey_db/src/lib.rs index 267c6eb..0f40950 100644 --- a/geoffrey_db/src/lib.rs +++ b/geoffrey_db/src/lib.rs @@ -4,3 +4,26 @@ pub mod database; pub mod error; pub mod helper; pub(crate) mod migration; +pub mod query; + +#[cfg(test)] +mod test { + use crate::database::Database; + use geoffrey_models::models::locations::LocationDb; + use geoffrey_models::models::player::Player; + use lazy_static::lazy_static; + use std::path::Path; + use std::sync::Mutex; + + lazy_static! { + pub static ref DB: Database = Database::new(Path::new("../test_database")).unwrap(); + pub static ref LOCK: Mutex<()> = Mutex::default(); + } + + pub fn cleanup() { + DB.clear_tree::().unwrap(); + DB.clear_tree::().unwrap(); + DB.db.clear().unwrap(); + DB.db.flush().unwrap(); + } +} diff --git a/geoffrey_db/src/query/location_query.rs b/geoffrey_db/src/query/location_query.rs new file mode 100644 index 0000000..338d41c --- /dev/null +++ b/geoffrey_db/src/query/location_query.rs @@ -0,0 +1,144 @@ +use crate::error::Result; +use crate::query::QueryBuilder; +use geoffrey_models::models::locations::{LocationDb, LocationType}; + +impl QueryBuilder { + pub fn with_owner(self, owner_id: u64) -> Self { + self.add_query_clause(Box::new(move |_: u64, loc: &LocationDb| { + loc.owners().contains(&owner_id) + })) + } + + pub fn with_name_regex(self, exp: &str) -> Result { + let filter = regex::Regex::new(exp)?; + + Ok( + self.add_query_clause(Box::new(move |_: u64, loc: &LocationDb| { + filter.is_match(&loc.name) + })), + ) + } + + pub fn with_name(self, name: &str) -> Self { + let name = name.to_lowercase(); + self.add_query_clause(Box::new(move |_: u64, loc: &LocationDb| { + loc.name.to_lowercase() == name + })) + } + + pub fn with_type(self, loc_type: LocationType) -> Self { + self.add_query_clause(Box::new(move |_: u64, loc: &LocationDb| { + let next_loc_type: LocationType = loc.loc_data.clone().into(); + + loc_type == next_loc_type + })) + } +} + +#[cfg(test)] +mod test { + use crate::query::location_query::QueryBuilder; + use crate::test::{cleanup, DB, LOCK}; + use geoffrey_models::models::locations::shop::Shop; + use geoffrey_models::models::locations::{LocationDataDb, LocationDb, LocationType}; + use geoffrey_models::models::player::{Player, UserID}; + use geoffrey_models::models::Position; + + fn setup_db() -> (Player, LocationDb, LocationDb) { + let player = DB + .insert(Player::new("Test", UserID::DiscordUUID { discord_uuid: 5 })) + .unwrap(); + let base = DB + .insert(LocationDb::new( + "Test Base", + Position::default(), + player.id.unwrap(), + None, + LocationDataDb::Base, + )) + .unwrap(); + let shop = DB + .insert(LocationDb::new( + "Test Shop", + Position::default(), + player.id.unwrap(), + None, + LocationDataDb::Shop(Shop::default()), + )) + .unwrap(); + + (player, base, shop) + } + + #[test] + fn test_with_owner_query() { + let _lock = LOCK.lock().unwrap(); + cleanup(); + let (player, base, _) = setup_db(); + + let query: QueryBuilder = QueryBuilder::new(); + + let query = query.with_owner(55); + + assert!(DB.run_query(query).is_err()); + + let query: QueryBuilder = QueryBuilder::new(); + let query = query.with_owner(player.id.unwrap()); + + let locations: Vec = DB.run_query(query).unwrap(); + assert_eq!(locations.len(), 2); + assert_eq!(locations[0].name, base.name); + } + + #[test] + fn test_with_type_query() { + let _lock = LOCK.lock().unwrap(); + cleanup(); + let (_, base, shop) = setup_db(); + + let query: QueryBuilder = QueryBuilder::new(); + + let query = query.with_type(LocationType::Base); + + let locations: Vec = DB.run_query(query).unwrap(); + assert_eq!(locations.len(), 1); + assert_eq!(locations[0].name, base.name); + + let query: QueryBuilder = QueryBuilder::new(); + let query = query.with_type(LocationType::Shop); + + let locations: Vec = DB.run_query(query).unwrap(); + assert_eq!(locations.len(), 1); + assert_eq!(locations[0].name, shop.name); + } + + #[test] + fn test_with_name_query() { + let _lock = LOCK.lock().unwrap(); + cleanup(); + let (_, base, _) = setup_db(); + + let query: QueryBuilder = QueryBuilder::new(); + + let query = query.with_name("Test Base"); + + let locations: Vec = DB.run_query(query).unwrap(); + assert_eq!(locations.len(), 1); + assert_eq!(locations[0].name, base.name); + } + + #[test] + fn test_with_name_regex_query() { + let _lock = LOCK.lock().unwrap(); + cleanup(); + let (_, base, _) = setup_db(); + + let query: QueryBuilder = QueryBuilder::new(); + + let query = query.with_name_regex("Test").unwrap(); + + let locations: Vec = DB.run_query(query).unwrap(); + assert_eq!(locations.len(), 2); + assert_eq!(locations[0].name, base.name); + } +} diff --git a/geoffrey_db/src/query/mod.rs b/geoffrey_db/src/query/mod.rs new file mode 100644 index 0000000..975f6cb --- /dev/null +++ b/geoffrey_db/src/query/mod.rs @@ -0,0 +1,33 @@ +use geoffrey_models::GeoffreyDatabaseModel; + +pub mod location_query; +pub mod player_query; + +pub type GeoffreyDBQuery = Box bool>; + +pub struct QueryBuilder { + pub queries: Vec>, +} + +impl Default for QueryBuilder { + fn default() -> Self { + Self::new() + } +} + +impl QueryBuilder { + pub fn new() -> Self { + QueryBuilder { + queries: Vec::new(), + } + } + + pub fn add_query_clause(mut self, clause: GeoffreyDBQuery) -> Self { + self.queries.push(clause); + self + } + + pub fn with_id(self, id: u64) -> Self { + self.add_query_clause(Box::new(move |entry_id, _| entry_id == id)) + } +} diff --git a/geoffrey_db/src/query/player_query.rs b/geoffrey_db/src/query/player_query.rs new file mode 100644 index 0000000..41321e3 --- /dev/null +++ b/geoffrey_db/src/query/player_query.rs @@ -0,0 +1,101 @@ +use crate::error::Result; +use crate::query::QueryBuilder; +use geoffrey_models::models::player::{Player, UserID}; + +impl QueryBuilder { + pub fn with_name_regex(self, exp: &str) -> Result { + let filter = regex::Regex::new(exp)?; + + Ok( + self.add_query_clause(Box::new(move |_: u64, player: &Player| { + filter.is_match(&player.name) + })), + ) + } + + pub fn with_name(self, name: &str) -> Self { + let name = name.to_lowercase(); + self.add_query_clause(Box::new(move |_: u64, player: &Player| { + player.name.to_lowercase() == name + })) + } + + pub fn with_user_id(self, user_id: UserID) -> Self { + self.add_query_clause(Box::new(move |_: u64, player: &Player| { + player.has_user_id(&user_id) + })) + } +} + +#[cfg(test)] +mod test { + use crate::query::QueryBuilder; + use crate::test::{cleanup, DB, LOCK}; + use geoffrey_models::models::player::{Player, UserID}; + + fn setup_db() -> (Player, Player, Player) { + let player1 = DB + .insert(Player::new( + "ZeroHD", + UserID::DiscordUUID { discord_uuid: 0 }, + )) + .unwrap(); + let player2 = DB + .insert(Player::new( + "Vakbezel", + UserID::MinecraftUUID { + mc_uuid: "0000".to_string(), + }, + )) + .unwrap(); + let player3 = DB + .insert(Player::new( + "Etzelia", + UserID::DiscordUUID { discord_uuid: 5 }, + )) + .unwrap(); + + (player1, player2, player3) + } + + #[test] + fn test_with_name() { + let _lock = LOCK.lock().unwrap(); + cleanup(); + let (player1, player2, player3) = setup_db(); + + let query: QueryBuilder = QueryBuilder::new(); + let query = query.with_name("ZeroHD"); + assert_eq!(DB.run_query(query).unwrap()[0].name, player1.name); + + let query: QueryBuilder = QueryBuilder::new(); + let query = query.with_name("Vakbezel"); + assert_eq!(DB.run_query(query).unwrap()[0].name, player2.name); + + let query: QueryBuilder = QueryBuilder::new(); + let query = query.with_name("Etzelia"); + assert_eq!(DB.run_query(query).unwrap()[0].name, player3.name); + } + + #[test] + fn test_with_user_id() { + let _lock = LOCK.lock().unwrap(); + cleanup(); + let (player1, player2, player3) = setup_db(); + + let query: QueryBuilder = QueryBuilder::new(); + let query = query.with_user_id(UserID::DiscordUUID { discord_uuid: 0 }); + assert_eq!(DB.run_query(query).unwrap()[0].name, player1.name); + + let query: QueryBuilder = QueryBuilder::new(); + let query = query.with_user_id(UserID::MinecraftUUID { + mc_uuid: "0000".to_string(), + }); + let query = query.with_name("Vakbezel"); + assert_eq!(DB.run_query(query).unwrap()[0].name, player2.name); + + let query: QueryBuilder = QueryBuilder::new(); + let query = query.with_user_id(UserID::DiscordUUID { discord_uuid: 5 }); + assert_eq!(DB.run_query(query).unwrap()[0].name, player3.name); + } +} diff --git a/geoffrey_models/src/models/parameters/restock_params.rs b/geoffrey_models/src/models/parameters/item_command_params.rs similarity index 76% rename from geoffrey_models/src/models/parameters/restock_params.rs rename to geoffrey_models/src/models/parameters/item_command_params.rs index 2c695c5..e5e3d4a 100644 --- a/geoffrey_models/src/models/parameters/restock_params.rs +++ b/geoffrey_models/src/models/parameters/item_command_params.rs @@ -2,12 +2,12 @@ use crate::models::parameters::GeoffreyParam; use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize, Clone)] -pub struct RestockParameters { +pub struct ItemCommandParams { pub shop_name: String, pub item_name: String, } -impl RestockParameters { +impl ItemCommandParams { pub fn new(shop_name: String, item_name: String) -> Self { Self { shop_name, @@ -16,4 +16,4 @@ impl RestockParameters { } } -impl GeoffreyParam for RestockParameters {} +impl GeoffreyParam for ItemCommandParams {} diff --git a/geoffrey_models/src/models/parameters/mod.rs b/geoffrey_models/src/models/parameters/mod.rs index e6a7056..ef425c2 100644 --- a/geoffrey_models/src/models/parameters/mod.rs +++ b/geoffrey_models/src/models/parameters/mod.rs @@ -5,10 +5,9 @@ pub mod delete_params; pub mod edit_params; pub mod find_params; pub mod info_params; +pub mod item_command_params; pub mod link_params; pub mod register_params; -pub mod remove_item_params; -pub mod restock_params; pub mod selling_params; pub mod set_portal_params; diff --git a/geoffrey_models/src/models/parameters/remove_item_params.rs b/geoffrey_models/src/models/parameters/remove_item_params.rs deleted file mode 100644 index a43349f..0000000 --- a/geoffrey_models/src/models/parameters/remove_item_params.rs +++ /dev/null @@ -1,19 +0,0 @@ -use crate::models::parameters::GeoffreyParam; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct RemoveItemParameters { - pub shop_name: String, - pub item_name: String, -} - -impl RemoveItemParameters { - pub fn new(shop_name: String, item_name: String) -> Self { - Self { - shop_name, - item_name, - } - } -} - -impl GeoffreyParam for RemoveItemParameters {} diff --git a/geoffrey_models/src/models/response/api_error.rs b/geoffrey_models/src/models/response/api_error.rs index 94e85c0..1c7d3e8 100644 --- a/geoffrey_models/src/models/response/api_error.rs +++ b/geoffrey_models/src/models/response/api_error.rs @@ -13,6 +13,7 @@ pub enum GeoffreyAPIError { ParameterInvalid(String), PlayerRegistrationWithoutMCUUID, AccountLinkInvalid, + InvalidRegex(String), } impl Display for GeoffreyAPIError { @@ -42,6 +43,9 @@ impl Display for GeoffreyAPIError { GeoffreyAPIError::AccountLinkInvalid => { "The supplied account link code is invalid".to_string() } + GeoffreyAPIError::InvalidRegex(err) => { + format!("Invalid regex supplied: {}", err) + } }; write!(f, "{}", string) }