From 8e8798f5098e6ba6abf63144ebb3eda14555e0ea Mon Sep 17 00:00:00 2001 From: Joey Hines Date: Sun, 7 Nov 2021 14:23:18 -0700 Subject: [PATCH] First pass on shop/item handling + The `add_item` command adds items to a location of type `Shop` + `selling` allows items to be queried + Items are not sorted yet + The user will be able to apply optional sorting params + Allowed the `insert` function of the DB to be used to update a model already in the DB + Clippy + Fmt --- geoffrey_api/src/commands/add_item.rs | 67 +++++++++++++++++ geoffrey_api/src/commands/mod.rs | 8 +- geoffrey_api/src/commands/selling.rs | 73 +++++++++++++++++++ geoffrey_db/src/database.rs | 4 +- geoffrey_models/src/models/item.rs | 4 +- geoffrey_models/src/models/locations/mod.rs | 2 +- .../src/models/parameters/add_item_params.rs | 23 ++++++ geoffrey_models/src/models/parameters/mod.rs | 2 + .../src/models/parameters/selling_params.rs | 33 +++++++++ .../src/models/response/api_error.rs | 4 + geoffrey_models/src/models/response/mod.rs | 1 + .../src/models/response/selling_listing.rs | 11 +++ 12 files changed, 227 insertions(+), 5 deletions(-) create mode 100644 geoffrey_api/src/commands/add_item.rs create mode 100644 geoffrey_api/src/commands/selling.rs create mode 100644 geoffrey_models/src/models/parameters/add_item_params.rs create mode 100644 geoffrey_models/src/models/parameters/selling_params.rs create mode 100644 geoffrey_models/src/models/response/selling_listing.rs diff --git a/geoffrey_api/src/commands/add_item.rs b/geoffrey_api/src/commands/add_item.rs new file mode 100644 index 0000000..afe7f61 --- /dev/null +++ b/geoffrey_api/src/commands/add_item.rs @@ -0,0 +1,67 @@ +use crate::commands::{Command, RequestType}; +use crate::context::Context; +use crate::Result; +use geoffrey_db::helper::load_location; +use geoffrey_models::models::item::ItemListing; +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; +use geoffrey_models::models::CommandLevel; +use std::sync::Arc; + +pub struct AddItem {} + +impl Command for AddItem { + type Req = AddItemParams; + type Resp = Location; + + fn command_name() -> String { + "add_item".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 shops: Vec = ctx + .db + .filter(|_, loc: &LocationDb| { + let loc_type: LocationType = loc.loc_data.clone().into(); + + loc_type == LocationType::Shop + && loc.owners().contains(&user.id.unwrap()) + && loc.name.to_lowercase().contains(&req.shop.to_lowercase()) + })? + .collect(); + + if shops.is_empty() { + Err(GeoffreyAPIError::EntryNotFound) + } else if shops.len() > 1 { + Err(GeoffreyAPIError::MultipleLocationsMatch) + } else { + let mut shop = shops[0].clone(); + + if let LocationDataDb::Shop(shop_data) = &mut shop.loc_data { + shop_data.item_listings.insert(ItemListing::new( + &req.item_name, + req.price, + req.quantity, + )); + + 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/mod.rs b/geoffrey_api/src/commands/mod.rs index 4569288..91d72d3 100644 --- a/geoffrey_api/src/commands/mod.rs +++ b/geoffrey_api/src/commands/mod.rs @@ -1,6 +1,8 @@ +use crate::commands::add_item::AddItem; use crate::commands::add_location::AddLocation; use crate::commands::find::FindCommand; use crate::commands::register::Register; +use crate::commands::selling::Selling; use crate::context::Context; use crate::helper::{get_player_from_req, get_token_from_req}; use crate::Result; @@ -17,10 +19,12 @@ use std::sync::Arc; use warp::filters::BoxedFilter; use warp::Filter; +pub mod add_item; pub mod add_location; pub mod add_token; pub mod find; pub mod register; +pub mod selling; #[derive(Debug, Clone, PartialEq)] #[allow(clippy::upper_case_acronyms)] @@ -109,6 +113,8 @@ pub fn command_filter( warp::path("command").and( create_command_filter::(ctx.clone()) .or(create_command_filter::(ctx.clone())) - .or(create_command_filter::(ctx)), + .or(create_command_filter::(ctx.clone())) + .or(create_command_filter::(ctx.clone())) + .or(create_command_filter::(ctx)), ) } diff --git a/geoffrey_api/src/commands/selling.rs b/geoffrey_api/src/commands/selling.rs new file mode 100644 index 0000000..c09ed82 --- /dev/null +++ b/geoffrey_api/src/commands/selling.rs @@ -0,0 +1,73 @@ +use crate::commands::{Command, RequestType}; +use crate::context::Context; +use crate::Result; +use geoffrey_models::models::locations::{LocationDataDb, LocationDb}; +use geoffrey_models::models::parameters::selling_params::SellingParams; +use geoffrey_models::models::player::Player; +use geoffrey_models::models::response::selling_listing::SellingListing; +use geoffrey_models::models::CommandLevel; +use std::sync::Arc; + +pub struct Selling {} + +impl Command for Selling { + type Req = SellingParams; + type Resp = Vec; + + fn command_name() -> String { + "selling".to_string() + } + + fn request_type() -> RequestType { + RequestType::GET + } + + fn command_level() -> CommandLevel { + CommandLevel::ALL + } + + fn run_command(ctx: Arc, req: Self::Req, _: Option) -> Result { + let shops: Vec = ctx + .db + .filter(|_, loc: &LocationDb| { + if let LocationDataDb::Shop(shop_data) = loc.loc_data.clone() { + shop_data.item_listings.iter().any(|item| { + item.item + .name + .to_lowercase() + .contains(&req.query.to_lowercase()) + }) + } else { + false + } + })? + .filter_map(|shop: LocationDb| { + if let LocationDataDb::Shop(shop_data) = shop.clone().loc_data { + let listings: Vec = shop_data + .item_listings + .iter() + .filter_map(|item| { + if item.item.name.to_lowercase().contains(&req.query) { + Some(SellingListing { + listing: item.clone(), + shop_name: shop.name.clone(), + shop_loc: shop.position, + portal: shop.tunnel.clone(), + }) + } else { + None + } + }) + .collect(); + + Some(listings) + } else { + None + } + }) + .flatten() + .collect(); + + Ok(shops) + } +} diff --git a/geoffrey_db/src/database.rs b/geoffrey_db/src/database.rs index fa89b54..4f5ebb7 100644 --- a/geoffrey_db/src/database.rs +++ b/geoffrey_db/src/database.rs @@ -34,7 +34,9 @@ impl Database { } }; - let match_count = self.filter(|_, o: &T| !o.check_unique(&model))?.count(); + let match_count = self + .filter(|o_id, o: &T| o_id != id && !o.check_unique(&model))? + .count(); if match_count > 0 { log::debug!("{} is not unique: {:?}", T::tree(), model); diff --git a/geoffrey_models/src/models/item.rs b/geoffrey_models/src/models/item.rs index 66cd98f..ef9ce37 100644 --- a/geoffrey_models/src/models/item.rs +++ b/geoffrey_models/src/models/item.rs @@ -23,14 +23,14 @@ pub struct ItemListing { } impl ItemListing { - fn new(item: &str, price: u32, count_per_price: u32, restocked_time: DateTime) -> Self { + pub fn new(item: &str, price: u32, count_per_price: u32) -> Self { Self { item: Item { name: item.to_string(), }, price, count_per_price, - restocked_time, + restocked_time: Utc::now(), } } } diff --git a/geoffrey_models/src/models/locations/mod.rs b/geoffrey_models/src/models/locations/mod.rs index cc0200c..22123f0 100644 --- a/geoffrey_models/src/models/locations/mod.rs +++ b/geoffrey_models/src/models/locations/mod.rs @@ -14,7 +14,7 @@ pub mod market; pub mod shop; pub mod town; -#[derive(Serialize, Deserialize, Debug, Clone, Copy)] +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialOrd, PartialEq)] pub enum LocationType { Base, Shop, diff --git a/geoffrey_models/src/models/parameters/add_item_params.rs b/geoffrey_models/src/models/parameters/add_item_params.rs new file mode 100644 index 0000000..7b2bb19 --- /dev/null +++ b/geoffrey_models/src/models/parameters/add_item_params.rs @@ -0,0 +1,23 @@ +use crate::models::parameters::CommandRequest; +use crate::models::player::UserID; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct AddItemParams { + pub token: String, + pub user_id: UserID, + pub item_name: String, + pub price: u32, + pub quantity: u32, + pub shop: String, +} + +impl CommandRequest for AddItemParams { + fn token(&self) -> String { + self.token.clone() + } + + fn user_id(&self) -> Option { + Some(self.user_id.clone()) + } +} diff --git a/geoffrey_models/src/models/parameters/mod.rs b/geoffrey_models/src/models/parameters/mod.rs index fc8cbd5..276d2ae 100644 --- a/geoffrey_models/src/models/parameters/mod.rs +++ b/geoffrey_models/src/models/parameters/mod.rs @@ -1,7 +1,9 @@ +pub mod add_item_params; pub mod add_location_params; pub mod add_token_params; pub mod find_params; pub mod register_params; +pub mod selling_params; use crate::models::player::{Player, UserID}; use crate::models::token::{Permissions, Token}; diff --git a/geoffrey_models/src/models/parameters/selling_params.rs b/geoffrey_models/src/models/parameters/selling_params.rs new file mode 100644 index 0000000..4e161d3 --- /dev/null +++ b/geoffrey_models/src/models/parameters/selling_params.rs @@ -0,0 +1,33 @@ +use crate::models::parameters::CommandRequest; +use crate::models::player::UserID; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub enum ItemSort { + Price, + Restock, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub enum Order { + High, + Low, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct SellingParams { + pub token: String, + pub query: String, + pub sort: Option, + pub order: Option, +} + +impl CommandRequest for SellingParams { + fn token(&self) -> String { + self.token.clone() + } + + fn user_id(&self) -> Option { + None + } +} diff --git a/geoffrey_models/src/models/response/api_error.rs b/geoffrey_models/src/models/response/api_error.rs index 176d95f..041cd81 100644 --- a/geoffrey_models/src/models/response/api_error.rs +++ b/geoffrey_models/src/models/response/api_error.rs @@ -9,6 +9,7 @@ pub enum GeoffreyAPIError { EntryNotUnique, DatabaseError(String), TokenNotAuthorized, + MultipleLocationsMatch, } impl Display for GeoffreyAPIError { @@ -26,6 +27,9 @@ impl Display for GeoffreyAPIError { GeoffreyAPIError::TokenNotAuthorized => { "Token supplied in request is not authorized".to_string() } + GeoffreyAPIError::MultipleLocationsMatch => { + "The location query returned multiple locations.".to_string() + } }; write!(f, "{}", string) } diff --git a/geoffrey_models/src/models/response/mod.rs b/geoffrey_models/src/models/response/mod.rs index 5a01db4..15430d9 100644 --- a/geoffrey_models/src/models/response/mod.rs +++ b/geoffrey_models/src/models/response/mod.rs @@ -2,6 +2,7 @@ use crate::models::response::api_error::GeoffreyAPIError; use serde::{Deserialize, Serialize}; pub mod api_error; +pub mod selling_listing; #[derive(Debug, Serialize, Deserialize)] pub enum APIResponse { diff --git a/geoffrey_models/src/models/response/selling_listing.rs b/geoffrey_models/src/models/response/selling_listing.rs new file mode 100644 index 0000000..93f788d --- /dev/null +++ b/geoffrey_models/src/models/response/selling_listing.rs @@ -0,0 +1,11 @@ +use crate::models::item::ItemListing; +use crate::models::{Position, Tunnel}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct SellingListing { + pub shop_name: String, + pub shop_loc: Position, + pub portal: Option, + pub listing: ItemListing, +}