From 185e1403e30260b0d3289a5ee8089285965b4997 Mon Sep 17 00:00:00 2001 From: Artus Date: Thu, 17 Oct 2019 12:56:07 +0200 Subject: [PATCH] moves API logic inside its own module --- lootalot_db/src/lib.rs | 240 ++++++++----------------------- lootalot_db/src/models/player.rs | 15 ++ src/api.rs | 166 +++++++++++++++++++++ src/lib.rs | 0 src/main.rs | 3 +- src/server.rs | 24 ++-- 6 files changed, 252 insertions(+), 196 deletions(-) create mode 100644 src/api.rs create mode 100644 src/lib.rs diff --git a/lootalot_db/src/lib.rs b/lootalot_db/src/lib.rs index 1626c37..bb55bef 100644 --- a/lootalot_db/src/lib.rs +++ b/lootalot_db/src/lib.rs @@ -19,8 +19,8 @@ mod schema; pub use models::{ claim::{Claim, Claims}, - item::{Item, LootManager}, - player::{Player, Players, Wealth}, + item::{Item, LootManager, Inventory}, + player::{Player, Wealth, Players, AsPlayer}, }; /// The connection used @@ -29,184 +29,66 @@ pub type DbConnection = SqliteConnection; pub type Pool = r2d2::Pool>; /// The result of a query on DB pub type QueryResult = Result; -/// The result of an action provided by DbApi -pub type ActionResult = Result; -#[derive(Serialize, Deserialize, Debug)] -pub enum Update { - Wealth(Wealth), - ItemAdded(Item), - ItemRemoved(Item), - ClaimAdded(Claim), - ClaimRemoved(Claim), +/// Sets up a connection pool and returns it. +/// Uses the DATABASE_URL environment variable (must be set) +pub fn create_pool() -> Pool { + let connspec = std::env::var("DATABASE_URL").expect("DATABASE_URL"); + dbg!(&connspec); + let manager = ConnectionManager::::new(connspec); + r2d2::Pool::builder() + .build(manager) + .expect("Failed to create pool.") } -#[derive(Serialize, Deserialize, Debug)] -pub enum Value { - Item(Item), - Claim(Claim), - ItemList(Vec), - ClaimList(Vec), - PlayerList(Vec), -} -#[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 updates: Option>, // A list of updates, if any - pub errors: Option, // A text describing errors, if any -} - -impl ApiResponse { - fn push_update(&mut self, update: Update) { - if let Some(v) = self.updates.as_mut() { - v.push(update); - } else { - self.updates = Some(vec![update]); - } - } - - 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); - } - - fn notifiy>(&mut self, text: S) { - self.notify = Some(text.into()); - } -} - -pub enum ApiError { - DieselError(diesel::result::Error), - InvalidAction(String), -} - -pub enum ApiActions { - FetchPlayers, - FetchInventory, - // Player actions - FetchLoot(i32), - UpdateWealth(i32, f32), - BuyItems(i32, Vec<(i32, Option)>), - SellItems(i32, Vec<(i32, Option)>), - ClaimItem(i32, i32), - UnclaimItem(i32, i32), - // Group actions - AddLoot(Vec), -} - -pub enum AdminActions { - AddPlayer(String, f32), - //AddInventoryItem(pub String, pub i32), - ResolveClaims, - //SetClaimsTimeout(pub i32), -} - -pub fn execute( +/// Sells a single item inside a transaction +/// +/// # Returns +/// The deleted entity and the updated Wealth (as a difference from previous value) +pub fn sell_item_transaction( conn: &DbConnection, - query: ApiActions, -) -> Result { - let mut response = ApiResponse::default(); - match query { - ApiActions::FetchPlayers => { - response.set_value(Value::PlayerList(models::player::Players(conn).all()?)); + id: i32, + loot_id: i32, + price_mod: Option, +) -> QueryResult<(Item, Wealth)> { + conn.transaction(|| { + let deleted = 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; } - ApiActions::FetchInventory => { - response.set_value(Value::ItemList(models::item::Inventory(conn).all()?)); - } - ApiActions::FetchLoot(id) => { - 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)?, - )); - } - ApiActions::BuyItems(id, params) => { - let mut cumulated_diff: Vec = Vec::with_capacity(params.len()); - let mut added_items: Vec = Vec::with_capacity(params.len()); - for (item_id, price_mod) in params.into_iter() { - // Use a transaction to avoid incoherant state in case of error - if let Ok((item, diff)) = conn.transaction(|| { - // Find item in inventory - let item = models::item::Inventory(conn).find(item_id)?; - let new_item = models::item::LootManager(conn, id).add_from(&item)?; - let sell_price = match price_mod { - Some(modifier) => item.base_price as f32 * modifier, - None => item.base_price as f32, - }; - models::player::AsPlayer(conn, id) - .update_wealth(-sell_price) - .map(|diff| (new_item, diff)) - }) { - cumulated_diff.push(diff); - response.push_update(Update::ItemAdded(item)); - } else { - response.push_error(format!("Error adding {}", item_id)); - } - } - let all_diff = cumulated_diff - .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(all_diff)); - } - ApiActions::SellItems(id, params) => { - 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)?, - )); - } - ApiActions::UnclaimItem(id, item) => { - response.push_update(Update::ClaimRemoved( - models::claim::Claims(conn).remove(id, item)?, - )); - } - // Group actions - ApiActions::AddLoot(items) => {} - } - Ok(response) + let wealth = AsPlayer(conn, id) + .update_wealth(sell_value)?; + Ok((deleted, wealth)) + }) +} + +/// Buys a single item, copied from inventory. +/// Runs inside a transaction +/// +/// # Returns +/// The created entity and the updated Wealth (as a difference from previous value) +pub fn buy_item_from_inventory( + conn: &DbConnection, + id: i32, + item_id: i32, + price_mod: Option, +) -> QueryResult<(Item, Wealth)> { + conn.transaction(|| { + // Find item in inventory + let item = Inventory(conn).find(item_id)?; + let new_item = LootManager(conn, id).add_from(&item)?; + let sell_price = match price_mod { + Some(modifier) => item.base_price as f32 * modifier, + None => item.base_price as f32, + }; + AsPlayer(conn, id) + .update_wealth(-sell_price) + .map(|diff| (new_item, diff)) + }) } /// Fetch all existing claims @@ -238,16 +120,6 @@ pub fn resolve_claims(conn: &DbConnection) -> QueryResult<()> { Ok(()) } -/// Sets up a connection pool and returns it. -/// Uses the DATABASE_URL environment variable (must be set) -pub fn create_pool() -> Pool { - let connspec = std::env::var("DATABASE_URL").expect("DATABASE_URL"); - dbg!(&connspec); - let manager = ConnectionManager::::new(connspec); - r2d2::Pool::builder() - .build(manager) - .expect("Failed to create pool.") -} #[cfg(none)] mod tests_old { diff --git a/lootalot_db/src/models/player.rs b/lootalot_db/src/models/player.rs index 2b79383..b0f95c0 100644 --- a/lootalot_db/src/models/player.rs +++ b/lootalot_db/src/models/player.rs @@ -132,6 +132,21 @@ impl Wealth { } } +use std::ops::Add; + +impl Add for Wealth { + type Output = Self; + + fn add(self, other: Self) -> Self { + Wealth { + cp: self.cp + other.cp, + sp: self.sp + other.sp, + gp: self.gp + other.gp, + pp: self.pp + other.pp + } + } +} + /// Representation of a new player record #[derive(Insertable)] #[table_name = "players"] diff --git a/src/api.rs b/src/api.rs new file mode 100644 index 0000000..e3fc9f6 --- /dev/null +++ b/src/api.rs @@ -0,0 +1,166 @@ +use lootalot_db::{self as db, DbConnection, QueryResult}; + +/// Every possible update which can happen during a query +#[derive(Serialize, Deserialize, Debug)] +pub enum Update { + Wealth(db::Wealth), + ItemAdded(db::Item), + ItemRemoved(db::Item), + ClaimAdded(db::Claim), + ClaimRemoved(db::Claim), +} + +/// Every value which can be queried +#[derive(Serialize, Deserialize, Debug)] +pub enum Value { + Item(db::Item), + Claim(db::Claim), + ItemList(Vec), + ClaimList(Vec), + PlayerList(Vec), +} + +/// A generic response for all queries +#[derive(Serialize, Deserialize, Debug, Default)] +pub struct ApiResponse { + /// The value requested, if any + pub value: Option, + /// A text to notify user, if relevant + pub notification: Option, + /// A list of updates, if any + pub updates: Option>, + /// A text describing errors, if any + pub errors: Option, +} + +impl ApiResponse { + fn push_update(&mut self, update: Update) { + if let Some(v) = self.updates.as_mut() { + v.push(update); + } else { + self.updates = Some(vec![update]); + } + } + + 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); + } + + fn notify>(&mut self, text: S) { + self.notification = Some(text.into()); + } +} + +pub enum ApiError { + DieselError(diesel::result::Error), + InvalidAction(String), +} + +/// Every allowed queries on the database +pub enum ApiActions { + FetchPlayers, + FetchInventory, + FetchClaims, + // Player actions + FetchLoot(i32), + UpdateWealth(i32, f32), + BuyItems(i32, Vec<(i32, Option)>), + SellItems(i32, Vec<(i32, Option)>), + ClaimItem(i32, i32), + UnclaimItem(i32, i32), + // Group actions + AddLoot(Vec), +} + +pub enum AdminActions { + AddPlayer(String, f32), + //AddInventoryItem(pub String, pub i32), + ResolveClaims, + //SetClaimsTimeout(pub i32), +} + +pub fn execute( + conn: &DbConnection, + query: ApiActions, +) -> Result { + let mut response = ApiResponse::default(); + match query { + ApiActions::FetchPlayers => { + response.set_value(Value::PlayerList(db::Players(conn).all()?)); + } + ApiActions::FetchInventory => { + response.set_value(Value::ItemList(db::Inventory(conn).all()?)); + } + ApiActions::FetchClaims => { + response.set_value(Value::ClaimList(db::fetch_claims(conn)?)); + } + ApiActions::FetchLoot(id) => { + response.set_value(Value::ItemList(db::LootManager(conn, id).all()?)); + } + ApiActions::UpdateWealth(id, amount) => { + response.push_update(Update::Wealth( + db::AsPlayer(conn, id).update_wealth(amount)?, + )); + } + ApiActions::BuyItems(id, params) => { + let mut cumulated_diff: Vec = Vec::with_capacity(params.len()); + let mut added_items: u16 = 0; + for (item_id, price_mod) in params.into_iter() { + // Use a transaction to avoid incoherant state in case of error + if let Ok((item, diff)) = db::buy_item_from_inventory(conn, id, item_id, price_mod) { + cumulated_diff.push(diff); + response.push_update(Update::ItemAdded(item)); + added_items += 1; + } else { + response.push_error(format!("Error adding {}", item_id)); + } + } + response.notify(format!("Added {} items", added_items)); + response.push_update(Update::Wealth( + cumulated_diff + .into_iter() + .fold(db::Wealth::from_gp(0.0), |acc, i| acc + i), + )); + } + ApiActions::SellItems(id, params) => { + let mut all_results: Vec = Vec::with_capacity(params.len()); + let mut sold_items: u16 = 0; + for (loot_id, price_mod) in params.into_iter() { + if let Ok((deleted, diff)) = db::sell_item_transaction(conn, id, loot_id, price_mod) { + all_results.push(diff); + response.push_update(Update::ItemRemoved(deleted)); + sold_items += 1; + } else { + response.push_error(format!("Error selling {}", loot_id)); + } + } + response.notify(format!("Sold {} items", sold_items)); + response.push_update(Update::Wealth( + all_results + .into_iter() + .fold(db::Wealth::from_gp(0.0), |acc, i| acc + i), + )); + } + ApiActions::ClaimItem(id, item) => { + response.push_update(Update::ClaimAdded( + db::Claims(conn).add(id, item)?, + )); + } + ApiActions::UnclaimItem(id, item) => { + response.push_update(Update::ClaimRemoved( + db::Claims(conn).remove(id, item)?, + )); + } + // Group actions + ApiActions::AddLoot(items) => {} + } + Ok(response) +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/main.rs b/src/main.rs index 089780e..8978b05 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,9 +2,10 @@ extern crate actix_web; extern crate dotenv; extern crate env_logger; extern crate lootalot_db; -extern crate serde; +#[macro_use] extern crate serde; mod server; +mod api; fn main() { std::env::set_var("RUST_LOG", "actix_web=info"); diff --git a/src/server.rs b/src/server.rs index 1093337..02e8901 100644 --- a/src/server.rs +++ b/src/server.rs @@ -5,6 +5,7 @@ use futures::Future; use std::env; use lootalot_db as db; +use crate::api; type AppPool = web::Data; type PlayerId = web::Path; @@ -14,11 +15,11 @@ 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, + query: api::ApiActions, ) -> impl Future { let conn = pool.get().unwrap(); - web::block(move || db::execute(&conn, query)).then(|res| match res { + web::block(move || api::execute(&conn, query)).then(|res| match res { Ok(r) => HttpResponse::Ok().json(r), Err(e) => { dbg!(&e); @@ -28,13 +29,14 @@ pub fn db_call( } fn configure_app(config: &mut web::ServiceConfig) { + use api::ApiActions as Q; config.service( web::scope("/api") .service( web::scope("/players") .service( web::resource("/").route( - web::get().to_async(|pool| db_call(pool, db::ApiActions::FetchPlayers)), + web::get().to_async(|pool| db_call(pool, Q::FetchPlayers)), ), //.route(web::post().to_async(endpoints::new_player)) ) // List of players .service( @@ -45,14 +47,14 @@ fn configure_app(config: &mut web::ServiceConfig) { //.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)) + db_call(pool, Q::ClaimItem(*player, *data)) }, )) .route(web::delete().to_async( |pool, (player, data): (PlayerId, ItemId)| { db_call( pool, - db::ApiActions::UnclaimItem(*player, *data), + Q::UnclaimItem(*player, *data), ) }, )), @@ -64,7 +66,7 @@ fn configure_app(config: &mut web::ServiceConfig) { |pool, (player, data): (PlayerId, web::Json)| { db_call( pool, - db::ApiActions::UpdateWealth(*player, *data), + Q::UpdateWealth(*player, *data), ) }, )), @@ -72,26 +74,26 @@ fn configure_app(config: &mut web::ServiceConfig) { .service( web::resource("/loot") .route(web::get().to_async(|pool, player: PlayerId| { - db_call(pool, db::ApiActions::FetchLoot(*player)) + db_call(pool, Q::FetchLoot(*player)) })) .route(web::put().to_async( move |pool, (player, data): (PlayerId, ItemListWithMods)| { - db_call(pool, db::ApiActions::BuyItems(*player, data.into_inner())) + db_call(pool, Q::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())) + db_call(pool, Q::SellItems(*player, data.into_inner())) }, )), ), ), ) - //.route("/claims", web::get().to_async(endpoints::player_claims)) + .route("/claims", web::get().to_async(|pool| db_call(pool, Q::FetchClaims))) .route( "/items", web::get() - .to_async(move |pool: AppPool| db_call(pool, db::ApiActions::FetchInventory)), + .to_async(move |pool: AppPool| db_call(pool, Q::FetchInventory)), ), ); }