14 Commits

Author SHA1 Message Date
08f29fc90e this looks this overdoing it... going back to master 2019-07-24 13:45:03 +02:00
4f60df88d7 moves player actions code inside UserAction trait impls 2019-07-20 15:49:51 +02:00
51a3d00d03 formats code 2019-07-20 15:00:28 +02:00
0636829fdd adds doc 2019-07-17 14:59:04 +02:00
143d9bb5aa adds tests 2019-07-17 14:26:27 +02:00
21369535e8 impls more DbApi actions, adds a test 2019-07-16 14:56:54 +02:00
441b7f5ad6 little fixes 2019-07-15 16:07:25 +02:00
4b55964786 adds comments and new tests 2019-07-15 15:55:13 +02:00
caabad3982 small refactoring 2019-07-15 15:34:51 +02:00
96f8e36c97 adds test 2019-07-15 15:25:49 +02:00
58f39ae5d0 code cleanup, starts testing 2019-07-15 14:59:49 +02:00
5d54d40bec learning how to test 2019-07-12 15:56:16 +02:00
caa8d3fad6 adds docs, tests unpack_gold_value 2019-07-11 15:35:32 +02:00
e08cd64743 enhance responses from API, integrates with frontend 2019-07-11 14:54:18 +02:00
18 changed files with 3738 additions and 238 deletions

1
.gitignore vendored
View File

@@ -3,6 +3,7 @@
node_modules node_modules
fontawesome fontawesome
package-lock.json
Cargo.lock Cargo.lock
**/*.sqlite3 **/*.sqlite3

View File

@@ -6,9 +6,10 @@ edition = "2018"
[dependencies] [dependencies]
dotenv = "*" dotenv = "*"
diesel_migrations = "*"
serde = "*" serde = "*"
serde_derive = "*" serde_derive = "*"
[dependencies.diesel] [dependencies.diesel]
version = "1.0" version = "1.4"
features = ["sqlite", "r2d2"] features = ["sqlite", "r2d2"]

Binary file not shown.

View File

