diff --git a/lootalot_db/src/lib.rs b/lootalot_db/src/lib.rs index 6f35223..1626c37 100644 --- a/lootalot_db/src/lib.rs +++ b/lootalot_db/src/lib.rs @@ -33,8 +33,7 @@ pub type QueryResult = Result; pub type ActionResult = Result; #[derive(Serialize, Deserialize, Debug)] -enum Update { - NoUpdate, +pub enum Update { Wealth(Wealth), ItemAdded(Item), ItemRemoved(Item), @@ -43,7 +42,7 @@ enum Update { } #[derive(Serialize, Deserialize, Debug)] -enum Value { +pub enum Value { Item(Item), Claim(Claim), ItemList(Vec), @@ -53,10 +52,10 @@ enum Value { #[derive(Serialize, Deserialize, Debug, Default)] pub struct ApiResponse { - pub value: Option, // The value requested, if any - pub notify: Option, // A text to notify user, if relevant + pub value: Option, // The value requested, if any + pub notify: Option, // A text to notify user, if relevant pub updates: Option>, // A list of updates, if any - pub errors: Option, // A text describing errors, if any + pub errors: Option, // A text describing errors, if any } impl ApiResponse { @@ -68,6 +67,14 @@ impl ApiResponse { } } + fn push_error>(&mut self, error: S) { + if let Some(errors) = self.errors.as_mut() { + *errors = format!("{}\n{}", errors, error.into()); + } else { + self.errors = Some(error.into()) + } + } + fn set_value(&mut self, value: Value) { self.value = Some(value); } @@ -77,24 +84,23 @@ impl ApiResponse { } } - pub enum ApiError { DieselError(diesel::result::Error), InvalidAction(String), } -pub enum ApiActions<'a> { +pub enum ApiActions { FetchPlayers, FetchInventory, // Player actions FetchLoot(i32), UpdateWealth(i32, f32), - BuyItems(i32, &'a Vec<(i32, Option)>), - SellItems(i32, &'a Vec<(i32, Option)>), + BuyItems(i32, Vec<(i32, Option)>), + SellItems(i32, Vec<(i32, Option)>), ClaimItem(i32, i32), UnclaimItem(i32, i32), // Group actions - AddLoot(&'a Vec), + AddLoot(Vec), } pub enum AdminActions { @@ -104,38 +110,26 @@ pub enum AdminActions { //SetClaimsTimeout(pub i32), } - -pub fn execute<'a>(pool: Pool, query: ApiActions<'a>) -> Result { - let conn = pool.get().map_err(|e| {dbg!(e); diesel::result::Error::NotFound })?; +pub fn execute( + conn: &DbConnection, + query: ApiActions, +) -> Result { let mut response = ApiResponse::default(); match query { ApiActions::FetchPlayers => { - response.set_value( - Value::PlayerList( - schema::players::table.load::(conn)? - ) - ); - }, + response.set_value(Value::PlayerList(models::player::Players(conn).all()?)); + } ApiActions::FetchInventory => { - response.set_value( - Value::ItemList( - models::item::Inventory(conn).all()?)); + response.set_value(Value::ItemList(models::item::Inventory(conn).all()?)); } ApiActions::FetchLoot(id) => { - response.set_value( - Value::ItemList( - models::item::LootManager(conn, id).all()? - ) - ); - }, + response.set_value(Value::ItemList(models::item::LootManager(conn, id).all()?)); + } ApiActions::UpdateWealth(id, amount) => { - response.push_update( - Update::Wealth( - models::player::AsPlayer(conn, id) - .update_wealth(amount)? - ) - ); - }, + response.push_update(Update::Wealth( + models::player::AsPlayer(conn, id).update_wealth(amount)?, + )); + } ApiActions::BuyItems(id, params) => { let mut cumulated_diff: Vec = Vec::with_capacity(params.len()); let mut added_items: Vec = Vec::with_capacity(params.len()); @@ -143,7 +137,7 @@ pub fn execute<'a>(pool: Pool, query: ApiActions<'a>) -> Result item.base_price as f32 * modifier, @@ -156,134 +150,92 @@ pub fn execute<'a>(pool: Pool, query: ApiActions<'a>) -> Result { - sell(conn, id, params, &mut response); - }, + let mut all_results: Vec = Vec::with_capacity(params.len()); + for (loot_id, price_mod) in params.into_iter() { + let res = conn.transaction(|| { + let deleted = models::item::LootManager(conn, id).remove(loot_id)?; + let mut sell_value = deleted.base_price as f32 / 2.0; + if let Some(modifier) = price_mod { + sell_value *= modifier; + } + models::player::AsPlayer(conn, id) + .update_wealth(sell_value) + .map(|diff| (deleted, diff)) + }); + if let Ok((deleted, diff)) = res { + all_results.push(diff); + response.push_update(Update::ItemRemoved(deleted)); + } else { + response.push_error(format!("Error selling {}", loot_id)); + } + } + let wealth = all_results + .into_iter() + .fold(Wealth::from_gp(0.0), |sum, diff| Wealth { + cp: sum.cp + diff.cp, + sp: sum.sp + diff.sp, + gp: sum.gp + diff.gp, + pp: sum.pp + diff.pp, + }); + response.push_update(Update::Wealth(wealth)); + } ApiActions::ClaimItem(id, item) => { - response.push_update( - Update::ClaimAdded( - models::claim::Claims(conn) - .add(id, item)? - ) - ); - }, + response.push_update(Update::ClaimAdded( + models::claim::Claims(conn).add(id, item)?, + )); + } ApiActions::UnclaimItem(id, item) => { - response.push_update( - Update::ClaimRemoved( - models::claim::Claims(conn) - .remove(id, item)? - ) - ); - }, + response.push_update(Update::ClaimRemoved( + models::claim::Claims(conn).remove(id, item)?, + )); + } // Group actions - ApiActions::AddLoot(items) => {}, + ApiActions::AddLoot(items) => {} } Ok(response) } - /// Fetch all existing claims - pub fn fetch_claims(conn: &DbConnection) -> QueryResult> { - schema::claims::table.load::(conn) - } +/// Fetch all existing claims +pub fn fetch_claims(conn: &DbConnection) -> QueryResult> { + schema::claims::table.load::(conn) +} - /// Sell a set of items from this player chest - /// - /// # Returns - /// Result containing the difference in coins after operation -pub fn sell( - conn: &DbConnection, - id: i32, - params: &Vec<(i32, Option)>, - response: &mut ApiResponse, -) { - let mut all_results: Vec = Vec::with_capacity(params.len()); - for (loot_id, price_mod) in params.into_iter() { - let res = conn.transaction(|| { - let deleted = models::item::LootManager(conn, id).remove(*loot_id)?; - let mut sell_value = deleted.base_price as f32 / 2.0; - if let Some(modifier) = price_mod { - sell_value *= modifier; - } - models::player::AsPlayer(conn, id) - .update_wealth(sell_value) - .map(|diff| (deleted, diff)) - }); - if let Ok((deleted, diff)) = res { - all_results.push(diff); - response.push_update( - Update::ItemRemoved(deleted) - ); - } else { - response.errors = Some(format!("Error selling {}", loot_id)); +/// Resolve all pending claims and dispatch claimed items. +/// +/// When a player gets an item, it's debt is increased by this item sell value +pub fn resolve_claims(conn: &DbConnection) -> QueryResult<()> { + let data = models::claim::Claims(conn).grouped_by_item()?; + dbg!(&data); + + for (item, claims) in data { + match claims.len() { + 1 => { + let claim = claims.get(0).unwrap(); + let player_id = claim.player_id; + conn.transaction(|| { + claim.resolve_claim(conn)?; + //models::item::LootManager(self.0, 0).set_owner(claim.loot_id, claim.player_id)?; + models::player::AsPlayer(conn, player_id).update_debt(item.sell_value()) + })?; } + _ => (), } - let wealth = all_results.into_iter().fold(Wealth::from_gp(0.0), |sum, diff| { - Wealth { - cp: sum.cp + diff.cp, - sp: sum.sp + diff.sp, - gp: sum.gp + diff.gp, - pp: sum.pp + diff.pp, - } - }); - response.push_update(Update::Wealth(wealth)); - } - -/// Wrapper for interactions of admins with the DB. -pub struct AsAdmin<'q>(&'q DbConnection); - -impl<'q> AsAdmin<'q> { - /// Adds a player to the database - /// - /// Takes the player name and starting wealth (in gold value). - pub fn add_player(self, name: &str, start_wealth: f32) -> ActionResult<()> { - models::player::Players(self.0).add(name, start_wealth)?; - Ok(()) - } - - /// Adds a list of items to the group loot - pub fn add_loot(self, items: Vec) -> ActionResult<()> { - for item_desc in items.iter() { - models::item::LootManager(self.0, 0).add_from(item_desc)?; - } - Ok(()) - } - - /// Resolve all pending claims and dispatch claimed items. - /// - /// When a player gets an item, it's debt is increased by this item sell value - pub fn resolve_claims(self) -> ActionResult<()> { - let data = models::claim::Claims(self.0).grouped_by_item()?; - dbg!(&data); - - for (item, claims) in data { - match claims.len() { - 1 => { - let claim = claims.get(0).unwrap(); - let player_id = claim.player_id; - self.0.transaction(|| { - claim.resolve_claim(self.0)?; - //models::item::LootManager(self.0, 0).set_owner(claim.loot_id, claim.player_id)?; - models::player::AsPlayer(self.0, player_id).update_debt(item.sell_value()) - })?; - } - _ => (), - } - } - Ok(()) } + Ok(()) } /// Sets up a connection pool and returns it. diff --git a/src/server.rs b/src/server.rs index 2ead15a..1093337 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,44 +1,24 @@ use actix_cors::Cors; use actix_files as fs; -use actix_web::{web, App, Error, HttpResponse, HttpServer}; +use actix_web::{web, middleware, App, Error, HttpResponse, HttpServer}; use futures::Future; -use lootalot_db::{DbApi, Pool, QueryResult}; -use lootalot_db::{Item, LootManager}; -use lootalot_db as db; -use serde::{Deserialize, Serialize}; use std::env; -use std::default; -type AppPool = web::Data; +use lootalot_db as db; -/// Wraps call to the DbApi and process its result as a async HttpResponse -/// -/// Provides a convenient way to call the api inside a route definition. Given a connection pool, -/// access to the api is granted in a closure. The closure is called in a blocking way and should -/// return a QueryResult. -/// If the query succeeds, it's result is returned as JSON data. Otherwise, an InternalServerError -/// is returned. -/// -/// # Usage -/// ``` -/// (...) -/// .route("path/to/", -/// move |pool: web::Data| { -/// // user data can be processed here -/// // ... -/// db_call(pool, move |api| { -/// // ...do what you want with the api -/// } -/// } -/// ) -/// ``` -pub fn db_call(pool: AppPool, query: Q) -> impl Future -where - J: serde::ser::Serialize + Send + 'static, - Q: Fn(DbConnection) -> ApiResponse + Send + 'static, +type AppPool = web::Data; +type PlayerId = web::Path; +type ItemId = web::Json; +type ItemListWithMods = web::Json)>>; + +/// Wraps call to the database query and convert its result as a async HttpResponse +pub fn db_call( + pool: AppPool, + query: db::ApiActions, +) -> impl Future { let conn = pool.get().unwrap(); - web::block(move || query(conn)).then(|res| match res { + web::block(move || db::execute(&conn, query)).then(|res| match res { Ok(r) => HttpResponse::Ok().json(r), Err(e) => { dbg!(&e); @@ -47,205 +27,91 @@ where }) } -mod endpoints { - - use super::*; - - #[derive(Serialize, Deserialize, Debug)] - pub struct PlayerClaim { - player_id: i32, - item_id: i32, - } - - #[derive(Serialize, Deserialize, Debug)] - pub struct WealthUpdate { - player_id: i32, - value_in_gp: f32, - } - - #[derive(Serialize, Deserialize, Debug)] - pub struct NewPlayer { - pub name: String, - pub wealth: f32, - } - - #[derive(Serialize, Deserialize, Debug)] - pub struct LootUpdate { - player_id: i32, - items: Vec<(i32, Option)>, - } - - pub fn players_list(pool: AppPool) -> impl Future { - db_call(pool, move |conn| { - let (value, errors) = match db::Players(conn).all() { - Ok(v) => (Some(v), None), - Err(e) => (None, Some(e.to_string())), - }; - ApiResponse { - value, - errors, - ..Default::default() - } - }) - } - - pub fn player_loot( - pool: AppPool, - player_id: web::Path, - ) -> impl Future { - db_call(pool, move |conn| { - let (value, errors) = { - match db::LootManager(&conn, *player_id).all() { - Ok(v) => (Some(v), None), - Err(e) => (None, Some(e.to_string())), - } - }; - ApiResponse { - value, - errors, - ..Default::default() - } - }) - } - - pub fn update_wealth( - pool: AppPool, - (player, data): (web::Path, web::Json), - ) -> impl Future { - db_call(pool, move |conn| { - let (updates, errors) = - match db::AsPlayer(conn, player) - .update_wealth(data.value_in_gp) { - Ok(w) => (Some(vec![w.as_tuple(),]), None), - Err(e) => (None, Some(e.to_string())), - }; - ApiResponse { - updates, - errors, - ..Default::default() - } - }) - } - - pub fn buy_item( - pool: AppPool, - data: web::Json, - ) -> impl Future { - db_call(pool, move |api| { - api.as_player(data.player_id).buy(&data.items) - }) - } - - pub fn sell_item( - pool: AppPool, - data: web::Json, - ) -> impl Future { - db_call(pool, move |api| { - api.as_player(data.player_id).sell(&data.items) - }) - } - pub fn player_claims(pool: AppPool) -> impl Future { - db_call(pool, move |api| api.fetch_claims()) - } - - pub fn put_claim( - pool: AppPool, - (player, loot): (web::Path, web::Json), - ) -> impl Future { - db_call(pool, move |api| api.as_player(*player).claim(loot.item_id)) - } - pub fn delete_claim( - pool: AppPool, - (player, data): (web::Path, web::Json), - ) -> impl Future { - db_call(pool, move |api| { - api.as_player(*player).unclaim(data.item_id) - }) - } +fn configure_app(config: &mut web::ServiceConfig) { + config.service( + web::scope("/api") + .service( + web::scope("/players") + .service( + web::resource("/").route( + web::get().to_async(|pool| db_call(pool, db::ApiActions::FetchPlayers)), + ), //.route(web::post().to_async(endpoints::new_player)) + ) // List of players + .service( + web::scope("/{player_id}") + //.route(web::get().to_async(...)) // Details of player + .service( + web::resource("/claims") + //.route(web::get().to_async(endpoints::player_claims)) + .route(web::put().to_async( + |pool, (player, data): (PlayerId, ItemId)| { + db_call(pool, db::ApiActions::ClaimItem(*player, *data)) + }, + )) + .route(web::delete().to_async( + |pool, (player, data): (PlayerId, ItemId)| { + db_call( + pool, + db::ApiActions::UnclaimItem(*player, *data), + ) + }, + )), + ) + .service( + web::resource("/wealth") + //.route(web::get().to_async(...)) + .route(web::put().to_async( + |pool, (player, data): (PlayerId, web::Json)| { + db_call( + pool, + db::ApiActions::UpdateWealth(*player, *data), + ) + }, + )), + ) + .service( + web::resource("/loot") + .route(web::get().to_async(|pool, player: PlayerId| { + db_call(pool, db::ApiActions::FetchLoot(*player)) + })) + .route(web::put().to_async( + move |pool, (player, data): (PlayerId, ItemListWithMods)| { + db_call(pool, db::ApiActions::BuyItems(*player, data.into_inner())) + }, + )) + .route(web::delete().to_async( + move |pool, (player, data): (PlayerId, ItemListWithMods)| { + db_call(pool, db::ApiActions::SellItems(*player, data.into_inner())) + }, + )), + ), + ), + ) + //.route("/claims", web::get().to_async(endpoints::player_claims)) + .route( + "/items", + web::get() + .to_async(move |pool: AppPool| db_call(pool, db::ApiActions::FetchInventory)), + ), + ); } -pub(crate) fn serve() -> std::io::Result<()> { +pub fn serve() -> std::io::Result<()> { let www_root: String = env::var("WWW_ROOT").expect("WWW_ROOT must be set"); + let pool = db::create_pool(); dbg!(&www_root); - let pool = lootalot_db::create_pool(); HttpServer::new(move || { App::new() .data(pool.clone()) + .configure(configure_app) .wrap( Cors::new() .allowed_origin("http://localhost:8080") .allowed_methods(vec!["GET", "POST", "PUT", "DELETE", "OPTIONS"]) .max_age(3600), ) - .service( - web::scope("/api") - .service( - web::scope("/players") - .service( - web::resource("/") - .route(web::get().to_async(endpoints::players_list)), - ) // List of players - //.route(web::put().to_async(endpoints::new_player)) // Create/Update player - .service( - web::scope("/{player_id}") - //.route(web::get().to_async(...)) // Details of player - .service( - web::resource("/claims") - //.route(web::get().to_async(endpoints::player_claims)) - .route(web::put().to_async(endpoints::put_claim)) - .route(web::delete().to_async(endpoints::delete_claim)), - ) - .service( - web::resource("/wealth") - //.route(web::get().to_async(...)) - .route(web::put().to_async(endpoints::update_wealth)), - ) - .service( - web::resource("/loot") - .route(web::get().to_async(endpoints::player_loot)) - .route(web::put().to_async(endpoints::buy_item)) - .route(web::delete().to_async(endpoints::sell_item)), - ), - ), - ) - .route("/claims", web::get().to_async(endpoints::player_claims)) - .route( - "/items", - web::get().to_async(move |pool: AppPool| { - db_call(pool, move |api| api.fetch_inventory()) - }), - ) - .service( - web::scope("/admin") - .route( - "/resolve-claims", - web::get().to_async(move |pool: AppPool| { - db_call(pool, move |api| api.as_admin().resolve_claims()) - }), - ) - .route( - "/add-loot", - web::post().to_async( - move |pool: AppPool, data: web::Json>| { - db_call(pool, move |api| { - api.as_admin().add_loot(data.to_vec()) - }) - }, - ), - ) - .route( - "/add-player", - web::get().to_async( - move |pool: AppPool, data: web::Json| { - db_call(pool, move |api| { - api.as_admin().add_player(&data.name, data.wealth) - }) - }, - ), - ), - ), - ) + .wrap(middleware::Logger::default()) .service(fs::Files::new("/", www_root.clone()).index_file("index.html")) }) .bind("127.0.0.1:8088")?