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::{ pub use models::{
claim::{Claim, Claims}, claim::{Claim, Claims},
item::{Item, LootManager}, item::{Item, LootManager},
player::{Player, Players}, player::{Player, Players, Wealth},
}; };
/// The connection used /// The connection used
@@ -32,13 +32,57 @@ 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> = Result<R, diesel::result::Error>; 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 { pub enum ApiError {
DieselError(diesel::result::Error), DieselError(diesel::result::Error),
InvalidAction(String), InvalidAction(String),
} }
pub type ApiResult<R> = Result<R, ApiError>;
pub enum ApiActions<'a> { pub enum ApiActions<'a> {
FetchPlayers, FetchPlayers,
FetchInventory, FetchInventory,
@@ -60,205 +104,142 @@ pub enum AdminActions {
//SetClaimsTimeout(pub i32), //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> { pub fn execute<'a>(pool: Pool, query: ApiActions<'a>) -> Result<ApiResponse, diesel::result::Error> {
/// Returns a DbApi using the user given connection let conn = pool.get().map_err(|e| {dbg!(e); diesel::result::Error::NotFound })?;
/// let mut response = ApiResponse::default();
/// # Usage match query {
/// ``` ApiActions::FetchPlayers => {
/// use lootalot_db::{DbConnection, DbApi}; response.set_value(
/// # use diesel::connection::Connection; Value::PlayerList(
/// let conn = DbConnection::establish(":memory:").unwrap(); schema::players::table.load::<models::Player>(conn)?
/// let api = DbApi::with_conn(&conn); )
/// ``` );
pub fn with_conn(conn: &'q DbConnection) -> Self { },
Self(conn) ApiActions::FetchInventory => {
response.set_value(
Value::ItemList(
models::item::Inventory(conn).all()?));
} }
/// Fetch the list of all players ApiActions::FetchLoot(id) => {
pub fn fetch_players(self) -> QueryResult<Vec<models::Player>> { response.set_value(
Ok(schema::players::table.load::<models::Player>(self.0)?) Value::ItemList(
} models::item::LootManager(conn, id).all()?
/// Fetch the inventory of items )
/// );
/// TODO: remove limit used for debug },
pub fn fetch_inventory(self) -> QueryResult<Vec<models::Item>> { ApiActions::UpdateWealth(id, amount) => {
models::item::Inventory(self.0).all() response.push_update(
} Update::Wealth(
/// Fetch all existing claims models::player::AsPlayer(conn, id)
pub fn fetch_claims(self) -> QueryResult<Vec<models::Claim>> { .update_wealth(amount)?
Ok(schema::claims::table.load::<models::Claim>(self.0)?) )
} );
/// Wrapper for acting as a specific player },
/// ApiActions::BuyItems(id, params) => {
/// # Usage let mut cumulated_diff: Vec<Wealth> = Vec::with_capacity(params.len());
/// ```
/// # 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()); let mut added_items: Vec<models::Item> = Vec::with_capacity(params.len());
for (item_id, price_mod) in params.into_iter() { for (item_id, price_mod) in params.into_iter() {
if let Ok((item, diff)) = self.conn.transaction(|| { // Use a transaction to avoid incoherant state in case of error
if let Ok((item, diff)) = conn.transaction(|| {
// Find item in inventory // Find item in inventory
let item = models::item::Inventory(self.conn).find(*item_id)?; let item = models::item::Inventory(conn).find(*item_id)?;
let new_item = models::item::LootManager(self.conn, self.id).add_from(&item)?; let new_item = models::item::LootManager(conn, id).add_from(&item)?;
let sell_price = match price_mod { let sell_price = match price_mod {
Some(modifier) => item.base_price as f32 * modifier, Some(modifier) => item.base_price as f32 * modifier,
None => item.base_price as f32, None => item.base_price as f32,
}; };
models::player::AsPlayer(self.conn, self.id) models::player::AsPlayer(conn, id)
.update_wealth(-sell_price) .update_wealth(-sell_price)
.map(|diff| (new_item, diff.as_tuple())) .map(|diff| (new_item, diff))
}) { }) {
cumulated_diff.push(diff); cumulated_diff.push(diff);
added_items.push(item); response.push_update(Update::ItemAdded(item));
} else {
response.errors = Some(format!("Error adding {}", item_id));
} }
} }
let all_diff = cumulated_diff.into_iter().fold((0, 0, 0, 0), |sum, diff| { let all_diff = cumulated_diff.into_iter().fold(Wealth::from_gp(0.0), |sum, diff|
( Wealth {
sum.0 + diff.0, cp: sum.cp + diff.cp,
sum.1 + diff.1, sp: sum.sp + diff.sp,
sum.2 + diff.2, gp: sum.gp + diff.gp,
sum.3 + diff.3, 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)?
) )
}); );
Ok((added_items, all_diff)) },
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 /// Sell a set of items from this player chest
/// ///
/// # Returns /// # Returns
/// Result containing the difference in coins after operation /// Result containing the difference in coins after operation
pub fn sell(self, params: &Vec<(i32, Option<f32>)>) -> ActionResult<(i32, i32, i32, i32)> { pub fn sell(
let mut all_results: Vec<(i32, i32, i32, i32)> = Vec::with_capacity(params.len()); 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() { for (loot_id, price_mod) in params.into_iter() {
let res = self.conn.transaction(|| { let res = conn.transaction(|| {
let deleted = models::item::LootManager(self.conn, self.id).remove(*loot_id)?; let deleted = models::item::LootManager(conn, id).remove(*loot_id)?;
let mut sell_value = deleted.base_price as f32 / 2.0; let mut sell_value = deleted.base_price as f32 / 2.0;
if let Some(modifier) = price_mod { if let Some(modifier) = price_mod {
sell_value *= modifier; 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 { if let Ok((deleted, diff)) = res {
all_results.push(diff.as_tuple()) all_results.push(diff);
response.push_update(
Update::ItemRemoved(deleted)
);
} else { } else {
// TODO: need to find a better way to deal with errors response.errors = Some(format!("Error selling {}", loot_id));
return Err(diesel::result::Error::NotFound);
} }
} }
Ok(all_results.into_iter().fold((0, 0, 0, 0), |sum, diff| { let wealth = all_results.into_iter().fold(Wealth::from_gp(0.0), |sum, diff| {
( Wealth {
sum.0 + diff.0, cp: sum.cp + diff.cp,
sum.1 + diff.1, sp: sum.sp + diff.sp,
sum.2 + diff.2, gp: sum.gp + diff.gp,
sum.3 + diff.3, pp: sum.pp + diff.pp,
)
}))
} }
});
/// Adds the value in gold to the player's wealth. response.push_update(Update::Wealth(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. /// Wrapper for interactions of admins with the DB.
pub struct AsAdmin<'q>(&'q DbConnection); pub struct AsAdmin<'q>(&'q DbConnection);

View File

@@ -5,7 +5,7 @@ use crate::models::{self, item::Loot};
use crate::schema::claims; use crate::schema::claims;
/// A Claim is a request by a single player on an item from group chest. /// A Claim is a request by a single player on an item from group chest.
#[derive(Identifiable, Queryable, Associations, Serialize, Debug)] #[derive(Identifiable, Queryable, Associations, Serialize, Deserialize, Debug)]
#[belongs_to(Loot)] #[belongs_to(Loot)]
pub struct Claim { pub struct Claim {
/// DB Identifier /// DB Identifier

View File

@@ -47,7 +47,7 @@ type OwnedLoot = Filter<looted::table, WithOwner>;
/// Represents an item that has been looted, /// Represents an item that has been looted,
/// hence has an owner. /// hence has an owner.
#[derive(Identifiable, Debug, Queryable, Serialize)] #[derive(Identifiable, Debug, Queryable)]
#[table_name = "looted"] #[table_name = "looted"]
pub(super) struct Loot { pub(super) struct Loot {
id: i32, id: i32,

View File

@@ -3,7 +3,7 @@ use crate::{DbConnection, QueryResult};
use diesel::prelude::*; use diesel::prelude::*;
/// Representation of a player in database /// Representation of a player in database
#[derive(Debug, Queryable, Serialize)] #[derive(Queryable, Serialize, Deserialize, Debug)]
pub struct Player { pub struct Player {
/// DB Identitier /// DB Identitier
pub id: i32, pub id: i32,
@@ -91,7 +91,7 @@ fn unpack_gold_value(gold: f32) -> (i32, i32, i32, i32) {
/// ///
/// Values are held as individual pieces counts. /// Values are held as individual pieces counts.
/// Allows conversion from and to a floating amount of gold pieces. /// Allows conversion from and to a floating amount of gold pieces.
#[derive(Queryable, AsChangeset, Debug)] #[derive(Queryable, AsChangeset, Serialize, Deserialize, Debug)]
#[table_name = "players"] #[table_name = "players"]
pub struct Wealth { pub struct Wealth {
pub cp: i32, pub cp: i32,