diff --git a/lootalot_db/src/lib.rs b/lootalot_db/src/lib.rs index 304cdcb..2c4d629 100644 --- a/lootalot_db/src/lib.rs +++ b/lootalot_db/src/lib.rs @@ -4,10 +4,8 @@ //! This module wraps all needed database operations. //! It exports a public API for integration with various clients (REST Api, CLI, ...) extern crate dotenv; -#[macro_use] -extern crate diesel; -#[macro_use] -extern crate serde_derive; +#[macro_use] extern crate diesel; +#[macro_use] extern crate serde_derive; use diesel::prelude::*; use diesel::query_dsl::RunQueryDsl; @@ -23,40 +21,9 @@ 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 = QueryResult>; +pub type ActionResult = Result; -/// Return status of an API Action -#[derive(Serialize, Debug)] -pub struct ActionStatus { - /// Has the action made changes ? - pub executed: bool, - /// Response payload - pub response: R, -} -impl ActionStatus<()> { - fn was_updated(updated_lines: usize) -> Self { - match updated_lines { - 1 => Self::ok(), - _ => Self::nop(), - } - } - fn ok() -> ActionStatus<()> { - Self { - executed: true, - response: (), - } - } -} - -impl ActionStatus { - fn nop() -> ActionStatus { - Self { - executed: false, - response: Default::default(), - } - } -} /// A wrapper providing an API over the database /// It offers a convenient way to deal with connection. /// @@ -160,15 +127,19 @@ impl<'q> AsPlayer<'q> { /// /// This currently panics if player wealth fails to be updated, as this is /// a serious error. TODO: handle deletion of bought item in case of wealth update failure. - pub fn buy<'a>(self, name: &'a str, price: i32) -> ActionResult> { - let new_item = models::item::NewLoot::to_player(self.id, (name, price)); - diesel::insert_into(schema::looted::table) - .values(&new_item) - .execute(self.conn) - .and_then(|r| match r { - 1 => Ok(self.update_wealth(-(price as f32)).unwrap()), - _ => Ok(ActionStatus::nop()), - }) + pub fn buy<'a>(self, name: &'a str, price: i32) -> ActionResult<(i32, i32, i32, i32)> { + self.conn.transaction(|| { + let new_item = models::item::NewLoot::to_player(self.id, (name, price)); + let _item_added = diesel::insert_into(schema::looted::table) + .values(&new_item) + .execute(self.conn) + .map(|rows_updated| match rows_updated { + 1 => (), + _ => panic!("RuntimeError: Buy made no changes at all"), + })?; + self.update_wealth(-(price as f32)) + }) + } /// Sell an item from this player chest /// @@ -180,35 +151,35 @@ impl<'q> AsPlayer<'q> { self, loot_id: i32, _price_mod: Option, - ) -> ActionResult> { + ) -> ActionResult<(i32, i32, i32, i32)> { // Check that the item belongs to player let exists_and_owned: bool = diesel::select(models::Loot::owns(self.id, loot_id)).get_result(self.conn)?; if !exists_and_owned { - return Ok(ActionStatus::nop()); + return Err(diesel::result::Error::NotFound); } - use schema::looted::dsl::*; - let loot_value = looted - .find(loot_id) - .select(base_price) - .first::(self.conn)?; - let sell_value = (loot_value / 2) as f32; - diesel::delete(looted.find(loot_id)) - .execute(self.conn) - .and_then(|r| match r { - // On deletion, update this player wealth - 1 => Ok(self.update_wealth(sell_value).unwrap()), - _ => Ok(ActionStatus { - executed: false, - response: None, - }), - }) + self.conn.transaction(|| { + use schema::looted::dsl::*; + let loot_value = looted + .find(loot_id) + .select(base_price) + .first::(self.conn)?; + let sell_value = (loot_value / 2) as f32; + let _deleted = diesel::delete(looted.find(loot_id)) + .execute(self.conn) + .map(|rows_updated| match rows_updated { + 1 => (), + _ => panic!("RuntimeError: Sell did not update DB as expected"), + })?; + self.update_wealth(sell_value) + }) + } /// 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> { + pub fn update_wealth(self, value_in_gp: f32) -> ActionResult<(i32, i32, i32, i32)> { use schema::players::dsl::*; let current_wealth = players .find(self.id) @@ -225,24 +196,24 @@ impl<'q> AsPlayer<'q> { .set(&updated_wealth) .execute(self.conn) .map(|r| match r { - 1 => ActionStatus { - executed: true, - response: Some(diff), - }, - _ => ActionStatus::nop(), + 1 => diff, + _ => panic!("RuntimeError: UpdateWealth did no changes at all!"), }) } /// Put a claim on a specific item pub fn claim(self, item: i32) -> ActionResult<()> { let exists: bool = diesel::select(models::Loot::exists(item)).get_result(self.conn)?; if !exists { - return Ok(ActionStatus::nop()); + return Err(diesel::result::Error::NotFound); }; let claim = models::claim::NewClaim::new(self.id, item); diesel::insert_into(schema::claims::table) .values(&claim) .execute(self.conn) - .map(ActionStatus::was_updated) + .map(|rows_updated| match rows_updated { + 1 => (), + _ => panic!("RuntimeError: Claim did no change at all!"), + }) } /// Withdraw claim pub fn unclaim(self, item: i32) -> ActionResult<()> { @@ -251,9 +222,13 @@ impl<'q> AsPlayer<'q> { claims .filter(loot_id.eq(item)) .filter(player_id.eq(self.id)), - ) - .execute(self.conn) - .map(ActionStatus::was_updated) + ) + .execute(self.conn) + .and_then(|rows_updated| match rows_updated { + 1 => Ok(()), + 0 => Err(diesel::result::Error::NotFound), + _ => panic!("RuntimeError: UnclaimItem did not make expected changes"), + }) } } @@ -268,7 +243,10 @@ impl<'q> AsAdmin<'q> { diesel::insert_into(schema::players::table) .values(&models::player::NewPlayer::create(&name, start_wealth)) .execute(self.0) - .map(ActionStatus::was_updated) + .map(|rows_updated| match rows_updated { + 1 => (), + _ => panic!("RuntimeError: UnclaimItem did not make expected changes"), + }) } /// Adds a list of items to the group loot @@ -279,7 +257,7 @@ impl<'q> AsAdmin<'q> { .values(&new_item) .execute(self.0)?; } - Ok(ActionStatus::ok()) + Ok(()) } /// Resolve all pending claims and dispatch claimed items. @@ -296,7 +274,7 @@ impl<'q> AsAdmin<'q> { dbg!(data); // If mutiples claims -> find highest resolve, give to this player // If only one claim -> give to claiming - Ok(ActionStatus::nop()) + Err(diesel::result::Error::NotFound) } } @@ -347,11 +325,10 @@ mod tests { let diff = DbApi::with_conn(&conn) .as_player(1) .update_wealth(-401.21) - .unwrap() - .response - .unwrap(); + .ok(); // Check the returned diff - assert_eq!(diff, (-1, -2, -1, -4)); + assert_eq!(diff, Some((-1, -2, -1, -4))); + let diff = diff.unwrap(); let players = DbApi::with_conn(&conn).fetch_players().unwrap(); let player = players.get(1).unwrap(); // Check that we can add old value to return diff to get resulting value @@ -366,9 +343,8 @@ mod tests { let conn = test_connection(); let result = DbApi::with_conn(&conn) .as_admin() - .add_player("PlayerName".to_string(), 403.21) - .unwrap(); - assert_eq!(result.executed, true); + .add_player("PlayerName".to_string(), 403.21); + assert_eq!(result.is_ok(), true); let players = DbApi::with_conn(&conn).fetch_players().unwrap(); assert_eq!(players.len(), 2); let new_player = players.get(1).unwrap(); @@ -399,16 +375,16 @@ mod tests { .add_loot(vec![("Épée", 25)]) .unwrap(); // Claim an existing item - let result = DbApi::with_conn(&conn).as_player(1).claim(1).unwrap(); - assert_eq!(result.executed, true); + let result = DbApi::with_conn(&conn).as_player(1).claim(1); + assert_eq!(result.is_ok(), true); let claims = DbApi::with_conn(&conn).fetch_claims().unwrap(); assert_eq!(claims.len(), 1); let claim = claims.get(0).unwrap(); assert_eq!(claim.player_id, 1); assert_eq!(claim.loot_id, 1); // Claim an inexistant item - let result = DbApi::with_conn(&conn).as_player(1).claim(2).unwrap(); - assert_eq!(result.executed, false); + let result = DbApi::with_conn(&conn).as_player(1).claim(2); + assert_eq!(result.is_ok(), false); } #[test] @@ -423,13 +399,17 @@ mod tests { .add_loot(vec![("Épée", 25)]) .unwrap(); // Claim an existing item - let result = DbApi::with_conn(&conn).as_player(1).claim(1).unwrap(); - assert_eq!(result.executed, true); - let result = DbApi::with_conn(&conn).as_player(1).unclaim(1).unwrap(); - assert_eq!(result.executed, true); - // Check that unclaimed items will not be unclaimed... - let result = DbApi::with_conn(&conn).as_player(1).unclaim(1).unwrap(); - assert_eq!(result.executed, false); + let result = DbApi::with_conn(&conn).as_player(1).claim(1); + assert_eq!(result.is_ok(), true); + // Claiming twice is an error + let result = DbApi::with_conn(&conn).as_player(1).claim(1); + assert_eq!(result.is_ok(), false); + // Unclaiming and item + let result = DbApi::with_conn(&conn).as_player(1).unclaim(1); + assert_eq!(result.is_ok(), true); + // Check that not claimed items will not be unclaimed... + let result = DbApi::with_conn(&conn).as_player(1).unclaim(1); + assert_eq!(result.is_ok(), false); let claims = DbApi::with_conn(&conn).fetch_claims().unwrap(); assert_eq!(claims.len(), 0); } @@ -448,10 +428,8 @@ mod tests { // Buy an item let bought = DbApi::with_conn(&conn) .as_player(1) - .buy("Sword", 800) - .unwrap(); - assert_eq!(bought.executed, true); // Was updated ? - assert_eq!(bought.response, Some((0, 0, 0, -8))); // Returns diff of player wealth ? + .buy("Sword", 800); + assert_eq!(bought.ok(), Some((0, 0, 0, -8))); // Returns diff of player wealth ? let chest = DbApi::with_conn(&conn).as_player(1).loot().unwrap(); assert_eq!(chest.len(), 1); let loot = chest.get(0).unwrap(); @@ -463,10 +441,8 @@ mod tests { // Sell back let sold = DbApi::with_conn(&conn) .as_player(1) - .sell(loot.id, None) - .unwrap(); - assert_eq!(sold.executed, true); - assert_eq!(sold.response, Some((0, 0, 0, 4))); + .sell(loot.id, None); + assert_eq!(sold.ok(), Some((0, 0, 0, 4))); let chest = DbApi::with_conn(&conn).as_player(1).loot().unwrap(); assert_eq!(chest.len(), 0); let players = DbApi::with_conn(&conn).fetch_players().unwrap(); @@ -484,9 +460,8 @@ mod tests { let loot_to_add = vec![("Cape d'invisibilité", 8000), ("Arc long", 25)]; let result = DbApi::with_conn(&conn) .as_admin() - .add_loot(loot_to_add.clone()) - .unwrap(); - assert_eq!(result.executed, true); + .add_loot(loot_to_add.clone()); + assert_eq!(result.is_ok(), true); let looted = DbApi::with_conn(&conn).as_player(0).loot().unwrap(); assert_eq!(looted.len(), 2); // NB: Not a problem now, but this adds constraints of items being