stablizing ApiResponse inside lootalot_db crate

This commit is contained in:
2019-10-16 22:29:38 +02:00
parent 8af7790d17
commit 8cfa21eccf
4 changed files with 170 additions and 189 deletions

View File

@@ -20,7 +20,7 @@ mod schema;
pub use models::{
claim::{Claim, Claims},
item::{Item, LootManager},
player::{Player, Players},
player::{Player, Players, Wealth},
};
/// The connection used
@@ -32,13 +32,57 @@ pub type QueryResult<T> = Result<T, diesel::result::Error>;
/// The result of an action provided by DbApi
pub type ActionResult<R> = Result<R, diesel::result::Error>;
#[derive(Serialize, Deserialize, Debug)]
enum Update {
NoUpdate,
Wealth(Wealth),
ItemAdded(Item),
ItemRemoved(Item),
ClaimAdded(Claim),
ClaimRemoved(Claim),
}
#[derive(Serialize, Deserialize, Debug)]
enum Value {
Item(Item),
Claim(Claim),
ItemList(Vec<Item>),
ClaimList(Vec<Claim>),
PlayerList(Vec<Player>),
}
#[derive(Serialize, Deserialize, Debug, Default)]
pub struct ApiResponse {
pub value: Option<Value>, // The value requested, if any
pub notify: Option<String>, // A text to notify user, if relevant
pub updates: Option<Vec<Update>>, // A list of updates, if any
pub errors: Option<String>, // A text describing errors, if any
}
impl ApiResponse {
fn push_update(&mut self, update: Update) {
if let Some(v) = self.updates.as_mut() {
v.push(update);
} else {
self.updates = Some(vec![update]);
}
}
fn set_value(&mut self, value: Value) {
self.value = Some(value);
}
fn notifiy<S: Into<String>>(&mut self, text: S) {
self.notify = Some(text.into());
}
}
pub enum ApiError {
DieselError(diesel::result::Error),
InvalidAction(String),
}
pub type ApiResult<R> = Result<R, ApiError>;
pub enum ApiActions<'a> {
FetchPlayers,
FetchInventory,
@@ -60,206 +104,143 @@ pub enum AdminActions {
//SetClaimsTimeout(pub i32),
}
/// A wrapper providing an API over the database
/// It offers a convenient way to deal with connection.
///
/// # Note
/// All methods consumes the DbApi, so that only one action
/// can be performed using a single instance.
///
/// # Todo list
/// ```text
/// v .as_player()
/// // Needs an action's history (one entry only should be enough)
/// x .undo_last_action() -> Success status
/// v .as_admin()
/// // When adding loot, an identifier should be used to build some kind of history
/// vx .add_loot(identifier, [items_desc]) -> Success status
/// x .sell_loot([players], [excluded_item_ids]) -> Success status (bool, player_share)
/// // Claims should be resolved after a certain delay
/// x .set_claims_timeout()
/// v .resolve_claims()
/// v .add_player(player_data)
/// ```
///
pub struct DbApi<'q>(&'q DbConnection);
impl<'q> DbApi<'q> {
/// Returns a DbApi using the user given connection
///
/// # Usage
/// ```
/// use lootalot_db::{DbConnection, DbApi};
/// # use diesel::connection::Connection;
/// let conn = DbConnection::establish(":memory:").unwrap();
/// let api = DbApi::with_conn(&conn);
/// ```
pub fn with_conn(conn: &'q DbConnection) -> Self {
Self(conn)
}
/// Fetch the list of all players
pub fn fetch_players(self) -> QueryResult<Vec<models::Player>> {
Ok(schema::players::table.load::<models::Player>(self.0)?)
}
/// Fetch the inventory of items
///
/// TODO: remove limit used for debug
pub fn fetch_inventory(self) -> QueryResult<Vec<models::Item>> {
models::item::Inventory(self.0).all()
}
/// Fetch all existing claims
pub fn fetch_claims(self) -> QueryResult<Vec<models::Claim>> {
Ok(schema::claims::table.load::<models::Claim>(self.0)?)
}
/// Wrapper for acting as a specific player
///
/// # Usage
/// ```
/// # use lootalot_db::{DbConnection, DbApi};
/// # use diesel::connection::Connection;
/// # let conn = DbConnection::establish(":memory:").unwrap();
/// # let api = DbApi::with_conn(&conn);
/// let player_id: i32 = 1; // Id that references player in DB
/// let player = api.as_player(player_id);
/// ```
pub fn as_player(self, id: i32) -> AsPlayer<'q> {
AsPlayer { id, conn: self.0 }
}
/// Wrapper for acting as the admin
pub fn as_admin(self) -> AsAdmin<'q> {
AsAdmin(self.0)
}
}
/// A wrapper for interactions of players with the database.
/// Possible actions are exposed as methods
pub struct AsPlayer<'q> {
id: i32,
conn: &'q DbConnection,
}
impl<'q> AsPlayer<'q> {
/// Fetch the content of a player's chest
///
/// # Usage
/// ```
/// # extern crate diesel_migrations;
/// # use lootalot_db::{DbConnection, DbApi};
/// # use diesel::connection::Connection;
/// # let conn = DbConnection::establish(":memory:").unwrap();
/// # diesel_migrations::run_pending_migrations(&conn).unwrap();
/// # let api = DbApi::with_conn(&conn);
/// // Get loot of player with id of 1
/// let loot = api.as_player(1).loot().unwrap();
/// assert_eq!(format!("{:?}", loot), "[]".to_string());
/// ```
pub fn loot(self) -> QueryResult<Vec<models::Item>> {
models::item::LootManager(self.conn, self.id).all()
}
/// Buy a batch of items and add them to this player chest
///
/// Items can only be bought from inventory. Hence, the use
/// of the entity's id in 'items' table.
///
/// # Params
/// List of (Item's id in inventory, Option<Price modifier>)
///
/// # Returns
/// Result containing the difference in coins after operation
pub fn buy<'a>(
self,
params: &Vec<(i32, Option<f32>)>,
) -> ActionResult<(Vec<models::Item>, (i32, i32, i32, i32))> {
let mut cumulated_diff: Vec<(i32, i32, i32, i32)> = Vec::with_capacity(params.len());
let mut added_items: Vec<models::Item> = Vec::with_capacity(params.len());
for (item_id, price_mod) in params.into_iter() {
if let Ok((item, diff)) = self.conn.transaction(|| {
// Find item in inventory
let item = models::item::Inventory(self.conn).find(*item_id)?;
let new_item = models::item::LootManager(self.conn, self.id).add_from(&item)?;
let sell_price = match price_mod {
Some(modifier) => item.base_price as f32 * modifier,
None => item.base_price as f32,
};
models::player::AsPlayer(self.conn, self.id)
.update_wealth(-sell_price)
.map(|diff| (new_item, diff.as_tuple()))
}) {
cumulated_diff.push(diff);
added_items.push(item);
}
pub fn execute<'a>(pool: Pool, query: ApiActions<'a>) -> Result<ApiResponse, diesel::result::Error> {
let conn = pool.get().map_err(|e| {dbg!(e); diesel::result::Error::NotFound })?;
let mut response = ApiResponse::default();
match query {
ApiActions::FetchPlayers => {
response.set_value(
Value::PlayerList(
schema::players::table.load::<models::Player>(conn)?
)
);
},
ApiActions::FetchInventory => {
response.set_value(
Value::ItemList(
models::item::Inventory(conn).all()?));
}
let all_diff = cumulated_diff.into_iter().fold((0, 0, 0, 0), |sum, diff| {
(
sum.0 + diff.0,
sum.1 + diff.1,
sum.2 + diff.2,
sum.3 + diff.3,
)
});
Ok((added_items, all_diff))
ApiActions::FetchLoot(id) => {
response.set_value(
Value::ItemList(
models::item::LootManager(conn, id).all()?
)
);
},
ApiActions::UpdateWealth(id, amount) => {
response.push_update(
Update::Wealth(
models::player::AsPlayer(conn, id)
.update_wealth(amount)?
)
);
},
ApiActions::BuyItems(id, params) => {
let mut cumulated_diff: Vec<Wealth> = Vec::with_capacity(params.len());
let mut added_items: Vec<models::Item> = Vec::with_capacity(params.len());
for (item_id, price_mod) in params.into_iter() {
// Use a transaction to avoid incoherant state in case of error
if let Ok((item, diff)) = conn.transaction(|| {
// Find item in inventory
let item = models::item::Inventory(conn).find(*item_id)?;
let new_item = models::item::LootManager(conn, id).add_from(&item)?;
let sell_price = match price_mod {
Some(modifier) => item.base_price as f32 * modifier,
None => item.base_price as f32,
};
models::player::AsPlayer(conn, id)
.update_wealth(-sell_price)
.map(|diff| (new_item, diff))
}) {
cumulated_diff.push(diff);
response.push_update(Update::ItemAdded(item));
} else {
response.errors = Some(format!("Error adding {}", item_id));
}
}
let all_diff = cumulated_diff.into_iter().fold(Wealth::from_gp(0.0), |sum, diff|
Wealth {
cp: sum.cp + diff.cp,
sp: sum.sp + diff.sp,
gp: sum.gp + diff.gp,
pp: sum.pp + diff.pp,
}
);
response.push_update(Update::Wealth(all_diff));
},
ApiActions::SellItems(id, params) => {
sell(conn, id, params, &mut response);
},
ApiActions::ClaimItem(id, item) => {
response.push_update(
Update::ClaimAdded(
models::claim::Claims(conn)
.add(id, item)?
)
);
},
ApiActions::UnclaimItem(id, item) => {
response.push_update(
Update::ClaimRemoved(
models::claim::Claims(conn)
.remove(id, item)?
)
);
},
// Group actions
ApiActions::AddLoot(items) => {},
}
Ok(response)
}
/// Fetch all existing claims
pub fn fetch_claims(conn: &DbConnection) -> QueryResult<Vec<models::Claim>> {
schema::claims::table.load::<models::Claim>(conn)
}
/// Sell a set of items from this player chest
///
/// # Returns
/// Result containing the difference in coins after operation
pub fn sell(self, params: &Vec<(i32, Option<f32>)>) -> ActionResult<(i32, i32, i32, i32)> {
let mut all_results: Vec<(i32, i32, i32, i32)> = Vec::with_capacity(params.len());
pub fn sell(
conn: &DbConnection,
id: i32,
params: &Vec<(i32, Option<f32>)>,
response: &mut ApiResponse,
) {
let mut all_results: Vec<Wealth> = Vec::with_capacity(params.len());
for (loot_id, price_mod) in params.into_iter() {
let res = self.conn.transaction(|| {
let deleted = models::item::LootManager(self.conn, self.id).remove(*loot_id)?;
let res = conn.transaction(|| {
let deleted = models::item::LootManager(conn, id).remove(*loot_id)?;
let mut sell_value = deleted.base_price as f32 / 2.0;
if let Some(modifier) = price_mod {
sell_value *= modifier;
}
models::player::AsPlayer(self.conn, self.id).update_wealth(sell_value)
models::player::AsPlayer(conn, id)
.update_wealth(sell_value)
.map(|diff| (deleted, diff))
});
if let Ok(diff) = res {
all_results.push(diff.as_tuple())
if let Ok((deleted, diff)) = res {
all_results.push(diff);
response.push_update(
Update::ItemRemoved(deleted)
);
} else {
// TODO: need to find a better way to deal with errors
return Err(diesel::result::Error::NotFound);
response.errors = Some(format!("Error selling {}", loot_id));
}
}
Ok(all_results.into_iter().fold((0, 0, 0, 0), |sum, diff| {
(
sum.0 + diff.0,
sum.1 + diff.1,
sum.2 + diff.2,
sum.3 + diff.3,
)
}))
let wealth = all_results.into_iter().fold(Wealth::from_gp(0.0), |sum, diff| {
Wealth {
cp: sum.cp + diff.cp,
sp: sum.sp + diff.sp,
gp: sum.gp + diff.gp,
pp: sum.pp + diff.pp,
}
});
response.push_update(Update::Wealth(wealth));
}
/// 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<(i32, i32, i32, i32)> {
models::player::AsPlayer(self.conn, self.id)
.update_wealth(value_in_gp)
.map(|w| w.as_tuple())
}
/// Put a claim on a specific item
pub fn claim(self, item: i32) -> ActionResult<()> {
models::claim::Claims(self.conn)
.add(self.id, item)
.map(|claim| {
dbg!("created");
dbg!(claim);
})
}
/// Withdraw claim
pub fn unclaim(self, item: i32) -> ActionResult<()> {
models::claim::Claims(self.conn)
.remove(self.id, item)
.map(|c| {
dbg!("deleted");
dbg!(c);
})
}
}
/// Wrapper for interactions of admins with the DB.
pub struct AsAdmin<'q>(&'q DbConnection);