@@ -1,3 +1,8 @@
//! Loot-a-lot Database module
//!
//! # Description
//! This module wraps all needed database operations.
//! 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;
@@ -5,40 +10,74 @@ extern crate diesel;
extern crate serde_derive; extern crate serde_derive;
use diesel::prelude::*; use diesel::prelude::*;
use diesel::r2d2::{self, ConnectionManager};
use diesel::query_dsl::RunQueryDsl; use diesel::query_dsl::RunQueryDsl;
use diesel::r2d2::{self, ConnectionManager};
mod models; mod transactions;
pub mod models;
mod schema; mod schema;
use transactions::{DbTransaction};
/// The connection used
pub type DbConnection = SqliteConnection; pub type DbConnection = SqliteConnection;
/// A pool of connections
pub type Pool = r2d2::Pool<ConnectionManager<DbConnection>>; pub type Pool = r2d2::Pool<ConnectionManager<DbConnection>>;
/// 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>;
pub type ActionResult = QueryResult<bool>; pub type ActionResult<T> = QueryResult<ActionStatus<T>>;
/// Return status of an 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<()> {
pub fn was_updated(updated_lines: usize) -> Self {
match updated_lines {
1 => Self::ok(),
_ => Self::nop(),
}
}
pub fn ok() -> ActionStatus<()> {
Self {
executed: true,
response: (),
}
}
}
impl<T: Default + serde::Serialize> ActionStatus<T> {
pub 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.
///
/// # Note
/// All methods consumes the DbApi, so that only one action
/// can be performed using a single instance.
/// ///
/// # Todo list /// # Todo list
/// /// ```text
/// struct DbApi<'q>(&'q DbConnection); /// v .as_player()
/// ::new() -> DbApi<'q> (Db finds a connection by itself, usefull for cli) /// // Needs an action's history (one entry only should be enough)
/// ::with_conn(conn) -> DbApi<'q> (uses a user-defined connection) /// x .undo_last_action() -> Success status
/// v .fetch_players()
/// v .fetch_inventory()
/// v .fetch_claims()
/// v .as_player(player_id) -> AsPlayer<'q>
/// v .loot() -> List of items owned (Vec<Item>)
/// v .claim(loot_id) -> Success status (bool)
/// v .unclaim(loot_id) -> Success status (bool)
/// x .sell(loot_id) -> Success status (bool, earned)
/// x .buy(item_desc) -> Success status (bool, cost)
/// v .update_wealth(value_in_gold) -> Success status (bool, new_wealth)
/// v .as_admin() /// v .as_admin()
/// x .add_loot(identifier, [items_desc]) -> Success status /// // 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) /// x .sell_loot([players], [excluded_item_ids]) -> Success status (bool, player_share)
/// // Claims should be resolved after a certain delay
/// x .set_claims_timeout()
/// x .resolve_claims() /// x .resolve_claims()
/// v .add_player(player_data) /// v .add_player(player_data)
/// ```
/// ///
pub struct DbApi<'q>(&'q DbConnection); pub struct DbApi<'q>(&'q DbConnection);
@@ -47,43 +86,34 @@ impl<'q> DbApi<'q> {
/// ///
/// # Usage /// # Usage
/// ``` /// ```
/// let conn = DbConnection::establish(); /// use lootalot_db::{DbConnection, DbApi};
/// # use diesel::connection::Connection;
/// let conn = DbConnection::establish(":memory:").unwrap();
/// let api = DbApi::with_conn(&conn); /// let api = DbApi::with_conn(&conn);
/// ``` /// ```
pub fn with_conn(conn: &'q DbConnection) -> Self { pub fn with_conn(conn: &'q DbConnection) -> Self {
Self(conn) Self(conn)
} }
/// Fetch the list of all players /// Fetch the list of all players
///
/// This method consumes the DbApi object.
pub fn fetch_players(self) -> QueryResult<Vec<models::Player>> { pub fn fetch_players(self) -> QueryResult<Vec<models::Player>> {
Ok( Ok(schema::players::table.load::<models::Player>(self.0)?)
schema::players::table
.load::<models::Player>(self.0)?
)
} }
/// Fetch the inventory of items /// Fetch the inventory of items
///
/// Consumes the DbApi instance
pub fn fetch_inventory(self) -> QueryResult<Vec<models::Item>> { pub fn fetch_inventory(self) -> QueryResult<Vec<models::Item>> {
Ok( Ok(schema::items::table.load::<models::Item>(self.0)?)
schema::items::table
.load::<models::Item>(self.0)?
)
} }
/// Fetch all existing claims
pub fn fetch_claims(self) -> QueryResult<Vec<models::Claim>> { pub fn fetch_claims(self) -> QueryResult<Vec<models::Claim>> {
Ok( Ok(schema::claims::table.load::<models::Claim>(self.0)?)
schema::claims::table
.load::<models::Claim>(self.0)?
)
} }
/// Wrapper for acting as a specific player /// Wrapper for acting as a specific player
/// ///
/// The DbApi is moved inside a new AsPlayer object.
///
/// # Usage /// # 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_id: i32 = 1; // Id that references player in DB
/// let player = api.as_player(player_id); /// let player = api.as_player(player_id);
/// ``` /// ```
@@ -97,7 +127,7 @@ impl<'q> DbApi<'q> {
} }
} }
/// A wrapper for interactions of players with the database /// A wrapper for interactions of players with the database.
/// Possible actions are exposed as methods /// Possible actions are exposed as methods
pub struct AsPlayer<'q> { pub struct AsPlayer<'q> {
id: i32, id: i32,
@@ -106,66 +136,162 @@ pub struct AsPlayer<'q> {
impl<'q> AsPlayer<'q> { impl<'q> AsPlayer<'q> {
/// Fetch the content of a player's chest /// 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>> { pub fn loot(self) -> QueryResult<Vec<models::Item>> {
Ok( Ok(models::Item::owned_by(self.id).load(self.conn)?)
models::Item::owned_by(self.id)
.load(self.conn)?
)
} }
/// Buy an item and add it to this player chest
///
/// TODO: Items should be picked from a custom list
///
/// # Panics
///
/// 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<S: Into<String>>(self, name: S, price: i32) -> ActionResult<Option<(i32, i32, i32, i32)>> {
match transactions::player::Buy.execute(
self.conn,
transactions::player::AddLootParams {
player_id: self.id,
loot_name: name.into(),
loot_price: price,
},
) {
Ok(res) => Ok(ActionStatus { executed: true, response: Some(res.loot_cost) }),
Err(e) => { dbg!(&e); Ok(ActionStatus { executed: false, response: None}) },
}
}
/// Sell an item from this player chest
///
/// # Panics
///
/// This currently panics if player wealth fails to be updated, as this is
/// a serious error. TODO: handle restoring of sold item in case of wealth update failure.
pub fn sell(
self,
loot_id: i32,
_price_mod: Option<f32>,
) -> ActionResult<Option<(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());
}
transactions::player::Sell.execute(
self.conn,
transactions::player::LootParams {
player_id: self.id,
loot_id,
},
)
.map(|res| ActionStatus { executed: true, response: Some(res.loot_cost) })
.or_else(|e| { dbg!(&e); Ok(ActionStatus::nop()) })
}
/// 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 { pub fn update_wealth(self, value_in_gp: f32) -> ActionResult<Option<(i32, i32, i32, i32)>> {
use schema::players::dsl::*; transactions::player::UpdateWealth.execute(
let current_wealth = players self.conn,
.find(self.id) transactions::player::WealthParams {
.select((cp, sp, gp, pp)) player_id: self.id,
.first::<models::WealthUpdate>(self.conn)?; value_in_gp,
// TODO: improve this },
// should be move inside a WealthUpdate method )
let update = models::WealthUpdate::from_gp( .map(|res| ActionStatus { executed: true, response: Some(res) })
current_wealth.to_gp() + value_in_gp .or_else(|e| { dbg!(&e); Ok(ActionStatus::nop())})
);
diesel::update(players)
.filter(id.eq(self.id))
.set(&update)
.execute(self.conn)
// TODO: need to work out what this boolean REALLY means
.map(|r| match r { 1 => true, _ => false })
} }
/// 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 request = models::NewClaim { player_id: self.id, loot_id: item }; let exists: bool =
diesel::insert_into(schema::claims::table) diesel::select(models::Loot::exists(item)).get_result(self.conn)?;
.values(&request) if !exists {
.execute(self.conn) return Ok(ActionStatus::nop());
.map(|r| match r { 1 => true, _ => false }) };
transactions::player::PutClaim.execute(
self.conn,
transactions::player::LootParams {
player_id: self.id,
loot_id: item,
},
)
.map(|_| ActionStatus { executed: true, response: () })
.or_else(|e| { dbg!(&e); Ok(ActionStatus::nop())})
} }
/// Withdraw claim /// Withdraw claim
pub fn unclaim(self, item: i32) -> ActionResult { pub fn unclaim(self, item: i32) -> ActionResult<()> {
use schema::claims::dsl::*; transactions::player::WithdrawClaim.execute(
diesel::delete( self.conn,
claims transactions::player::LootParams {
.filter(loot_id.eq(item)) player_id: self.id,
.filter(player_id.eq(self.id))) loot_id: item,
.execute(self.conn) },
.map(|r| match r { 1 => true, _ => false }) )
.map(|_| ActionStatus { executed: true, response: () })
.or_else(|e| { dbg!(&e); Ok(ActionStatus::nop())})
} }
} }
/// Wrapper for interactions of admins with the DB.
pub struct AsAdmin<'q>(&'q DbConnection); pub struct AsAdmin<'q>(&'q DbConnection);
impl<'q> AsAdmin<'q> { impl<'q> AsAdmin<'q> {
/// Adds a player to the database
pub fn add_player(self, name: String, start_wealth: f32) -> ActionResult { ///
/// Takes the player name and starting wealth (in gold value).
pub fn add_player(self, name: String, start_wealth: f32) -> ActionResult<()> {
diesel::insert_into(schema::players::table) diesel::insert_into(schema::players::table)
.values(&models::NewPlayer::create(&name, start_wealth)) .values(&models::player::NewPlayer::create(&name, start_wealth))
.execute(self.0) .execute(self.0)
.map(|r| match r { 1 => true, _ => false }) .map(ActionStatus::was_updated)
}
/// Adds a list of items to the group loot
pub fn add_loot<'a>(self, items: Vec<(&'a str, i32)>) -> ActionResult<()> {
for item_desc in items.into_iter() {
let new_item = models::item::NewLoot::to_group(item_desc);
diesel::insert_into(schema::looted::table)
.values(&new_item)
.execute(self.0)?;
}
Ok(ActionStatus::ok())
}
/// Resolve all pending claims and dispatch claimed items.
///
/// When a player gets an item, it's debt is increased by this item sell value
pub fn resolve_claims(self) -> ActionResult<()> {
// Fetch all claims, grouped by items.
let loot = models::Loot::owned_by(0).load(self.0)?;
let claims = schema::claims::table
.load::<models::Claim>(self.0)?
.grouped_by(&loot);
// For each claimed item
let data = loot.into_iter().zip(claims).collect::<Vec<_>>();
dbg!(data);
// If mutiples claims -> find highest resolve, give to this player
// If only one claim -> give to claiming
Ok(ActionStatus::nop())
} }
} }
/// Sets up a connection pool and returns it.
/// Uses the DATABASE_URL environment variable (must be set)
pub fn create_pool() -> Pool { pub fn create_pool() -> Pool {
let connspec = std::env::var("DATABASE_URL").expect("DATABASE_URL"); let connspec = std::env::var("DATABASE_URL").expect("DATABASE_URL");
dbg!(&connspec); dbg!(&connspec);
@@ -177,5 +303,187 @@ pub fn create_pool() -> Pool {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*;
type TestConnection = DbConnection;
/// Return a connection to a fresh database (stored in memory)
fn test_connection() -> TestConnection {
let test_conn = DbConnection::establish(":memory:").unwrap();
diesel_migrations::run_pending_migrations(&test_conn).unwrap();
test_conn
}
/// When migrations are run, a special player with id 0 and name "Groupe"
/// must be created.
#[test]
fn test_group_is_autocreated() {
let conn = test_connection();
let players = DbApi::with_conn(&conn).fetch_players().unwrap();
assert_eq!(players.len(), 1);
let group = players.get(0).unwrap();
assert_eq!(group.id, 0);
assert_eq!(group.name, "Groupe".to_string());
}
/// When a player updates wealth, a difference is returned by API.
/// Added to the previous amount of coins, it should equal the updated weath.
#[test]
fn test_player_updates_wealth() {
let conn = test_connection();
DbApi::with_conn(&conn)
.as_admin()
.add_player("PlayerName".to_string(), 403.21)
.unwrap();
let diff = DbApi::with_conn(&conn)
.as_player(1)
.update_wealth(-401.21)
.unwrap()
.response
.unwrap();
// Check the returned diff
assert_eq!(diff, (-1, -2, -1, -4));
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
assert_eq!(
(player.cp, player.sp, player.gp, player.pp),
(1 + diff.0, 2 + diff.1, 3 + diff.2, 4 + diff.3)
);
}
#[test]
fn test_admin_add_player() {
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);
let players = DbApi::with_conn(&conn).fetch_players().unwrap();
assert_eq!(players.len(), 2);
let new_player = players.get(1).unwrap();
assert_eq!(new_player.name, "PlayerName");
assert_eq!(
(new_player.cp, new_player.sp, new_player.gp, new_player.pp),
(1, 2, 3, 4)
);
}
#[test]
fn test_admin_resolve_claims() {
let conn = test_connection();
let claims = DbApi::with_conn(&conn).fetch_claims().unwrap();
assert_eq!(claims.len(), 0);
assert_eq!(true, false); // Failing as test is not complete
}
#[test]
fn test_player_claim_item() {
let conn = test_connection();
DbApi::with_conn(&conn)
.as_admin()
.add_player("Player".to_string(), 0.0)
.unwrap();
DbApi::with_conn(&conn)
.as_admin()
.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 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);
}
#[test]
fn test_player_unclaim_item() {
let conn = test_connection();
DbApi::with_conn(&conn)
.as_admin()
.add_player("Player".to_string(), 0.0)
.unwrap();
DbApi::with_conn(&conn)
.as_admin()
.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 claims = DbApi::with_conn(&conn).fetch_claims().unwrap();
assert_eq!(claims.len(), 0);
}
/// All-in-one checks one a simple buy/sell procedure
///
/// Checks that player's chest and wealth are updated.
/// Checks that items are sold at half their value.
#[test]
fn test_buy_sell_simple() {
let conn = test_connection();
DbApi::with_conn(&conn)
.as_admin()
.add_player("Player".to_string(), 1000.0)
.unwrap();
// 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 ?
let chest = DbApi::with_conn(&conn).as_player(1).loot().unwrap();
assert_eq!(chest.len(), 1);
let loot = chest.get(0).unwrap();
assert_eq!(loot.name, "Sword");
assert_eq!(loot.base_price, 800);
let players = DbApi::with_conn(&conn).fetch_players().unwrap();
let player = players.get(1).unwrap();
assert_eq!(player.pp, 2);
// 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)));
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();
let player = players.get(1).unwrap();
assert_eq!(player.pp, 6);
}
#[test]
fn test_admin_add_loot() {
let conn = test_connection();
assert_eq!(
0,
DbApi::with_conn(&conn).as_player(0).loot().unwrap().len()
);
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);
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
// created in the same order.
for (added, to_add) in looted.into_iter().zip(loot_to_add) {
assert_eq!(added.name, to_add.0);
assert_eq!(added.base_price, to_add.1);
}
}
} }

