use std::sync::Arc; use geoffrey_models::models::item::ItemListing; use geoffrey_models::models::locations::{LocationDataDb, LocationDb}; use geoffrey_models::models::parameters::selling_params::{ItemSort, Order, SellingParams}; use geoffrey_models::models::player::Player; use geoffrey_models::models::response::selling_listing::SellingListing; use geoffrey_models::models::CommandLevel; use crate::api_endpoint::{ApiEndpoint, RequestType}; use crate::commands::Command; use crate::context::Context; use crate::Result; pub struct Selling {} impl ApiEndpoint for Selling { fn endpoint_name() -> String { "selling".to_string() } fn request_type() -> RequestType { RequestType::GET } } impl Command for Selling { type Req = SellingParams; type Resp = Vec; fn command_level() -> CommandLevel { CommandLevel::ALL } fn run_command(ctx: Arc, req: &Self::Req, _: Option) -> Result { let mut listings: 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.portal.clone(), }) } else { None } }) .collect(); Some(listings) } else { None } }) .flatten() .collect(); let sort = req.sort.as_ref().unwrap_or(&ItemSort::Restock); match sort { ItemSort::Price => { listings.sort_by(|a, b| ItemListing::order_by_price(&a.listing, &b.listing)) } ItemSort::Restock => { listings.sort_by(|a, b| ItemListing::order_by_restock(&a.listing, &b.listing)) } } let ordering = if let Some(order) = &req.order { order } else { match sort { ItemSort::Price => &Order::Low, ItemSort::Restock => &Order::High, } }; if *ordering == Order::High { listings.reverse(); } Ok(listings) } } #[cfg(test)] mod test { use std::path::PathBuf; use std::time::Instant; use geoffrey_models::models::item::ItemListing; use geoffrey_models::models::locations::shop::Shop; use geoffrey_models::models::locations::{LocationDataDb, LocationDb}; use geoffrey_models::models::parameters::selling_params::SellingParams; use geoffrey_models::models::settings::GeoffreySettings; use geoffrey_models::models::Position; use crate::commands::selling::Selling; use crate::commands::Command; use crate::config::{GeoffreyAPIConfig, ServerConfig}; use crate::context::Context; use crate::Args; fn test_selling_lookup_speed(iter: i32, shop_count: i32, items_for_sale: i32) -> f32 { let config = GeoffreyAPIConfig { server_config: ServerConfig { db_path: PathBuf::from("test_db/"), host: "".to_string(), }, geoffrey_settings: GeoffreySettings::default(), }; let ctx = Context::new(config.clone(), Args::default()).unwrap(); let mut shop_data = Shop::default(); for _ in 0..items_for_sale { shop_data .item_listings .insert(ItemListing::new("sed", 5, 5)); } for i in 0..shop_count { let shop = LocationDb::new( format!("Test Shop {} {}", iter, i).as_str(), Position::default(), 0, None, LocationDataDb::Shop(shop_data.clone()), ); ctx.db.insert(shop).unwrap(); } let start = Instant::now(); Selling::run_command( ctx, &SellingParams { query: "sed".to_string(), sort: None, order: None, }, None, ) .unwrap(); let query_dur = start.elapsed().as_secs_f32(); return query_dur; } #[test] fn stress_test() { std::fs::remove_dir_all(PathBuf::from("test_db")).ok(); println!("Shop Count,Item Count,Query Time,Query Time / Shop"); for shop_count in (100..=500).step_by(100) { let query_dur = test_selling_lookup_speed(shop_count, 100, 10); println!( "{},{},{},{}", shop_count, 10, query_dur, query_dur / (shop_count as f32) ); } std::fs::remove_dir_all(PathBuf::from("test_db")).ok(); } }