makes ActionResult simpler, uses SQL transactions
This commit is contained in:
@@ -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)> {
|
||||||
|
self.conn.transaction(|| {
|
||||||
let new_item = models::item::NewLoot::to_player(self.id, (name, price));
|
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)
|
.values(&new_item)
|
||||||
.execute(self.conn)
|
.execute(self.conn)
|
||||||
.and_then(|r| match r {
|
.map(|rows_updated| match rows_updated {
|
||||||
1 => Ok(self.update_wealth(-(price as f32)).unwrap()),
|
1 => (),
|
||||||
_ => Ok(ActionStatus::nop()),
|
_ => 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);
|
||||||
}
|
}
|
||||||
|
self.conn.transaction(|| {
|
||||||
use schema::looted::dsl::*;
|
use schema::looted::dsl::*;
|
||||||
let loot_value = looted
|
let loot_value = looted
|
||||||
.find(loot_id)
|
.find(loot_id)
|
||||||
.select(base_price)
|
.select(base_price)
|
||||||
.first::<i32>(self.conn)?;
|
.first::<i32>(self.conn)?;
|
||||||
let sell_value = (loot_value / 2) as f32;
|
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)
|
.execute(self.conn)
|
||||||
.and_then(|r| match r {
|
.map(|rows_updated| match rows_updated {
|
||||||
// On deletion, update this player wealth
|
1 => (),
|
||||||
1 => Ok(self.update_wealth(sell_value).unwrap()),
|
_ => panic!("RuntimeError: Sell did not update DB as expected"),
|
||||||
_ => Ok(ActionStatus {
|
})?;
|
||||||
executed: false,
|
self.update_wealth(sell_value)
|
||||||
response: None,
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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<()> {
|
||||||
@@ -253,7 +224,11 @@ impl<'q> AsPlayer<'q> {
|
|||||||
.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
|
||||||
|
|||||||
Reference in New Issue
Block a user