View File

@@ -1,17 +1,29 @@
use diesel::prelude::*; use crate::models::item::Loot;
use crate::schema::claims; use crate::schema::claims;
#[derive(Queryable, Serialize, Debug)] /// A Claim is a request by a single player on an item from group chest.
#[derive(Identifiable, Queryable, Associations, Serialize, Debug)]
#[belongs_to(Loot)]
pub struct Claim { pub struct Claim {
id: i32, /// DB Identifier
player_id: i32, pub id: i32,
loot_id: i32, /// ID that references the player making this claim
resolve: i32, pub player_id: i32,
/// ID that references the loot claimed
pub loot_id: i32,
/// WIP: How bad the player wants this item
pub resolve: i32,
} }
#[derive(Insertable, Debug)] #[derive(Insertable, Debug)]
#[table_name = "claims"] #[table_name = "claims"]
pub struct NewClaim { pub(crate) struct NewClaim {
pub player_id: i32, player_id: i32,
pub loot_id: i32, loot_id: i32,
}
impl NewClaim {
pub(crate) fn new(player_id: i32, loot_id: i32) -> Self {
Self { player_id, loot_id }
}
} }

View File

@@ -1,12 +1,10 @@
use crate::schema::looted;
use diesel::dsl::{exists, Eq, Filter, Find, Select};
use diesel::expression::exists::Exists;
use diesel::prelude::*; use diesel::prelude::*;
use diesel::dsl::{Eq, Filter, Select};
use crate::schema::{looted, items};
use crate::DbConnection;
type ItemColumns = type ItemColumns = (looted::id, looted::name, looted::base_price);
( looted::id, looted::name, looted::base_price, ); const ITEM_COLUMNS: ItemColumns = (looted::id, looted::name, looted::base_price);
const ITEM_COLUMNS: ItemColumns =
( looted::id, looted::name, looted::base_price, );
type OwnedBy = Select<OwnedLoot, ItemColumns>; type OwnedBy = Select<OwnedLoot, ItemColumns>;
/// Represents a unique item in inventory /// Represents a unique item in inventory
@@ -16,16 +14,15 @@ type OwnedBy = Select<OwnedLoot, ItemColumns>;
/// Or maybe this is a little too confusing ?? /// Or maybe this is a little too confusing ??
#[derive(Debug, Queryable, Serialize)] #[derive(Debug, Queryable, Serialize)]
pub struct Item { pub struct Item {
id: i32, pub id: i32,
name: String, pub name: String,
base_price: i32, pub base_price: i32,
} }
impl Item { impl Item {
/// Public proxy for Loot::owned_by that selects only Item fields /// Public proxy for Loot::owned_by that selects only Item fields
pub fn owned_by(player: i32) -> OwnedBy { pub fn owned_by(player: i32) -> OwnedBy {
Loot::owned_by(player) Loot::owned_by(player).select(ITEM_COLUMNS)
.select(ITEM_COLUMNS)
} }
} }
@@ -33,37 +30,48 @@ type WithOwner = Eq<looted::owner_id, i32>;
type OwnedLoot = Filter<looted::table, WithOwner>; type OwnedLoot = Filter<looted::table, WithOwner>;
/// Represents an item that has been looted /// Represents an item that has been looted
#[derive(Debug, Queryable, Serialize)] #[derive(Identifiable, Debug, Queryable, Serialize)]
struct Loot { #[table_name = "looted"]
pub(crate) struct Loot {
id: i32, id: i32,
name: String, name: String,
base_value: i32, base_price: i32,
owner: i32, owner: i32,
} }
impl Loot { impl Loot {
/// A filter on Loot that is owned by given player /// A filter on Loot that is owned by given player
fn owned_by(id: i32) -> OwnedLoot { pub(crate) fn owned_by(id: i32) -> OwnedLoot {
looted::table looted::table.filter(looted::owner_id.eq(id))
.filter(looted::owner_id.eq(id)) }
pub(crate) fn owns(player: i32, item: i32) -> Exists<Find<OwnedLoot, i32>> {
exists(Loot::owned_by(player).find(item))
}
pub(crate) fn exists(id: i32) -> Exists<Find<looted::table, i32>> {
exists(looted::table.find(id))
} }
} }
type ItemDesc<'a> = (&'a str, i32); /// Description of an item : (name, value in gold)
pub type ItemDesc<'a> = (&'a str, i32);
/// An item being looted or bought. /// An item being looted or bought.
/// ///
/// The owner is set to 0 in case of looting, /// The owner is set to 0 in case of looting,
/// to the id of buying player otherwise. /// to the id of buying player otherwise.
#[derive(Insertable)] #[derive(Insertable)]
#[table_name = "looted"] #[table_name = "looted"]
struct NewLoot<'a> { pub(crate) struct NewLoot<'a> {
name: &'a str, name: &'a str,
base_price: i32, base_price: i32,
owner_id: i32, owner_id: i32,
} }
impl<'a> NewLoot<'a> { impl<'a> NewLoot<'a> {
fn to_group(desc: ItemDesc<'a>) -> Self { /// A new loot going to the group (loot procedure)
pub(crate) fn to_group(desc: ItemDesc<'a>) -> Self {
Self { Self {
name: desc.0, name: desc.0,
base_price: desc.1, base_price: desc.1,
@@ -71,7 +79,8 @@ impl<'a> NewLoot<'a> {
} }
} }
fn to_player(player: i32, desc: ItemDesc<'a>) -> Self { /// A new loot going to a specific player (buy procedure)
pub(crate) fn to_player(player: i32, desc: ItemDesc<'a>) -> Self {
Self { Self {
name: desc.0, name: desc.0,
base_price: desc.1, base_price: desc.1,

View File

@@ -1,7 +1,8 @@
mod item; pub(super) mod claim;
mod player; pub(super) mod item;
mod claim; pub(super) mod player;
pub use claim::Claim;
pub use item::Item; pub use item::Item;
pub use player::{Player, NewPlayer, WealthUpdate}; pub(crate) use item::Loot;
pub use claim::{NewClaim, Claim}; pub use player::{Player, Wealth};

View File

@@ -1,20 +1,25 @@
use diesel::prelude::*;
use crate::schema::players; use crate::schema::players;
use crate::DbConnection;
/// Representation of a player in database /// Representation of a player in database
#[derive(Debug, Queryable, Serialize)] #[derive(Debug, Queryable, Serialize)]
pub struct Player { pub struct Player {
id: i32, /// DB Identitier
name: String, pub id: i32,
debt: i32, /// Full name of the character
cp: i32, pub name: String,
sp: i32, /// Amount of gold coins owed to the group.
gp: i32, /// Taking a looted items will increase the debt by it's sell value
pp: i32, pub debt: i32,
/// Count of copper pieces
pub cp: i32,
/// Count of silver pieces
pub sp: i32,
/// Count of gold pieces
pub gp: i32,
/// Count of platinum pieces
pub pp: i32,
} }
/// Unpack a floating value in gold pieces to integer /// Unpack a floating value in gold pieces to integer
/// values of copper, silver, gold and platinum pieces /// values of copper, silver, gold and platinum pieces
/// ///
@@ -33,38 +38,55 @@ fn unpack_gold_value(gold: f32) -> (i32, i32, i32, i32) {
(cp, sp, gp, pp) (cp, sp, gp, pp)
} }
/// Represent an update on a player's wealth /// State of a player's wealth
/// ///
/// The values held here are the amount of pieces to add or /// Values are held as individual pieces counts.
/// substract to player wealth. /// Allows conversion from and to a floating amount of gold pieces.
#[derive(Queryable, AsChangeset, Debug)] #[derive(Queryable, AsChangeset, Debug)]
#[table_name = "players"] #[table_name = "players"]
pub struct WealthUpdate { pub struct Wealth {
cp: i32, pub cp: i32,
sp: i32, pub sp: i32,
gp: i32, pub gp: i32,
pp: i32 pub pp: i32,
} }
impl WealthUpdate{ impl Wealth {
/// Unpack individual pieces counts from gold value /// Unpack individual pieces counts from gold value
///
/// # Examples
/// ```
/// # use lootalot_db::models::Wealth;
/// let wealth = Wealth::from_gp(403.21);
/// assert_eq!(wealth.as_tuple(), (1, 2, 3, 4));
/// ```
pub fn from_gp(gp: f32) -> Self { pub fn from_gp(gp: f32) -> Self {
let (cp, sp, gp, pp) = unpack_gold_value(gp); let (cp, sp, gp, pp) = unpack_gold_value(gp);
Self { cp, sp, gp, pp } Self { cp, sp, gp, pp }
} }
/// Convert total value to a floating value in gold pieces
///
/// # Examples
/// ```
/// # use lootalot_db::models::Wealth;
/// let wealth = Wealth{ pp: 4, gp: 3, sp: 2, cp: 1};
/// assert_eq!(wealth.to_gp(), 403.21);
/// ```
pub fn to_gp(&self) -> f32 { pub fn to_gp(&self) -> f32 {
let i = self.pp * 100 + self.gp; let i = self.pp * 100 + self.gp;
let f = (self.sp * 10 + self.cp) as f32 / 100.0; let f = (self.sp * 10 + self.cp) as f32 / 100.0;
i as f32 + f i as f32 + f
} }
/// Pack the counts inside a tuple, from lower to higher coin value.
pub fn as_tuple(&self) -> (i32, i32, i32, i32) {
(self.cp, self.sp, self.gp, self.pp)
}
} }
/// Representation of a new player record /// Representation of a new player record
#[derive(Insertable)] #[derive(Insertable)]
#[table_name = "players"] #[table_name = "players"]
pub struct NewPlayer<'a> { pub(crate) struct NewPlayer<'a> {
name: &'a str, name: &'a str,
cp: i32, cp: i32,
sp: i32, sp: i32,
@@ -73,37 +95,37 @@ pub struct NewPlayer<'a> {
} }
impl<'a> NewPlayer<'a> { impl<'a> NewPlayer<'a> {
pub fn create(name: &'a str, wealth: f32) -> Self { pub(crate) fn create(name: &'a str, wealth_in_gp: f32) -> Self {
let wealth = WealthUpdate::from_gp(wealth); let (cp, sp, gp, pp) = Wealth::from_gp(wealth_in_gp).as_tuple();
Self { Self {
name, name,
cp: wealth.cp, cp,
sp: wealth.sp, sp,
gp: wealth.gp, gp,
pp: wealth.pp, pp,
} }
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*;
use crate::tests;
#[test] #[test]
fn new_player_only_with_name() { fn test_unpack_gold_values() {
let n = NewPlayer::new("Féfi", None); use super::unpack_gold_value;
assert_eq!(n.name, "Féfi"); let test_values = [
assert_eq!(n.cp, 0); (1.0, (0, 0, 1, 0)),
assert_eq!(n.sp, 0); (1.23, (3, 2, 1, 0)),
assert_eq!(n.gp, 0); (1.03, (3, 0, 1, 0)),
assert_eq!(n.pp, 0); (100.23, (3, 2, 0, 1)),
(-100.23, (-3, -2, -0, -1)),
(10189.23, (3, 2, 89, 101)),
(-8090.20, (0, -2, -90, -80)),
];
for (tested, expected) in test_values.into_iter() {
assert_eq!(unpack_gold_value(*tested), *expected);
}
} }
#[test]
fn new_player_with_wealth() {
let initial_wealth = (1, 2, 3, 4);
let n = NewPlayer::new("Féfi", Some(initial_wealth));
assert_eq!(initial_wealth, (n.cp, n.sp, n.gp, n.pp));
}
} }

View File

@@ -40,9 +40,4 @@ joinable!(claims -> looted (loot_id));
joinable!(claims -> players (player_id)); joinable!(claims -> players (player_id));
joinable!(looted -> players (owner_id)); joinable!(looted -> players (owner_id));
allow_tables_to_appear_in_same_query!( allow_tables_to_appear_in_same_query!(claims, items, looted, players,);
claims,
items,
looted,
players,
);

View File

@@ -0,0 +1,295 @@
//! TODO:
//! Extract actions provided by API into their dedicated module.
//! Will allow more flexibilty to combinate them inside API methods.
//! Should make it easier to add a new feature : Reverting an action
//!
use crate::models;
use crate::schema;
use crate::DbConnection;
use diesel::prelude::*;
// TODO: revertable actions :
// - Buy
// - Sell
// - UpdateWealth
pub type TransactionResult<T> = Result<T, diesel::result::Error>;
pub trait DbTransaction {
type Params;
type Response: serde::Serialize;
fn execute<'q>(
self,
conn: &'q DbConnection,
params: Self::Params,
) -> TransactionResult<Self::Response>;
}
pub trait Revertable : DbTransaction {
fn revert<'q>(
self,
conn: &'q DbConnection,
player_id: i32,
params: <Self as DbTransaction>::Response,
) -> TransactionResult<<Self as DbTransaction>::Response>;
}
/// Return status of an 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<()> {
pub fn was_updated(updated_lines: usize) -> Self {
match updated_lines {
1 => Self::ok(),
_ => Self::nop(),
}
}
pub fn ok() -> ActionStatus<()> {
Self {
executed: true,
response: (),
}
}
}
impl<T: Default + serde::Serialize> ActionStatus<T> {
pub fn nop() -> ActionStatus<T> {
Self {
executed: false,
response: Default::default(),
}
}
}
// Or a module ?
pub(crate) mod player {
use super::*;
pub struct AddLootParams {
pub player_id: i32,
pub loot_name: String,
pub loot_price: i32,
}
pub struct Buy;
enum LootTransactionKind {
Buy,
Sell,
}
#[derive(Serialize, Debug)]
pub struct LootTransaction {
player_id: i32,
loot_id: i32,
kind: LootTransactionKind,
pub loot_cost: (i32, i32, i32, i32),
}
impl DbTransaction for Buy {
type Params = AddLootParams;
type Response = LootTransaction;
fn execute<'q>(
self,
conn: &'q DbConnection,
params: Self::Params,
) -> TransactionResult<Self::Response> {
let added_item = {
let new_item = models::item::NewLoot::to_player(
params.player_id,
(&params.loot_name, params.loot_price),
);
diesel::insert_into(schema::looted::table)
.values(&new_item)
.execute(conn)?
// TODO: return ID of inserted item
};
let updated_wealth = UpdateWealth.execute(
conn,
WealthParams {
player_id: params.player_id,
value_in_gp: -(params.loot_price as f32),
},
);
match (added_item, updated_wealth) {
(1, Ok(loot_cost)) => Ok(LootTransaction {
kind: LootTransactionKind::Buy,
player_id: params.player_id,
loot_id: 0, //TODO: find added item ID
loot_cost,
}),
// TODO: Handle other cases
_ => panic!()
}
}
}
impl Revertable for Buy {
fn revert<'q>(self, conn: &'q DbConnection, player_id: i32, params: <Self as DbTransaction>::Response)
-> TransactionResult<<Self as DbTransaction>::Response> {
unimplemented!()
}
}
pub struct LootParams {
pub player_id: i32,
pub loot_id: i32,
}
pub struct Sell;
impl DbTransaction for Sell {
type Params = LootParams;
type Response = LootTransaction;
fn execute<'q>(
self,
conn: &DbConnection,
params: Self::Params,
) -> TransactionResult<Self::Response> {
use schema::looted::dsl::*;
let loot_value = looted
.find(params.loot_id)
.select(base_price)
.first::<i32>(conn)?;
let sell_value = (loot_value / 2) as f32;
diesel::delete(looted.find(params.loot_id))
.execute(conn)
.and_then(|r| match r {
// On deletion, update this player wealth
1 => Ok(UpdateWealth
.execute(
conn,
WealthParams {
player_id: params.player_id,
value_in_gp: sell_value as f32,
},
)
.unwrap()),
_ => Ok(ActionStatus {
executed: false,
response: None,
}),
})
}
}
pub struct PutClaim;
impl DbTransaction for PutClaim {
type Params = LootParams;
type Response = ();
fn execute<'q>(
self,
conn: &DbConnection,
params: Self::Params,
) -> TransactionResult<Self::Response> {
let claim = models::claim::NewClaim::new(params.player_id, params.loot_id);
diesel::insert_into(schema::claims::table)
.values(&claim)
.execute(conn)
.and_then(|_| Ok(()))
}
}
pub struct WithdrawClaim;
impl DbTransaction for WithdrawClaim {
type Params = LootParams;
type Response = ();
fn execute<'q>(
self,
conn: &DbConnection,
params: Self::Params,
) -> TransactionResult<Self::Response> {
use schema::claims::dsl::*;
diesel::delete(
claims
.filter(loot_id.eq(params.loot_id))
.filter(player_id.eq(params.player_id)),
)
.execute(conn)
.and_then(|_| Ok(()))
}
}
pub struct WealthParams {
pub player_id: i32,
pub value_in_gp: f32,
}
pub struct UpdateWealth;
impl DbTransaction for UpdateWealth {
type Params = WealthParams;
type Response = (i32, i32, i32, i32);
fn execute<'q>(
self,
conn: &'q DbConnection,
params: WealthParams,
) -> TransactionResult<Self::Response> {
use schema::players::dsl::*;
let current_wealth = players
.find(params.player_id)
.select((cp, sp, gp, pp))
.first::<models::Wealth>(conn)?;
// TODO: improve thisdiesel dependant transaction
// should be move inside a WealthUpdate method
let updated_wealth =
models::Wealth::from_gp(current_wealth.to_gp() + params.value_in_gp);
// Difference in coins that is sent back
let (old, new) = (current_wealth.as_tuple(), updated_wealth.as_tuple());
let diff = (new.0 - old.0, new.1 - old.1, new.2 - old.2, new.3 - old.3);
diesel::update(players)
.filter(id.eq(params.player_id))
.set(&updated_wealth)
.execute(conn)
.and_then(|r| match r {
1 => Ok(diff),
_ => panic!("UpdateWealth made no changes !"),
})
}
}
impl Revertable for UpdateWealth {
fn revert<'q>(
self,
conn: &'q DbConnection,
player_id: i32,
params: <Self as DbTransaction>::Response,
) -> TransactionResult<<Self as DbTransaction>::Response> {
use schema::players::dsl::*;
let cur_wealth = players
.find(player_id)
.select((cp, sp, gp, pp))
.first::<models::Wealth>(conn)?;
let reverted_wealth = models::player::Wealth {
cp: cur_wealth.cp - params.0,
sp: cur_wealth.cp - params.1,
gp: cur_wealth.cp - params.2,
pp: cur_wealth.cp - params.3,
};
// Difference in coins that is sent back
let diff = ( -params.0, -params.1, -params.2, -params.3);
diesel::update(players)
.filter(id.eq(params.0))
.set(&reverted_wealth)
.execute(conn)
.and_then(|r| match r {
1 => Ok(diff),
_ => panic!("RevertableWealthUpdate made no changes"),
})
}
}
}
pub(crate) mod admin {
pub struct AddPlayer;
pub struct AddLoot;
pub struct SellLoot;
pub struct ResolveClaims;
}

