report_out_of_stock command + new query system
continuous-integration/woodpecker the build was successful Details

+ 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
main
Joey Hines 2021-12-30 21:48:33 -06:00
parent 928e59a700
commit c6ec8467c1
Signed by: joeyahines
GPG Key ID: 0C681E6AED894AE4
21 changed files with 510 additions and 86 deletions

View File

@ -3,9 +3,10 @@ use crate::config::GeoffreyAPIConfig;
use crate::context::Context; use crate::context::Context;
use crate::helper::validate_string_parameter; use crate::helper::validate_string_parameter;
use crate::Result; 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::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::parameters::add_item_params::AddItemParams;
use geoffrey_models::models::player::Player; use geoffrey_models::models::player::Player;
use geoffrey_models::models::response::api_error::GeoffreyAPIError; use geoffrey_models::models::response::api_error::GeoffreyAPIError;
@ -33,8 +34,16 @@ impl Command for AddItem {
fn run_command(ctx: Arc<Context>, req: &Self::Req, user: Option<Player>) -> Result<Self::Resp> { fn run_command(ctx: Arc<Context>, req: &Self::Req, user: Option<Player>) -> Result<Self::Resp> {
let user = user.unwrap(); let user = user.unwrap();
let mut shop = let query = QueryBuilder::<LocationDb>::default()
find_location_by_name_type(&ctx.db, &req.shop, user.id.unwrap(), LocationType::Shop)?; .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 { if let LocationDataDb::Shop(shop_data) = &mut shop.loc_data {
shop_data.item_listings.insert(ItemListing::new( shop_data.item_listings.insert(ItemListing::new(

View File

@ -1,7 +1,8 @@
use crate::commands::{Command, RequestType}; use crate::commands::{Command, RequestType};
use crate::context::Context; use crate::context::Context;
use crate::Result; 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::locations::{Location, LocationDb};
use geoffrey_models::models::parameters::delete_params::DeleteParams; use geoffrey_models::models::parameters::delete_params::DeleteParams;
use geoffrey_models::models::player::Player; use geoffrey_models::models::player::Player;
@ -30,7 +31,16 @@ impl Command for Delete {
fn run_command(ctx: Arc<Context>, req: &Self::Req, user: Option<Player>) -> Result<Self::Resp> { fn run_command(ctx: Arc<Context>, req: &Self::Req, user: Option<Player>) -> Result<Self::Resp> {
let user = user.unwrap(); let user = user.unwrap();
let location = find_location_by_name(&ctx.db, &req.location, user.id.unwrap())?; let query = QueryBuilder::<LocationDb>::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)?; let location = load_location(&ctx.db, &location).map_err(GeoffreyAPIError::from)?;
ctx.db.remove::<LocationDb>(location.id)?; ctx.db.remove::<LocationDb>(location.id)?;

View File

@ -1,10 +1,12 @@
use crate::commands::{Command, RequestType}; use crate::commands::{Command, RequestType};
use crate::context::Context; use crate::context::Context;
use crate::Result; use crate::Result;
use geoffrey_db::helper::{find_location_by_name, load_location}; use geoffrey_db::helper::load_location;
use geoffrey_models::models::locations::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::parameters::edit_params::EditParams;
use geoffrey_models::models::player::Player; use geoffrey_models::models::player::Player;
use geoffrey_models::models::response::api_error::GeoffreyAPIError;
use geoffrey_models::models::CommandLevel; use geoffrey_models::models::CommandLevel;
use std::sync::Arc; use std::sync::Arc;
@ -29,7 +31,15 @@ impl Command for Edit {
fn run_command(ctx: Arc<Context>, req: &Self::Req, user: Option<Player>) -> Result<Self::Resp> { fn run_command(ctx: Arc<Context>, req: &Self::Req, user: Option<Player>) -> Result<Self::Resp> {
let user = user.unwrap(); let user = user.unwrap();
let mut location = find_location_by_name(&ctx.db, &req.loc_name, user.id.unwrap())?; let query = QueryBuilder::<LocationDb>::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 { if let Some(new_pos) = &req.new_pos {
location.position = *new_pos; location.position = *new_pos;

View File

@ -2,6 +2,7 @@ use crate::commands::{Command, RequestType};
use crate::context::Context; use crate::context::Context;
use crate::Result; use crate::Result;
use geoffrey_db::helper::load_location; use geoffrey_db::helper::load_location;
use geoffrey_db::query::QueryBuilder;
use geoffrey_models::models::locations::{Location, LocationDb}; use geoffrey_models::models::locations::{Location, LocationDb};
use geoffrey_models::models::parameters::info_params::InfoParams; use geoffrey_models::models::parameters::info_params::InfoParams;
use geoffrey_models::models::player::Player; use geoffrey_models::models::player::Player;
@ -28,17 +29,12 @@ impl Command for InfoCommand {
} }
fn run_command(ctx: Arc<Context>, req: &Self::Req, _: Option<Player>) -> Result<Self::Resp> { fn run_command(ctx: Arc<Context>, req: &Self::Req, _: Option<Player>) -> Result<Self::Resp> {
let query = req.location_name.to_lowercase(); let query = QueryBuilder::<LocationDb>::default().with_name_regex(&req.location_name)?;
let location: LocationDb = ctx let location = ctx
.db .db
.filter(|_, loc: &LocationDb| { .run_query(query)?
let name = loc.name.to_lowercase(); .pop()
name == query
})
.map_err(GeoffreyAPIError::from)?
.next()
.ok_or(GeoffreyAPIError::EntryNotFound)?; .ok_or(GeoffreyAPIError::EntryNotFound)?;
Ok(load_location(&ctx.db, &location)?) Ok(load_location(&ctx.db, &location)?)

View File

@ -7,6 +7,7 @@ use crate::commands::info::InfoCommand;
use crate::commands::link::LinkCommand; use crate::commands::link::LinkCommand;
use crate::commands::register::Register; use crate::commands::register::Register;
use crate::commands::remove_item::RemoveItem; use crate::commands::remove_item::RemoveItem;
use crate::commands::report_out_of_stock::ReportOutOfStock;
use crate::commands::restock::Restock; use crate::commands::restock::Restock;
use crate::commands::selling::Selling; use crate::commands::selling::Selling;
use crate::commands::set_portal::SetPortal; use crate::commands::set_portal::SetPortal;
@ -37,6 +38,7 @@ pub mod info;
pub mod link; pub mod link;
pub mod register; pub mod register;
pub mod remove_item; pub mod remove_item;
pub mod report_out_of_stock;
pub mod restock; pub mod restock;
pub mod selling; pub mod selling;
pub mod set_portal; pub mod set_portal;
@ -147,6 +149,7 @@ pub fn command_filter(
.or(create_command_filter::<RemoveItem>(ctx.clone())) .or(create_command_filter::<RemoveItem>(ctx.clone()))
.or(create_command_filter::<InfoCommand>(ctx.clone())) .or(create_command_filter::<InfoCommand>(ctx.clone()))
.or(create_command_filter::<Restock>(ctx.clone())) .or(create_command_filter::<Restock>(ctx.clone()))
.or(create_command_filter::<ReportOutOfStock>(ctx.clone()))
.or(create_command_filter::<SetPortal>(ctx)), .or(create_command_filter::<SetPortal>(ctx)),
) )
} }

View File

@ -1,9 +1,10 @@
use crate::commands::{Command, RequestType}; use crate::commands::{Command, RequestType};
use crate::context::Context; use crate::context::Context;
use crate::Result; use crate::Result;
use geoffrey_db::helper::{find_location_by_name_type, load_location}; use geoffrey_db::helper::load_location;
use geoffrey_models::models::locations::{Location, LocationDataDb, LocationType}; use geoffrey_db::query::QueryBuilder;
use geoffrey_models::models::parameters::remove_item_params::RemoveItemParameters; 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::player::Player;
use geoffrey_models::models::response::api_error::GeoffreyAPIError; use geoffrey_models::models::response::api_error::GeoffreyAPIError;
use geoffrey_models::models::CommandLevel; use geoffrey_models::models::CommandLevel;
@ -12,7 +13,7 @@ use std::sync::Arc;
pub struct RemoveItem {} pub struct RemoveItem {}
impl Command for RemoveItem { impl Command for RemoveItem {
type Req = RemoveItemParameters; type Req = ItemCommandParams;
type Resp = Location; type Resp = Location;
fn command_name() -> String { fn command_name() -> String {
@ -30,12 +31,16 @@ impl Command for RemoveItem {
fn run_command(ctx: Arc<Context>, req: &Self::Req, user: Option<Player>) -> Result<Self::Resp> { fn run_command(ctx: Arc<Context>, req: &Self::Req, user: Option<Player>) -> Result<Self::Resp> {
let user = user.unwrap(); let user = user.unwrap();
let mut shop = find_location_by_name_type( let query = QueryBuilder::<LocationDb>::default()
&ctx.db, .with_type(LocationType::Shop)
&req.shop_name, .with_name(&req.shop_name)
user.id.unwrap(), .with_owner(user.id.unwrap());
LocationType::Shop,
)?; let mut shop = ctx
.db
.run_query(query)?
.pop()
.ok_or(GeoffreyAPIError::EntryNotFound)?;
if let LocationDataDb::Shop(shop_data) = &mut shop.loc_data { if let LocationDataDb::Shop(shop_data) = &mut shop.loc_data {
shop_data shop_data

View File

@ -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<Context>, req: &Self::Req, user: Option<Player>) -> Result<Self::Resp> {
let user = user.unwrap();
let query = QueryBuilder::<LocationDb>::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<ItemListing> = 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)
}
}
}

View File

@ -1,10 +1,11 @@
use crate::commands::{Command, RequestType}; use crate::commands::{Command, RequestType};
use crate::context::Context; use crate::context::Context;
use crate::Result; 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::item::ItemListing;
use geoffrey_models::models::locations::{Location, LocationDataDb, LocationType}; use geoffrey_models::models::locations::{Location, LocationDataDb, LocationDb, LocationType};
use geoffrey_models::models::parameters::restock_params::RestockParameters; use geoffrey_models::models::parameters::item_command_params::ItemCommandParams;
use geoffrey_models::models::player::Player; use geoffrey_models::models::player::Player;
use geoffrey_models::models::response::api_error::GeoffreyAPIError; use geoffrey_models::models::response::api_error::GeoffreyAPIError;
use geoffrey_models::models::CommandLevel; use geoffrey_models::models::CommandLevel;
@ -14,7 +15,7 @@ use std::sync::Arc;
pub struct Restock {} pub struct Restock {}
impl Command for Restock { impl Command for Restock {
type Req = RestockParameters; type Req = ItemCommandParams;
type Resp = Location; type Resp = Location;
fn command_name() -> String { fn command_name() -> String {
@ -32,16 +33,20 @@ impl Command for Restock {
fn run_command(ctx: Arc<Context>, req: &Self::Req, user: Option<Player>) -> Result<Self::Resp> { fn run_command(ctx: Arc<Context>, req: &Self::Req, user: Option<Player>) -> Result<Self::Resp> {
let user = user.unwrap(); let user = user.unwrap();
let mut shop = find_location_by_name_type( let query = QueryBuilder::<LocationDb>::default()
&ctx.db, .with_type(LocationType::Shop)
&req.shop_name, .with_name(&req.shop_name)
user.id.unwrap(), .with_owner(user.id.unwrap());
LocationType::Shop,
)?; let mut shop = ctx
.db
.run_query(query)?
.pop()
.ok_or(GeoffreyAPIError::EntryNotFound)?;
if let LocationDataDb::Shop(shop_data) = &mut shop.loc_data { if let LocationDataDb::Shop(shop_data) = &mut shop.loc_data {
let filter = regex::Regex::new(&req.item_name) 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<ItemListing> = shop_data let updated_items: HashSet<ItemListing> = shop_data
.item_listings .item_listings

View File

@ -1,10 +1,12 @@
use crate::commands::{Command, RequestType}; use crate::commands::{Command, RequestType};
use crate::context::Context; use crate::context::Context;
use crate::Result; use crate::Result;
use geoffrey_db::helper::{find_location_by_name, load_location}; use geoffrey_db::helper::load_location;
use geoffrey_models::models::locations::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::parameters::set_portal_params::SetPortalParams;
use geoffrey_models::models::player::Player; use geoffrey_models::models::player::Player;
use geoffrey_models::models::response::api_error::GeoffreyAPIError;
use geoffrey_models::models::CommandLevel; use geoffrey_models::models::CommandLevel;
use std::sync::Arc; use std::sync::Arc;
@ -29,7 +31,15 @@ impl Command for SetPortal {
fn run_command(ctx: Arc<Context>, req: &Self::Req, user: Option<Player>) -> Result<Self::Resp> { fn run_command(ctx: Arc<Context>, req: &Self::Req, user: Option<Player>) -> Result<Self::Resp> {
let user = user.unwrap(); let user = user.unwrap();
let mut location = find_location_by_name(&ctx.db, &req.loc_name, user.id.unwrap())?; let query = QueryBuilder::<LocationDb>::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()); location.portal = Some(req.portal.clone());

View File

@ -5,7 +5,7 @@ use serenity::model::interactions::application_command::{
}; };
use geoffrey_models::models::locations::Location; 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::arg_parse::option_to_string;
use crate::bot::commands::{BotCommand, CommandError}; use crate::bot::commands::{BotCommand, CommandError};
@ -18,7 +18,7 @@ pub struct RemoveItemCommand;
#[async_trait] #[async_trait]
impl BotCommand for RemoveItemCommand { impl BotCommand for RemoveItemCommand {
type ApiParams = RemoveItemParameters; type ApiParams = ItemCommandParams;
type ApiResp = Location; type ApiResp = Location;
fn command_name() -> String { fn command_name() -> String {

View File

@ -10,7 +10,7 @@ use crate::bot::arg_parse::option_to_string;
use crate::bot::commands::{BotCommand, CommandError}; use crate::bot::commands::{BotCommand, CommandError};
use crate::bot::formatters::display_loc_full; use crate::bot::formatters::display_loc_full;
use crate::bot::lang::PLAYER_DOES_NOT_HAVE_MATCHING_SHOP; 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 geoffrey_models::models::response::api_error::GeoffreyAPIError;
use serenity::builder::CreateApplicationCommand; use serenity::builder::CreateApplicationCommand;
@ -18,7 +18,7 @@ pub struct RestockCommand;
#[async_trait] #[async_trait]
impl BotCommand for RestockCommand { impl BotCommand for RestockCommand {
type ApiParams = RestockParameters; type ApiParams = ItemCommandParams;
type ApiResp = Location; type ApiResp = Location;
fn command_name() -> String { fn command_name() -> String {

View File

@ -1,5 +1,6 @@
use crate::error::{GeoffreyDBError, Result}; use crate::error::{GeoffreyDBError, Result};
use crate::migration::do_migration; use crate::migration::do_migration;
use crate::query::QueryBuilder;
use geoffrey_models::models::db_metadata::DBMetadata; use geoffrey_models::models::db_metadata::DBMetadata;
use geoffrey_models::GeoffreyDatabaseModel; use geoffrey_models::GeoffreyDatabaseModel;
use sled::IVec; use sled::IVec;
@ -113,6 +114,31 @@ impl Database {
})) }))
} }
pub fn run_query<T>(&self, query_builder: QueryBuilder<T>) -> Result<Vec<T>>
where
T: GeoffreyDatabaseModel,
{
let result: Vec<T> = 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<T>(&self, id: u64) -> Result<T> pub fn remove<T>(&self, id: u64) -> Result<T>
where where
T: GeoffreyDatabaseModel, T: GeoffreyDatabaseModel,
@ -145,29 +171,14 @@ impl Database {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::database::Database; use crate::test::{cleanup, DB, LOCK};
use geoffrey_models::models::locations::shop::Shop; use geoffrey_models::models::locations::shop::Shop;
use geoffrey_models::models::locations::{LocationDataDb, LocationDb}; use geoffrey_models::models::locations::{LocationDataDb, LocationDb};
use geoffrey_models::models::player::{Player, UserID}; use geoffrey_models::models::player::{Player, UserID};
use geoffrey_models::models::{Dimension, Position}; use geoffrey_models::models::{Dimension, Position};
use geoffrey_models::GeoffreyDatabaseModel; use geoffrey_models::GeoffreyDatabaseModel;
use lazy_static::lazy_static;
use std::path::Path;
use std::sync::Mutex;
use std::time::Instant; 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::<Player>().unwrap();
DB.clear_tree::<LocationDb>().unwrap();
DB.db.clear().unwrap();
DB.db.flush().unwrap();
}
#[test] #[test]
fn test_insert() { fn test_insert() {
let _lock = LOCK.lock().unwrap(); let _lock = LOCK.lock().unwrap();

View File

@ -8,6 +8,7 @@ pub enum GeoffreyDBError {
SerdeJsonError(serde_json::Error), SerdeJsonError(serde_json::Error),
NotUnique, NotUnique,
NotFound, NotFound,
RegexError(regex::Error),
} }
impl std::error::Error for GeoffreyDBError {} 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::SerdeJsonError(e) => write!(f, "Serde JSON Error: {}", e),
GeoffreyDBError::NotUnique => write!(f, "Entry is not unique."), GeoffreyDBError::NotUnique => write!(f, "Entry is not unique."),
GeoffreyDBError::NotFound => write!(f, "Entry was not found."), GeoffreyDBError::NotFound => write!(f, "Entry was not found."),
GeoffreyDBError::RegexError(e) => write!(f, "Regex Error: {}", e),
} }
} }
} }
@ -35,6 +37,12 @@ impl From<serde_json::Error> for GeoffreyDBError {
} }
} }
impl From<regex::Error> for GeoffreyDBError {
fn from(e: regex::Error) -> Self {
Self::RegexError(e)
}
}
impl From<GeoffreyDBError> for GeoffreyAPIError { impl From<GeoffreyDBError> for GeoffreyAPIError {
fn from(e: GeoffreyDBError) -> Self { fn from(e: GeoffreyDBError) -> Self {
match e { match e {
@ -42,6 +50,7 @@ impl From<GeoffreyDBError> for GeoffreyAPIError {
GeoffreyDBError::SerdeJsonError(_) => GeoffreyAPIError::DatabaseError(e.to_string()), GeoffreyDBError::SerdeJsonError(_) => GeoffreyAPIError::DatabaseError(e.to_string()),
GeoffreyDBError::NotUnique => GeoffreyAPIError::EntryNotUnique, GeoffreyDBError::NotUnique => GeoffreyAPIError::EntryNotUnique,
GeoffreyDBError::NotFound => GeoffreyAPIError::EntryNotFound, GeoffreyDBError::NotFound => GeoffreyAPIError::EntryNotFound,
GeoffreyDBError::RegexError(_) => GeoffreyAPIError::InvalidRegex(e.to_string()),
} }
} }
} }

View File

@ -4,3 +4,26 @@ pub mod database;
pub mod error; pub mod error;
pub mod helper; pub mod helper;
pub(crate) mod migration; 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::<Player>().unwrap();
DB.clear_tree::<LocationDb>().unwrap();
DB.db.clear().unwrap();
DB.db.flush().unwrap();
}
}

View File

@ -0,0 +1,144 @@
use crate::error::Result;
use crate::query::QueryBuilder;
use geoffrey_models::models::locations::{LocationDb, LocationType};
impl QueryBuilder<LocationDb> {
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<Self> {
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<LocationDb> = QueryBuilder::new();
let query = query.with_owner(55);
assert!(DB.run_query(query).is_err());
let query: QueryBuilder<LocationDb> = QueryBuilder::new();
let query = query.with_owner(player.id.unwrap());
let locations: Vec<LocationDb> = 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<LocationDb> = QueryBuilder::new();
let query = query.with_type(LocationType::Base);
let locations: Vec<LocationDb> = DB.run_query(query).unwrap();
assert_eq!(locations.len(), 1);
assert_eq!(locations[0].name, base.name);
let query: QueryBuilder<LocationDb> = QueryBuilder::new();
let query = query.with_type(LocationType::Shop);
let locations: Vec<LocationDb> = 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<LocationDb> = QueryBuilder::new();
let query = query.with_name("Test Base");
let locations: Vec<LocationDb> = 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<LocationDb> = QueryBuilder::new();
let query = query.with_name_regex("Test").unwrap();
let locations: Vec<LocationDb> = DB.run_query(query).unwrap();
assert_eq!(locations.len(), 2);
assert_eq!(locations[0].name, base.name);
}
}

View File

@ -0,0 +1,33 @@
use geoffrey_models::GeoffreyDatabaseModel;
pub mod location_query;
pub mod player_query;
pub type GeoffreyDBQuery<T> = Box<dyn Fn(u64, &T) -> bool>;
pub struct QueryBuilder<T: GeoffreyDatabaseModel> {
pub queries: Vec<GeoffreyDBQuery<T>>,
}
impl<T: GeoffreyDatabaseModel> Default for QueryBuilder<T> {
fn default() -> Self {
Self::new()
}
}
impl<T: GeoffreyDatabaseModel> QueryBuilder<T> {
pub fn new() -> Self {
QueryBuilder {
queries: Vec::new(),
}
}
pub fn add_query_clause(mut self, clause: GeoffreyDBQuery<T>) -> 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))
}
}

View File

@ -0,0 +1,101 @@
use crate::error::Result;
use crate::query::QueryBuilder;
use geoffrey_models::models::player::{Player, UserID};
impl QueryBuilder<Player> {
pub fn with_name_regex(self, exp: &str) -> Result<Self> {
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<Player> = QueryBuilder::new();
let query = query.with_name("ZeroHD");
assert_eq!(DB.run_query(query).unwrap()[0].name, player1.name);
let query: QueryBuilder<Player> = QueryBuilder::new();
let query = query.with_name("Vakbezel");
assert_eq!(DB.run_query(query).unwrap()[0].name, player2.name);
let query: QueryBuilder<Player> = 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<Player> = 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<Player> = 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<Player> = QueryBuilder::new();
let query = query.with_user_id(UserID::DiscordUUID { discord_uuid: 5 });
assert_eq!(DB.run_query(query).unwrap()[0].name, player3.name);
}
}

View File

@ -2,12 +2,12 @@ use crate::models::parameters::GeoffreyParam;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct RestockParameters { pub struct ItemCommandParams {
pub shop_name: String, pub shop_name: String,
pub item_name: String, pub item_name: String,
} }
impl RestockParameters { impl ItemCommandParams {
pub fn new(shop_name: String, item_name: String) -> Self { pub fn new(shop_name: String, item_name: String) -> Self {
Self { Self {
shop_name, shop_name,
@ -16,4 +16,4 @@ impl RestockParameters {
} }
} }
impl GeoffreyParam for RestockParameters {} impl GeoffreyParam for ItemCommandParams {}

View File

@ -5,10 +5,9 @@ pub mod delete_params;
pub mod edit_params; pub mod edit_params;
pub mod find_params; pub mod find_params;
pub mod info_params; pub mod info_params;
pub mod item_command_params;
pub mod link_params; pub mod link_params;
pub mod register_params; pub mod register_params;
pub mod remove_item_params;
pub mod restock_params;
pub mod selling_params; pub mod selling_params;
pub mod set_portal_params; pub mod set_portal_params;

View File

@ -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 {}

View File

@ -13,6 +13,7 @@ pub enum GeoffreyAPIError {
ParameterInvalid(String), ParameterInvalid(String),
PlayerRegistrationWithoutMCUUID, PlayerRegistrationWithoutMCUUID,
AccountLinkInvalid, AccountLinkInvalid,
InvalidRegex(String),
} }
impl Display for GeoffreyAPIError { impl Display for GeoffreyAPIError {
@ -42,6 +43,9 @@ impl Display for GeoffreyAPIError {
GeoffreyAPIError::AccountLinkInvalid => { GeoffreyAPIError::AccountLinkInvalid => {
"The supplied account link code is invalid".to_string() "The supplied account link code is invalid".to_string()
} }
GeoffreyAPIError::InvalidRegex(err) => {
format!("Invalid regex supplied: {}", err)
}
}; };
write!(f, "{}", string) write!(f, "{}", string)
} }