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
main
Joey Hines 2021-11-07 14:23:18 -07:00
parent 22ffe75422
commit 8e8798f509
No known key found for this signature in database
GPG Key ID: 80F567B5C968F91B
12 changed files with 227 additions and 5 deletions

View File

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

View File

@ -1,6 +1,8 @@
use crate::commands::add_item::AddItem;
use crate::commands::add_location::AddLocation; use crate::commands::add_location::AddLocation;
use crate::commands::find::FindCommand; use crate::commands::find::FindCommand;
use crate::commands::register::Register; use crate::commands::register::Register;
use crate::commands::selling::Selling;
use crate::context::Context; use crate::context::Context;
use crate::helper::{get_player_from_req, get_token_from_req}; use crate::helper::{get_player_from_req, get_token_from_req};
use crate::Result; use crate::Result;
@ -17,10 +19,12 @@ use std::sync::Arc;
use warp::filters::BoxedFilter; use warp::filters::BoxedFilter;
use warp::Filter; use warp::Filter;
pub mod add_item;
pub mod add_location; pub mod add_location;
pub mod add_token; pub mod add_token;
pub mod find; pub mod find;
pub mod register; pub mod register;
pub mod selling;
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
#[allow(clippy::upper_case_acronyms)] #[allow(clippy::upper_case_acronyms)]
@ -109,6 +113,8 @@ pub fn command_filter(
warp::path("command").and( warp::path("command").and(
create_command_filter::<FindCommand>(ctx.clone()) create_command_filter::<FindCommand>(ctx.clone())
.or(create_command_filter::<AddLocation>(ctx.clone())) .or(create_command_filter::<AddLocation>(ctx.clone()))
.or(create_command_filter::<Register>(ctx)), .or(create_command_filter::<Register>(ctx.clone()))
.or(create_command_filter::<Selling>(ctx.clone()))
.or(create_command_filter::<AddItem>(ctx)),
) )
} }

View File

@ -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<SellingListing>;
fn command_name() -> String {
"selling".to_string()
}
fn request_type() -> RequestType {
RequestType::GET
}
fn command_level() -> CommandLevel {
CommandLevel::ALL
}
fn run_command(ctx: Arc<Context>, req: Self::Req, _: Option<Player>) -> Result<Self::Resp> {
let shops: Vec<SellingListing> = 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<SellingListing> = 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)
}
}

View File

@ -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 { if match_count > 0 {
log::debug!("{} is not unique: {:?}", T::tree(), model); log::debug!("{} is not unique: {:?}", T::tree(), model);

View File

@ -23,14 +23,14 @@ pub struct ItemListing {
} }
impl ItemListing { impl ItemListing {
fn new(item: &str, price: u32, count_per_price: u32, restocked_time: DateTime<Utc>) -> Self { pub fn new(item: &str, price: u32, count_per_price: u32) -> Self {
Self { Self {
item: Item { item: Item {
name: item.to_string(), name: item.to_string(),
}, },
price, price,
count_per_price, count_per_price,
restocked_time, restocked_time: Utc::now(),
} }
} }
} }

View File

@ -14,7 +14,7 @@ pub mod market;
pub mod shop; pub mod shop;
pub mod town; pub mod town;
#[derive(Serialize, Deserialize, Debug, Clone, Copy)] #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialOrd, PartialEq)]
pub enum LocationType { pub enum LocationType {
Base, Base,
Shop, Shop,

View File

@ -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<UserID> {
Some(self.user_id.clone())
}
}

View File

@ -1,7 +1,9 @@
pub mod add_item_params;
pub mod add_location_params; pub mod add_location_params;
pub mod add_token_params; pub mod add_token_params;
pub mod find_params; pub mod find_params;
pub mod register_params; pub mod register_params;
pub mod selling_params;
use crate::models::player::{Player, UserID}; use crate::models::player::{Player, UserID};
use crate::models::token::{Permissions, Token}; use crate::models::token::{Permissions, Token};

View File

@ -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<ItemSort>,
pub order: Option<Order>,
}
impl CommandRequest for SellingParams {
fn token(&self) -> String {
self.token.clone()
}
fn user_id(&self) -> Option<UserID> {
None
}
}

View File

@ -9,6 +9,7 @@ pub enum GeoffreyAPIError {
EntryNotUnique, EntryNotUnique,
DatabaseError(String), DatabaseError(String),
TokenNotAuthorized, TokenNotAuthorized,
MultipleLocationsMatch,
} }
impl Display for GeoffreyAPIError { impl Display for GeoffreyAPIError {
@ -26,6 +27,9 @@ impl Display for GeoffreyAPIError {
GeoffreyAPIError::TokenNotAuthorized => { GeoffreyAPIError::TokenNotAuthorized => {
"Token supplied in request is not authorized".to_string() "Token supplied in request is not authorized".to_string()
} }
GeoffreyAPIError::MultipleLocationsMatch => {
"The location query returned multiple locations.".to_string()
}
}; };
write!(f, "{}", string) write!(f, "{}", string)
} }

View File

@ -2,6 +2,7 @@ use crate::models::response::api_error::GeoffreyAPIError;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
pub mod api_error; pub mod api_error;
pub mod selling_listing;
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub enum APIResponse<T> { pub enum APIResponse<T> {

View File

@ -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<Tunnel>,
pub listing: ItemListing,
}