View File

@@ -1,5 +1,11 @@
module.exports = { module.exports = {
presets: [ presets: [
'@vue/app' '@vue/app'
] ],
"presets": [["env", { "modules": false }]],
"env": {
"test": {
"presets": [["env", { "targets": { "node": "current" } }]]
}
}
} }

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,8 @@
"css-watch": "npm run css-build -- --watch", "css-watch": "npm run css-build -- --watch",
"serve": "vue-cli-service serve", "serve": "vue-cli-service serve",
"build": "vue-cli-service build", "build": "vue-cli-service build",
"lint": "vue-cli-service lint" "lint": "vue-cli-service lint",
"test": "jest"
}, },
"main": "sass/scroll.scss", "main": "sass/scroll.scss",
"dependencies": { "dependencies": {
@@ -19,10 +20,15 @@
"@vue/cli-plugin-babel": "^3.8.0", "@vue/cli-plugin-babel": "^3.8.0",
"@vue/cli-plugin-eslint": "^3.8.0", "@vue/cli-plugin-eslint": "^3.8.0",
"@vue/cli-service": "^3.8.0", "@vue/cli-service": "^3.8.0",
"@vue/test-utils": "^1.0.0-beta.29",
"babel-eslint": "^10.0.1", "babel-eslint": "^10.0.1",
"babel-jest": "^24.8.0",
"babel-preset-env": "^1.7.0",
"eslint": "^5.16.0", "eslint": "^5.16.0",
"eslint-plugin-vue": "^5.0.0", "eslint-plugin-vue": "^5.0.0",
"jest": "^24.8.0",
"node-sass": "^4.12.0", "node-sass": "^4.12.0",
"vue-jest": "^3.0.4",
"vue-template-compiler": "^2.6.10" "vue-template-compiler": "^2.6.10"
}, },
"eslintConfig": { "eslintConfig": {
@@ -47,5 +53,16 @@
"browserslist": [ "browserslist": [
"> 1%", "> 1%",
"last 2 versions" "last 2 versions"
] ],
"jest": {
"moduleFileExtensions": [
"js",
"json",
"vue"
],
"transform": {
".*\\.(vue)$": "vue-jest",
"^.+\\.js$": "<rootDir>/node_modules/babel-jest"
}
}
} }

