diff --git a/lootalot_db/src/lib.rs b/lootalot_db/src/lib.rs index 2284e8b..6f35223 100644 --- a/lootalot_db/src/lib.rs +++ b/lootalot_db/src/lib.rs @@ -20,7 +20,7 @@ mod schema; pub use models::{ claim::{Claim, Claims}, item::{Item, LootManager}, - player::{Player, Players}, + player::{Player, Players, Wealth}, }; /// The connection used @@ -32,13 +32,57 @@ pub type QueryResult = Result; /// The result of an action provided by DbApi pub type ActionResult = Result; +#[derive(Serialize, Deserialize, Debug)] +enum Update { + NoUpdate, + Wealth(Wealth), + ItemAdded(Item), + ItemRemoved(Item), + ClaimAdded(Claim), + ClaimRemoved(Claim), +} + +#[derive(Serialize, Deserialize, Debug)] +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 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 type ApiResult = Result; - pub enum ApiActions<'a> { FetchPlayers, FetchInventory, @@ -60,206 +104,143 @@ pub enum AdminActions { //SetClaimsTimeout(pub i32), } -/// A wrapper providing an API over the database -/// It offers a convenient way to deal with connection. -/// -/// # Note -/// All methods consumes the DbApi, so that only one action -/// can be performed using a single instance. -/// -/// # Todo list -/// ```text -/// v .as_player() -/// // Needs an action's history (one entry only should be enough) -/// x .undo_last_action() -> Success status -/// v .as_admin() -/// // When adding loot, an identifier should be used to build some kind of history -/// vx .add_loot(identifier, [items_desc]) -> Success status -/// x .sell_loot([players], [excluded_item_ids]) -> Success status (bool, player_share) -/// // Claims should be resolved after a certain delay -/// x .set_claims_timeout() -/// v .resolve_claims() -/// v .add_player(player_data) -/// ``` -/// -pub struct DbApi<'q>(&'q DbConnection); -impl<'q> DbApi<'q> { - /// Returns a DbApi using the user given connection - /// - /// # Usage - /// ``` - /// use lootalot_db::{DbConnection, DbApi}; - /// # use diesel::connection::Connection; - /// let conn = DbConnection::establish(":memory:").unwrap(); - /// let api = DbApi::with_conn(&conn); - /// ``` - pub fn with_conn(conn: &'q DbConnection) -> Self { - Self(conn) - } - /// Fetch the list of all players - pub fn fetch_players(self) -> QueryResult> { - Ok(schema::players::table.load::(self.0)?) - } - /// Fetch the inventory of items - /// - /// TODO: remove limit used for debug - pub fn fetch_inventory(self) -> QueryResult> { - models::item::Inventory(self.0).all() - } - /// Fetch all existing claims - pub fn fetch_claims(self) -> QueryResult> { - Ok(schema::claims::table.load::(self.0)?) - } - /// Wrapper for acting as a specific player - /// - /// # Usage - /// ``` - /// # use lootalot_db::{DbConnection, DbApi}; - /// # use diesel::connection::Connection; - /// # let conn = DbConnection::establish(":memory:").unwrap(); - /// # let api = DbApi::with_conn(&conn); - /// let player_id: i32 = 1; // Id that references player in DB - /// let player = api.as_player(player_id); - /// ``` - pub fn as_player(self, id: i32) -> AsPlayer<'q> { - AsPlayer { id, conn: self.0 } - } - - /// Wrapper for acting as the admin - pub fn as_admin(self) -> AsAdmin<'q> { - AsAdmin(self.0) - } -} - -/// A wrapper for interactions of players with the database. -/// Possible actions are exposed as methods -pub struct AsPlayer<'q> { - id: i32, - conn: &'q DbConnection, -} - -impl<'q> AsPlayer<'q> { - /// Fetch the content of a player's chest - /// - /// # Usage - /// ``` - /// # extern crate diesel_migrations; - /// # use lootalot_db::{DbConnection, DbApi}; - /// # use diesel::connection::Connection; - /// # let conn = DbConnection::establish(":memory:").unwrap(); - /// # diesel_migrations::run_pending_migrations(&conn).unwrap(); - /// # let api = DbApi::with_conn(&conn); - /// // Get loot of player with id of 1 - /// let loot = api.as_player(1).loot().unwrap(); - /// assert_eq!(format!("{:?}", loot), "[]".to_string()); - /// ``` - pub fn loot(self) -> QueryResult> { - models::item::LootManager(self.conn, self.id).all() - } - /// Buy a batch of items and add them to this player chest - /// - /// Items can only be bought from inventory. Hence, the use - /// of the entity's id in 'items' table. - /// - /// # Params - /// List of (Item's id in inventory, Option) - /// - /// # Returns - /// Result containing the difference in coins after operation - pub fn buy<'a>( - self, - params: &Vec<(i32, Option)>, - ) -> ActionResult<(Vec, (i32, i32, i32, i32))> { - let mut cumulated_diff: Vec<(i32, i32, i32, i32)> = Vec::with_capacity(params.len()); - let mut added_items: Vec = Vec::with_capacity(params.len()); - for (item_id, price_mod) in params.into_iter() { - if let Ok((item, diff)) = self.conn.transaction(|| { - // Find item in inventory - let item = models::item::Inventory(self.conn).find(*item_id)?; - let new_item = models::item::LootManager(self.conn, self.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(self.conn, self.id) - .update_wealth(-sell_price) - .map(|diff| (new_item, diff.as_tuple())) - }) { - cumulated_diff.push(diff); - added_items.push(item); - } +pub fn execute<'a>(pool: Pool, query: ApiActions<'a>) -> Result { + let conn = pool.get().map_err(|e| {dbg!(e); diesel::result::Error::NotFound })?; + let mut response = ApiResponse::default(); + match query { + ApiActions::FetchPlayers => { + response.set_value( + Value::PlayerList( + schema::players::table.load::(conn)? + ) + ); + }, + ApiActions::FetchInventory => { + response.set_value( + Value::ItemList( + models::item::Inventory(conn).all()?)); } - let all_diff = cumulated_diff.into_iter().fold((0, 0, 0, 0), |sum, diff| { - ( - sum.0 + diff.0, - sum.1 + diff.1, - sum.2 + diff.2, - sum.3 + diff.3, - ) - }); - Ok((added_items, all_diff)) + 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.errors = Some(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) => { + sell(conn, id, params, &mut response); + }, + 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) +} + + /// 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(self, params: &Vec<(i32, Option)>) -> ActionResult<(i32, i32, i32, i32)> { - let mut all_results: Vec<(i32, i32, i32, i32)> = Vec::with_capacity(params.len()); +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 = self.conn.transaction(|| { - let deleted = models::item::LootManager(self.conn, self.id).remove(*loot_id)?; + 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(self.conn, self.id).update_wealth(sell_value) + models::player::AsPlayer(conn, id) + .update_wealth(sell_value) + .map(|diff| (deleted, diff)) }); - if let Ok(diff) = res { - all_results.push(diff.as_tuple()) + if let Ok((deleted, diff)) = res { + all_results.push(diff); + response.push_update( + Update::ItemRemoved(deleted) + ); } else { - // TODO: need to find a better way to deal with errors - return Err(diesel::result::Error::NotFound); + response.errors = Some(format!("Error selling {}", loot_id)); } } - Ok(all_results.into_iter().fold((0, 0, 0, 0), |sum, diff| { - ( - sum.0 + diff.0, - sum.1 + diff.1, - sum.2 + diff.2, - sum.3 + diff.3, - ) - })) + 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)); } - /// Adds the value in gold to the player's wealth. - /// - /// Value can be negative to substract wealth. - pub fn update_wealth(self, value_in_gp: f32) -> ActionResult<(i32, i32, i32, i32)> { - models::player::AsPlayer(self.conn, self.id) - .update_wealth(value_in_gp) - .map(|w| w.as_tuple()) - } - /// Put a claim on a specific item - pub fn claim(self, item: i32) -> ActionResult<()> { - models::claim::Claims(self.conn) - .add(self.id, item) - .map(|claim| { - dbg!("created"); - dbg!(claim); - }) - } - /// Withdraw claim - pub fn unclaim(self, item: i32) -> ActionResult<()> { - models::claim::Claims(self.conn) - .remove(self.id, item) - .map(|c| { - dbg!("deleted"); - dbg!(c); - }) - } -} - /// Wrapper for interactions of admins with the DB. pub struct AsAdmin<'q>(&'q DbConnection); diff --git a/lootalot_db/src/models/claim.rs b/lootalot_db/src/models/claim.rs index c3037c8..107a1c7 100644 --- a/lootalot_db/src/models/claim.rs +++ b/lootalot_db/src/models/claim.rs @@ -5,7 +5,7 @@ use crate::models::{self, item::Loot}; use crate::schema::claims; /// A Claim is a request by a single player on an item from group chest. -#[derive(Identifiable, Queryable, Associations, Serialize, Debug)] +#[derive(Identifiable, Queryable, Associations, Serialize, Deserialize, Debug)] #[belongs_to(Loot)] pub struct Claim { /// DB Identifier diff --git a/lootalot_db/src/models/item.rs b/lootalot_db/src/models/item.rs index 01bed64..3ac9fd4 100644 --- a/lootalot_db/src/models/item.rs +++ b/lootalot_db/src/models/item.rs @@ -47,7 +47,7 @@ type OwnedLoot = Filter; /// Represents an item that has been looted, /// hence has an owner. -#[derive(Identifiable, Debug, Queryable, Serialize)] +#[derive(Identifiable, Debug, Queryable)] #[table_name = "looted"] pub(super) struct Loot { id: i32, diff --git a/lootalot_db/src/models/player.rs b/lootalot_db/src/models/player.rs index c2598bf..2b79383 100644 --- a/lootalot_db/src/models/player.rs +++ b/lootalot_db/src/models/player.rs @@ -3,7 +3,7 @@ use crate::{DbConnection, QueryResult}; use diesel::prelude::*; /// Representation of a player in database -#[derive(Debug, Queryable, Serialize)] +#[derive(Queryable, Serialize, Deserialize, Debug)] pub struct Player { /// DB Identitier pub id: i32, @@ -91,7 +91,7 @@ fn unpack_gold_value(gold: f32) -> (i32, i32, i32, i32) { /// /// Values are held as individual pieces counts. /// Allows conversion from and to a floating amount of gold pieces. -#[derive(Queryable, AsChangeset, Debug)] +#[derive(Queryable, AsChangeset, Serialize, Deserialize, Debug)] #[table_name = "players"] pub struct Wealth { pub cp: i32,