stablizing ApiResponse inside lootalot_db crate
This commit is contained in:
@@ -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,204 +104,141 @@ 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.
|
|
||||||
///
|
|
||||||
/// 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);
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
response.push_update(Update::Wealth(wealth));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wrapper for interactions of admins with the DB.
|
/// Wrapper for interactions of admins with the DB.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user