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.
//! 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<ConnectionManager<DbConnection>>;
/// The result of a query on DB
pub type QueryResult<T> = Result<T, diesel::result::Error>;
/// 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
/// 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<Option<(i32, i32, i32, i32)>> {
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));
diesel::insert_into(schema::looted::table)
let _item_added = 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()),
.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<f32>,
) -> ActionResult<Option<(i32, i32, i32, i32)>> {
) -> 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);
}
self.conn.transaction(|| {
use schema::looted::dsl::*;
let loot_value = looted
.find(loot_id)
.select(base_price)
.first::<i32>(self.conn)?;
let sell_value = (loot_value / 2) as f32;
diesel::delete(looted.find(loot_id))
let _deleted = 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,
}),
.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<Option<(i32, i32, i32, i32)>> {
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<()> {
@@ -253,7 +224,11 @@ impl<'q> AsPlayer<'q> {
.filter(player_id.eq(self.id)),
)
.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)
.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