From e9f535ac86e2d6cbe854b9dc9c0061c99bac186c Mon Sep 17 00:00:00 2001 From: Artus Date: Mon, 28 Oct 2019 15:27:26 +0100 Subject: [PATCH] impls undoable events --- lootalot_db/src/lib.rs | 22 +++++++++-- lootalot_db/src/models/history.rs | 27 +++++++++++++ lootalot_db/src/models/player/wealth.rs | 50 ++++++++++++++++++++++++- src/api.rs | 41 ++++++++++++-------- src/server.rs | 9 ++++- 5 files changed, 128 insertions(+), 21 deletions(-) diff --git a/lootalot_db/src/lib.rs b/lootalot_db/src/lib.rs index a24dbdc..0a7d0b2 100644 --- a/lootalot_db/src/lib.rs +++ b/lootalot_db/src/lib.rs @@ -20,6 +20,7 @@ pub use models::{ claim::{Claim, Claims}, item::{Item, LootManager, Inventory}, player::{Player, Wealth, Players, AsPlayer}, + history::Event, }; /// The connection used @@ -53,11 +54,18 @@ pub enum Update { impl Update { /// Change back what has been updated - fn undo(self) -> QueryResult<()> { + fn undo(&self, conn: &DbConnection, id: i32) -> QueryResult<()> { match self { - Update::Wealth(diff) => {}, - Update::ItemAdded(item) => {}, - Update::ItemRemoved(item) => {}, + Update::Wealth(diff) => { + AsPlayer(conn, id).update_wealth(-diff.to_gp())?; + }, + Update::ItemAdded(item) => { + LootManager(conn, id).remove(item.id)?; + }, + Update::ItemRemoved(item) => { + LootManager(conn, id).add_from(&item)?; + }, + // Unused for now Update::ClaimAdded(claim) => {}, Update::ClaimRemoved(claim) => {}, }; @@ -191,6 +199,12 @@ pub fn split_and_share(conn: &DbConnection, amount: i32, players: Vec) -> Q } +/// Reverts the last action stored for player +pub fn undo_last_action(conn: &DbConnection, id: i32) -> QueryResult { + let last_event = models::history::get_last_of_player(conn, id)?; + last_event.undo(conn) +} + #[cfg(none)] mod tests_old { diff --git a/lootalot_db/src/models/history.rs b/lootalot_db/src/models/history.rs index 5b99b93..94e465c 100644 --- a/lootalot_db/src/models/history.rs +++ b/lootalot_db/src/models/history.rs @@ -9,6 +9,9 @@ use crate::{DbConnection, QueryResult, Update}; #[derive(Debug, FromSqlRow)] pub struct UpdateList(Vec); +// TODO: decide if updates is really optionnal or not +// (like if storing an event without update is usefull ?) + /// An event in history #[derive(Debug, Queryable)] pub struct Event { @@ -19,6 +22,26 @@ pub struct Event { updates: Option, } +impl Event { + pub fn name(&self) -> &str { + &self.text + } + + /// TODO: why a move here ?? + /// Undo all updates in a single transaction + pub fn undo(self, conn: &DbConnection) -> QueryResult { + conn.transaction(move || { + if let Some(ref updates) = self.updates { + for update in updates.0.iter() { + update.undo(conn, self.player_id)?; + } + diesel::delete(history::table.find(self.id)).execute(conn)?; + } + Ok(self) + }) + } +} + impl FromSql for UpdateList where String: FromSql, @@ -38,7 +61,11 @@ struct NewEvent<'a> { updates: Option, } + /// Insert a new event +/// +/// # Warning +/// This actually swallow up conversion errors pub fn insert_event(conn: &DbConnection, id: i32, text: &str, updates: &Vec) -> QueryResult { diesel::insert_into(history::table) .values(&NewEvent { diff --git a/lootalot_db/src/models/player/wealth.rs b/lootalot_db/src/models/player/wealth.rs index 1088ac7..1903c6a 100644 --- a/lootalot_db/src/models/player/wealth.rs +++ b/lootalot_db/src/models/player/wealth.rs @@ -22,7 +22,7 @@ fn unpack_gold_value(gold: f64) -> (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, Serialize, Deserialize, Debug)] +#[derive(Queryable, AsChangeset, Serialize, Deserialize, PartialEq, Copy, Clone, Debug)] #[table_name = "players"] pub struct Wealth { pub cp: i32, @@ -116,4 +116,52 @@ mod tests { } } + #[test] + fn test_negative_wealth() { + use super::Wealth; + + assert_eq!( + Wealth{ cp: 3, sp: 2, gp: 1, pp: 0 } + Wealth{ cp: -8, pp: 0, sp: 0, gp: 0 }, + Wealth::from_gp(1.23 - 0.08) + ) + + } + #[test] + fn test_negative_wealth_inverse() { + use super::Wealth; + + assert_eq!( + (Wealth{ cp: 3, sp: 2, gp: 1, pp: 0 } + Wealth{ cp: -8, pp: 0, sp: 0, gp: 0 }).to_gp(), + 1.23 - 0.08 + ) + + } + #[test] + fn test_diff_adding() { + use super::Wealth; + + /// Let say we add 0.08 gp + /// 1.23 + 0.08 gold is 1.31, diff is cp: -2, sp: +1 + let old = Wealth::from_gp(1.23); + let new = Wealth::from_gp(1.31); + let diff = new - old; + assert_eq!(diff.as_tuple(), (-2, 1, 0, 0)); + assert_eq!(diff.to_gp(), 0.08); + assert_eq!(new - diff, old); + + } + #[test] + fn test_diff_subbing() { + use super::Wealth; + + /// Let say we sub 0.08 gp + /// 1.31 - 0.08 gold is 1.23, diff is cp: +2, sp: -1 + let old = Wealth::from_gp(1.31); + let new = Wealth::from_gp(1.23); + let diff = new - old; + assert_eq!(diff.as_tuple(), (2, -1, 0, 0)); + assert_eq!(diff.to_gp(), -0.08); + assert_eq!(new - diff, old); + } + } diff --git a/src/api.rs b/src/api.rs index 7ce89da..4b8ee42 100644 --- a/src/api.rs +++ b/src/api.rs @@ -68,6 +68,7 @@ pub enum ApiActions { SellItems(i32, SellParams), ClaimItem(i32, i32), UnclaimItem(i32, i32), + UndoLastAction(i32), // Group level AddLoot(Vec), // Admin level @@ -84,7 +85,7 @@ pub fn execute( let mut response = ApiResponse::default(); // Return an Option that describes what happened. // If there is some value, store the actions in db so that it can be reversed. - let action_text: Option<&str> = match query { + let action_text: Option<(i32, &str)> = match query { ApiActions::FetchPlayers => { response.set_value(Value::PlayerList(db::Players(conn).all()?)); None @@ -116,7 +117,7 @@ pub fn execute( db::AsPlayer(conn, id).update_wealth(amount)?, )); response.notify(format!("Mis à jour ({}po)!", amount)); - Some("Argent mis à jour") + Some((id, "Argent mis à jour")) } ApiActions::BuyItems(id, params) => { // TODO: check that player has enough money ! @@ -141,7 +142,7 @@ pub fn execute( total_amount.to_gp() )); response.push_update(Update::Wealth(total_amount)); - Some("Achat d'objets") + Some((id, "Achat d'objets")) } // Behavior differs if player is group or regular. // Group sells item like players then split the total amount among players. @@ -184,7 +185,7 @@ pub fn execute( response.push_update(Update::Wealth(total_amount)); } } - Some("Vente d'objets") + Some((id, "Vente d'objets")) } ApiActions::ClaimItem(id, item) => { response.push_update(Update::ClaimAdded(db::Claims(conn).add(id, item)?)); @@ -196,6 +197,13 @@ pub fn execute( response.notify("Bof! Finalement non.".to_string()); None } + ApiActions::UndoLastAction(id) => { + match db::undo_last_action(conn, id) { + Ok(ev) => response.notify(format!("'{}' annulé(e)", ev.name())), + Err(e) => response.push_error(format!("Erreur : {:?}", e)), + }; + None + } // Group actions ApiActions::AddLoot(items) => { let mut added_items = 0; @@ -214,20 +222,23 @@ pub fn execute( { response.push_error(format!("Erreur durant la notification : {:?}", e)); }; - Some("Nouveau loot") + Some((0, "Nouveau loot")) } }; - // match _action_text -> Save updates in DB + // Store the event if it can be undone. dbg!(&action_text); - Ok(response) -} - -/// Reverts the last action stored for player -fn revert_last_action(conn: &DbConnection, id: i32) -> Result { - let mut response = ApiResponse::default(); - // 1. Load the last action - // 2. Iterate trought updates and reverse them (in a single transaction) - // 3. Send nice message back ("'action desc text' annulé !") + if let Some((id, text)) = action_text { + db::models::history::insert_event( + conn, + id, + text, + response + .updates + .as_ref() + .expect("there should be updates in here !"), + )?; + } + // match _action_text -> Save updates in DB Ok(response) } diff --git a/src/server.rs b/src/server.rs index 6f9e1e4..1839517 100644 --- a/src/server.rs +++ b/src/server.rs @@ -112,7 +112,14 @@ fn configure_app(config: &mut web::ServiceConfig) { db_call(pool, Q::SellItems(*player, data.into_inner())) }, )), - ), + ) + .service( + web::scope("/events") + .route("/last", web::delete().to_async(|pool, player: PlayerId| { + db_call(pool, Q::UndoLastAction(*player)) + })) + ) + ), ) .route("/claims", web::get().to_async(|pool| db_call(pool, Q::FetchClaims)))