makes ActionResult simpler, uses SQL transactions

This commit is contained in:
2019-07-24 15:07:04 +02:00
parent 51a3d00d03
commit 89172177eb

View File

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