View File

@@ -16,7 +16,6 @@ $danger: $red;
$table-cell-border: 1px solid $dark-red; $table-cell-border: 1px solid $dark-red;
$table-striped-row-even-background-color: $yellow-light; $table-striped-row-even-background-color: $yellow-light;
$button-padding-horizontal: 1em; $button-padding-horizontal: 1em;
@import "../node_modules/bulma/bulma.sass"; @import "../node_modules/bulma/bulma.sass";

View File

@@ -22,12 +22,14 @@ const Api = {
.catch(e => console.error("Fetch error", e)); .catch(e => console.error("Fetch error", e));
}, },
putClaim (playerId, itemId) { putClaim (playerId, itemId) {
console.log('newRequest from', playerId, 'on', itemId); return fetch(API_ENDPOINT(playerId + "/claim/" + itemId))
return Promise.resolve(true); .then(r => r.json())
.catch(e => console.error("Fetch error", e));
}, },
unClaim (playerId, itemId) { unClaim (playerId, itemId) {
console.log('cancelRequest of', playerId, 'on', itemId); return fetch(API_ENDPOINT(playerId + "/unclaim/" + itemId))
return Promise.resolve(true); .then(r => r.json())
.catch(e => console.error("Fetch error", e));
}, },
updateWealth (playerId, goldValue) { updateWealth (playerId, goldValue) {
return fetch(API_ENDPOINT(playerId + "/update-wealth/" + goldValue)) return fetch(API_ENDPOINT(playerId + "/update-wealth/" + goldValue))
@@ -57,8 +59,16 @@ export const AppStorage = {
.then(data => { .then(data => {
const [players, claims] = data; const [players, claims] = data;
this.__initPlayerList(players); this.__initPlayerList(players);
console.log("claims", claims); this.__initClaimsStore(claims);
}); });
// TODO: when __initPlayerList won't use promises
//.then(_ => this.state.initiated = true);
},
__initClaimsStore(data) {
for (var idx in data) {
var claimDesc = data[idx];
this.state.player_claims[claimDesc.player_id].push(claimDesc.loot_id);
}
}, },
__initPlayerList(data) { __initPlayerList(data) {
for (var idx in data) { for (var idx in data) {
@@ -105,14 +115,18 @@ export const AppStorage = {
}, },
updatePlayerWealth (goldValue) { updatePlayerWealth (goldValue) {
if (this.debug) console.log('updatePlayerWealth', goldValue, this.state.player_id)
return Api.updateWealth(this.state.player_id, goldValue) return Api.updateWealth(this.state.player_id, goldValue)
.then(done => { .then(done => {
if (done) { if (done.executed) {
// Update player wealth // Update player wealth
this.state.player_list[this.state.player_id].cp += 1; var diff = done.response;
if (this.debug) console.log('updatePlayerWealth', diff)
this.state.player_list[this.state.player_id].cp += diff[0];
this.state.player_list[this.state.player_id].sp += diff[1];
this.state.player_list[this.state.player_id].gp += diff[2];
this.state.player_list[this.state.player_id].pp += diff[3];
} }
return done; return done.executed;
}); });
}, },
// Put a claim on an item from group chest. // Put a claim on an item from group chest.
@@ -120,7 +134,7 @@ export const AppStorage = {
const playerId = this.state.player_id const playerId = this.state.player_id
Api.putClaim(playerId, itemId) Api.putClaim(playerId, itemId)
.then(done => { .then(done => {
if (done) { if (done.executed) {
// Update cliend-side state // Update cliend-side state
this.state.player_claims[playerId].push(itemId); this.state.player_claims[playerId].push(itemId);
} else { } else {
@@ -133,7 +147,7 @@ export const AppStorage = {
const playerId = this.state.player_id const playerId = this.state.player_id
Api.unClaim(playerId, itemId) Api.unClaim(playerId, itemId)
.then(done => { .then(done => {
if (done) { if (done.executed) {
var idx = this.state.player_claims[playerId].indexOf(itemId); var idx = this.state.player_claims[playerId].indexOf(itemId);
if (idx > -1) { if (idx > -1) {
this.state.player_claims[playerId].splice(idx, 1); this.state.player_claims[playerId].splice(idx, 1);

View File

@@ -17,13 +17,11 @@
checkError (ev) { checkError (ev) {
const newValue = ev.target.value; const newValue = ev.target.value;
this.has_error = isNaN(newValue); this.has_error = isNaN(newValue);
if (!this.has_error) {
this.$emit( this.$emit(
'input', 'input',
Number(newValue) this.has_error ? 0 : Number(newValue)
); );
} }
}
}, },
} }
</script> </script>

View File

@@ -1,6 +1,6 @@
extern crate actix_web;
extern crate dotenv; extern crate dotenv;
extern crate env_logger; extern crate env_logger;
extern crate actix_web;
extern crate lootalot_db; extern crate lootalot_db;
mod server; mod server;
@@ -13,5 +13,5 @@ fn main() {
// Server is started with 'serve' subcommand // Server is started with 'serve' subcommand
// $ lootalot serve // $ lootalot serve
// Start the server. // Start the server.
server::serve(); server::serve().ok();
} }

View File

@@ -1,9 +1,9 @@
use std::env;
use futures::Future;
use actix_files as fs;
use actix_web::{web, App, HttpServer, HttpResponse, Error};
use actix_cors::Cors; use actix_cors::Cors;
use lootalot_db::{Pool, DbApi, QueryResult}; use actix_files as fs;
use actix_web::{web, App, Error, HttpResponse, HttpServer};
use futures::Future;
use lootalot_db::{DbApi, Pool, QueryResult};
use std::env;
type AppPool = web::Data<Pool>; type AppPool = web::Data<Pool>;
@@ -61,36 +61,63 @@ pub(crate) fn serve() -> std::io::Result<()> {
Cors::new() Cors::new()
.allowed_origin("http://localhost:8080") .allowed_origin("http://localhost:8080")
.allowed_methods(vec!["GET", "POST"]) .allowed_methods(vec!["GET", "POST"])
.max_age(3600) .max_age(3600),
) )
.service(
web::scope("/api")
.route( .route(
"/api/players", "/players",
web::get().to_async(move |pool: AppPool| { web::get().to_async(move |pool: AppPool| {
db_call(pool, move |api| api.fetch_players()) db_call(pool, move |api| api.fetch_players())
}), }),
) )
.route( .route(
"/api/claims", "/claims",
web::get().to_async(move |pool: AppPool| db_call(pool, move |api| api.fetch_claims())), web::get().to_async(move |pool: AppPool| {
db_call(pool, move |api| api.fetch_claims())
}),
) )
.route( .route(
"/api/{player_id}/update-wealth/{amount}", "/{player_id}/update-wealth/{amount}",
web::get().to_async(move |pool: AppPool, data: web::Path<(i32, f32)>| { web::get().to_async(move |pool: AppPool, data: web::Path<(i32, f32)>| {
db_call(pool, move |api| api.as_player(data.0).update_wealth(data.1)) db_call(pool, move |api| api.as_player(data.0).update_wealth(data.1))
}), }),
) )
.route( .route(
"/api/{player_id}/loot", "/{player_id}/loot",
web::get().to_async(move |pool: AppPool, player_id: web::Path<i32>| { web::get().to_async(move |pool: AppPool, player_id: web::Path<i32>| {
db_call(pool, move |api| api.as_player(*player_id).loot()) db_call(pool, move |api| api.as_player(*player_id).loot())
}), }),
) )
.route( .route(
"/api/admin/add-player/{name}/{wealth}", "/{player_id}/claim/{item_id}",
web::get().to_async(move |pool: AppPool, data: web::Path<(String, f32)>| { web::get().to_async(move |pool: AppPool, data: web::Path<(i32, i32)>| {
db_call(pool, move |api| api.as_admin().add_player(data.0.clone(), data.1)) db_call(pool, move |api| api.as_player(data.0).claim(data.1))
}), }),
) )
.route(
"/{player_id}/unclaim/{item_id}",
web::get().to_async(move |pool: AppPool, data: web::Path<(i32, i32)>| {
db_call(pool, move |api| api.as_player(data.0).unclaim(data.1))
}),
)
.route(
"/admin/resolve-claims",
web::get().to_async(move |pool: AppPool| {
db_call(pool, move |api| api.as_admin().resolve_claims())
}),
)
.route(
"/admin/add-player/{name}/{wealth}",
web::get().to_async(
move |pool: AppPool, data: web::Path<(String, f32)>| {
db_call(pool, move |api| {
api.as_admin().add_player(data.0.clone(), data.1)
})
},
),
),
)
.service(fs::Files::new("/", www_root.clone()).index_file("index.html")) .service(fs::Files::new("/", www_root.clone()).index_file("index.html"))
}) })
.bind("127.0.0.1:8088")? .bind("127.0.0.1:8088")?