makes ActionResult simpler, uses SQL transactions
This commit is contained in:
@@ -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)>> {
|
||||
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<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);
|
||||
}
|
||||
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))
|
||||
.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::<i32>(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<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<()> {
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user