From 3a1f8ec4f6a4479c68be4505c90602fd781b3d98 Mon Sep 17 00:00:00 2001 From: Artus Date: Thu, 10 Oct 2019 16:38:59 +0200 Subject: [PATCH 01/23] thinking in progress --- lootalot_db/src/lib.rs | 1 + lootalot_db/src/models/player.rs | 8 ++++- lootalot_db/src/updates.rs | 59 ++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 lootalot_db/src/updates.rs diff --git a/lootalot_db/src/lib.rs b/lootalot_db/src/lib.rs index 3509296..6d8f2d0 100644 --- a/lootalot_db/src/lib.rs +++ b/lootalot_db/src/lib.rs @@ -12,6 +12,7 @@ use diesel::query_dsl::RunQueryDsl; use diesel::r2d2::{self, ConnectionManager}; pub mod models; +mod updates; mod schema; /// The connection used diff --git a/lootalot_db/src/models/player.rs b/lootalot_db/src/models/player.rs index a6ec804..b26f47c 100644 --- a/lootalot_db/src/models/player.rs +++ b/lootalot_db/src/models/player.rs @@ -20,7 +20,13 @@ pub struct Player { pub pp: i32, } -/// Unpack a floating value in gold pieces to integer +impl Player { + pub fn by_id(id: i32) -> Self { + + } +} + +/// Unpack a floating value of gold pieces to integer /// values of copper, silver, gold and platinum pieces /// /// # Note diff --git a/lootalot_db/src/updates.rs b/lootalot_db/src/updates.rs new file mode 100644 index 0000000..5be23f5 --- /dev/null +++ b/lootalot_db/src/updates.rs @@ -0,0 +1,59 @@ +//! +//! updates.rs +//! +//! Contains semantic mutations of database +//! +use crate::DbConnection; +use crate::models::player::Wealth; + +type PlayerId = i32; +type ItemId = i32; + +enum LootUpdate { + AddedItem(ItemId), + RemovedItem(Item), + GivenToPlayer(ItemId), +} + +impl LootUpdate { + fn add_loot(conn: &DbConnection, to_player: PlayerId, item_desc: Item) -> Result { + use schema::looted::dsl::*; + let new_item = models::item::NewLoot::to_player(to_player, &item_desc); + diesel::insert_into(looted) + .values(&new_item) + .execute(conn)?; + // Return newly created + let created_id = looted.select(id).order(id.desc()).first(conn)?; + Ok(LootUpdate::AddedItem(created_id)) + } + + fn remove_loot(conn: &DbConnection, loot_id: ItemId) -> Result { + use schema::looted::dsl::*; + } + + fn give_to_player(conn: &DbConnection, loot_id: ItemId) -> Result { + use schema::looted::dsl::*; + } +} + +impl LootUpdate { + fn undo(self, conn: &DbConnection) -> Result<(), diesel::result::Error> { + match self { + LootUpdate::AddedItem(item_id) => { + // Remove the item + }, + LootUpdate::RemovedItem(item) => { + // Add the item back + }, + LootUpdate::GivenToPlayer(item_id) => { + // Change owner to group + } + } + } +} + +enum PlayerUpdate { + Wealth(Wealth), + ClaimItem(ItemId), + UnclaimItem(ItemId), +} From 068b2e7169273f5d29544e768ff5bdb89060ab76 Mon Sep 17 00:00:00 2001 From: Artus Date: Fri, 11 Oct 2019 16:07:09 +0200 Subject: [PATCH 02/23] reorganizes api endpoints --- lootalot_db/src/lib.rs | 2 +- lootalot_db/src/models/player.rs | 6 - lootalot_front/src/AppStorage.js | 10 +- src/server.rs | 263 ++++++++++++++++--------------- 4 files changed, 145 insertions(+), 136 deletions(-) diff --git a/lootalot_db/src/lib.rs b/lootalot_db/src/lib.rs index 6d8f2d0..da41e8d 100644 --- a/lootalot_db/src/lib.rs +++ b/lootalot_db/src/lib.rs @@ -12,7 +12,7 @@ use diesel::query_dsl::RunQueryDsl; use diesel::r2d2::{self, ConnectionManager}; pub mod models; -mod updates; +//mod updates; mod schema; /// The connection used diff --git a/lootalot_db/src/models/player.rs b/lootalot_db/src/models/player.rs index b26f47c..83f42d8 100644 --- a/lootalot_db/src/models/player.rs +++ b/lootalot_db/src/models/player.rs @@ -20,12 +20,6 @@ pub struct Player { pub pp: i32, } -impl Player { - pub fn by_id(id: i32) -> Self { - - } -} - /// Unpack a floating value of gold pieces to integer /// values of copper, silver, gold and platinum pieces /// diff --git a/lootalot_front/src/AppStorage.js b/lootalot_front/src/AppStorage.js index 160140c..1c7c8d9 100644 --- a/lootalot_front/src/AppStorage.js +++ b/lootalot_front/src/AppStorage.js @@ -18,7 +18,7 @@ export const Api = { .then(r => r.json()) }, fetchPlayerList () { - return fetch(API_ENDPOINT("players/all")) + return fetch(API_ENDPOINT("players/")) .then(r => r.json()) }, fetchInventory () { @@ -30,7 +30,7 @@ export const Api = { .then(r => r.json()) }, fetchLoot (playerId) { - return fetch(API_ENDPOINT("players/loot/" + playerId)) + return fetch(API_ENDPOINT("players/" + playerId + "/loot")) .then(r => r.json()) }, putClaim (player_id, item_id) { @@ -43,15 +43,15 @@ export const Api = { }, updateWealth (player_id, value_in_gp) { const payload = { player_id, value_in_gp: Number(value_in_gp) }; - return this.__doFetch("players/update-wealth", 'PUT', payload); + return this.__doFetch("players/" + player_id + "/wealth", 'PUT', payload); }, buyItems (player_id, items) { const payload = { player_id, items }; - return this.__doFetch("players/buy", 'POST', payload); + return this.__doFetch("players/" + player_id + "/loot", 'PUT', payload); }, sellItems (player_id, items) { const payload = { player_id, items }; - return this.__doFetch("players/sell", 'POST', payload); + return this.__doFetch("players/" + player_id + "/loot", 'DELETE', payload); }, newLoot (items) { return this.__doFetch("admin/add-loot", 'POST', items); diff --git a/src/server.rs b/src/server.rs index 157206a..2dde379 100644 --- a/src/server.rs +++ b/src/server.rs @@ -2,10 +2,10 @@ use actix_cors::Cors; use actix_files as fs; use actix_web::{web, App, Error, HttpResponse, HttpServer}; use futures::Future; -use lootalot_db::{DbApi, Pool, QueryResult}; use lootalot_db::models::Item; +use lootalot_db::{DbApi, Pool, QueryResult}; +use serde::{Deserialize, Serialize}; use std::env; -use serde::{Serialize, Deserialize}; type AppPool = web::Data; @@ -30,49 +30,94 @@ type AppPool = web::Data; /// } /// ) /// ``` -pub fn db_call( - pool: AppPool, - query: Q, -) -> impl Future - where J: serde::ser::Serialize + Send + 'static, - Q: Fn(DbApi) -> QueryResult + Send + 'static, +pub fn db_call(pool: AppPool, query: Q) -> impl Future +where + J: serde::ser::Serialize + Send + 'static, + Q: Fn(DbApi) -> QueryResult + Send + 'static, { + dbg!("db_call"); let conn = pool.get().unwrap(); web::block(move || { let api = DbApi::with_conn(&conn); query(api) }) - .then(|res| match res { - Ok(players) => HttpResponse::Ok().json(players), - Err(e) => { - dbg!(&e); - HttpResponse::InternalServerError().finish() - } + .then(|res| match res { + Ok(r) => HttpResponse::Ok().json(r), + Err(e) => { + dbg!(&e); + HttpResponse::InternalServerError().finish() + } + }) +} + +mod endpoints { + + use super::*; + + #[derive(Serialize, Deserialize, Debug)] + pub struct PlayerClaim { + player_id: i32, + item_id: i32, + } + + #[derive(Serialize, Deserialize, Debug)] + pub struct WealthUpdate { + player_id: i32, + value_in_gp: f32, + } + + #[derive(Serialize, Deserialize, Debug)] + pub struct NewPlayer { + pub name: String, + pub wealth: f32, + } + + #[derive(Serialize, Deserialize, Debug)] + pub struct LootUpdate { + player_id: i32, + items: Vec<(i32, Option)>, + } + + pub fn players_list(pool: AppPool) -> impl Future{ + db_call(pool, move |api| api.fetch_players()) + } + + pub fn player_loot(pool: AppPool, player_id: web::Path) -> impl Future{ + db_call(pool, move |api| api.as_player(*player_id).loot()) + } + + pub fn update_wealth(pool: AppPool, data: web::Json) -> impl Future{ + db_call(pool, move |api| { + api.as_player(data.player_id) + .update_wealth(data.value_in_gp) }) -} + } -#[derive(Serialize, Deserialize, Debug)] -struct PlayerClaim { - player_id: i32, - item_id: i32, -} + pub fn buy_item(pool: AppPool, data: web::Json) -> impl Future{ + db_call(pool, move |api| { + api.as_player(data.player_id).buy(&data.items) + }) + } -#[derive(Serialize, Deserialize, Debug)] -struct WealthUpdate { - player_id: i32, - value_in_gp: f32, -} + pub fn sell_item(pool: AppPool, data: web::Json) -> impl Future{ + db_call(pool, move |api| { + api.as_player(data.player_id).sell(&data.items) + }) + } + pub fn player_claims(pool: AppPool) -> impl Future{ + db_call(pool, move |api| api.fetch_claims()) + } -#[derive(Serialize, Deserialize, Debug)] -struct NewPlayer { - name: String, - wealth: f32, -} - -#[derive(Serialize, Deserialize, Debug)] -struct LootUpdate { - player_id: i32, - items: Vec<(i32, Option)>, + pub fn put_claim(pool: AppPool, data: web::Json) -> impl Future{ + db_call(pool, move |api| { + api.as_player(data.player_id).claim(data.item_id) + }) + } + pub fn delete_claim(pool: AppPool, data: web::Json) -> impl Future{ + db_call(pool, move |api| { + api.as_player(data.player_id).unclaim(data.item_id) + }) + } } pub(crate) fn serve() -> std::io::Result<()> { @@ -91,104 +136,74 @@ pub(crate) fn serve() -> std::io::Result<()> { ) .service( web::scope("/api") - .route("/items", web::get().to_async(move |pool: AppPool| { - db_call(pool, move |api| api.fetch_inventory()) - })) .service( web::scope("/players") - .route( - "/all", - web::get().to_async(move |pool: AppPool| { - db_call(pool, move |api| api - .fetch_players()) - }), - ) - .route( - "/loot/{player_id}", - web::get().to_async(move |pool: AppPool, player_id: web::Path| { - db_call(pool, move |api| api.as_player(*player_id).loot()) - }), - ) - .route( - "/update-wealth", - web::put().to_async(move |pool: AppPool, data: web::Json| { - db_call(pool, move |api| api - .as_player(data.player_id) - .update_wealth(data.value_in_gp)) - }), - ) - .route( - "/buy", - web::post().to_async(move |pool: AppPool, data: web::Json| { - db_call(pool, move |api| api - .as_player(data.player_id) - .buy(&data.items), + .service( web::resource("/").route(web::get().to_async(endpoints::players_list))) // List of players + //.route(web::put().to_async(endpoints::new_player)) // Create/Update player + .service( + web::scope("/{player_id}") + //.route(web::get().to_async(...)) // Details of player + .service( + web::resource("/claims") + //.route(web::get().to_async(endpoints::player_claims)) + .route(web::put().to_async(endpoints::put_claim)) + .route(web::delete().to_async(endpoints::delete_claim)), ) - }), - ) - .route( - "/sell", - web::post().to_async(move |pool: AppPool, data: web::Json| { - db_call(pool, move |api| api - .as_player(data.player_id) - .sell(&data.items), + .service( + web::resource("/wealth") + //.route(web::get().to_async(...)) + .route(web::put().to_async(endpoints::update_wealth)), ) - }), - ) + .service( + web::resource("/loot") + .route(web::get().to_async(endpoints::player_loot)) + .route(web::put().to_async(endpoints::buy_item)) + .route(web::delete().to_async(endpoints::sell_item)), + ), + ), + ) + .route( + "/claims", + web::get().to_async(endpoints::player_claims) + ) + .route( + "/items", + web::get().to_async(move |pool: AppPool| { + db_call(pool, move |api| api.fetch_inventory()) + }), ) .service( - web::resource("/claims") - .route(web::get() - .to_async(move |pool: AppPool| { - db_call(pool, move |api| api - .fetch_claims()) - })) - .route(web::put() - .to_async(move |pool: AppPool, data: web::Json| { - db_call(pool, move |api| api - .as_player(data.player_id) - .claim(data.item_id)) - })) - .route(web::delete() - .to_async(move |pool: AppPool, data: web::Json| { - db_call(pool, move |api| api - .as_player(data.player_id) - .unclaim(data.item_id)) - })) - ) - .service(web::scope("/admin") - .route( - "/resolve-claims", - web::get().to_async(move |pool: AppPool| { - db_call(pool, move |api| api.as_admin().resolve_claims()) - }), - ) - .route( - "/add-loot", - web::post().to_async( - move |pool: AppPool, data: web::Json>| { - db_call(pool, move |api| api - .as_admin() - .add_loot(data.to_vec()), - ) - } + web::scope("/admin") + .route( + "/resolve-claims", + web::get().to_async(move |pool: AppPool| { + db_call(pool, move |api| api.as_admin().resolve_claims()) + }), ) - ) - .route( - "/add-player", - web::get().to_async( - move |pool: AppPool, data: web::Json| { - db_call(pool, move |api| api - .as_admin() - .add_player(&data.name, data.wealth), - ) - }, + .route( + "/add-loot", + web::post().to_async( + move |pool: AppPool, data: web::Json>| { + db_call(pool, move |api| { + api.as_admin().add_loot(data.to_vec()) + }) + }, + ), + ) + .route( + "/add-player", + web::get().to_async( + move |pool: AppPool, data: web::Json| { + db_call(pool, move |api| { + api.as_admin().add_player(&data.name, data.wealth) + }) + }, + ), ), - ) - ) + ), ) .service(fs::Files::new("/", www_root.clone()).index_file("index.html")) }) - .bind("127.0.0.1:8088")? - .run() + .bind("127.0.0.1:8088")? + .run() } From 0df875d6a6ff4a25a7ecb50e4d869255610427e5 Mon Sep 17 00:00:00 2001 From: Artus Date: Sun, 13 Oct 2019 16:02:47 +0200 Subject: [PATCH 03/23] moves db logic inside model's managers --- lootalot_db/src/lib.rs | 109 ++++++++---------------------- lootalot_db/src/models/claim.rs | 43 +++++++++++- lootalot_db/src/models/item.rs | 111 +++++++++++++++++++++++-------- lootalot_db/src/models/mod.rs | 7 +- lootalot_db/src/models/player.rs | 28 ++++++++ src/server.rs | 1 - 6 files changed, 183 insertions(+), 116 deletions(-) diff --git a/lootalot_db/src/lib.rs b/lootalot_db/src/lib.rs index da41e8d..4c0bba4 100644 --- a/lootalot_db/src/lib.rs +++ b/lootalot_db/src/lib.rs @@ -70,7 +70,7 @@ impl<'q> DbApi<'q> { /// /// TODO: remove limit used for debug pub fn fetch_inventory(self) -> QueryResult> { - Ok(schema::items::table.limit(100).load::(self.0)?) + models::item::Inventory(self.0).all() } /// Fetch all existing claims pub fn fetch_claims(self) -> QueryResult> { @@ -120,7 +120,7 @@ impl<'q> AsPlayer<'q> { /// assert_eq!(format!("{:?}", loot), "[]".to_string()); /// ``` pub fn loot(self) -> QueryResult> { - Ok(models::Item::owned_by(self.id).load(self.conn)?) + models::item::LootManager(self.conn, self.id).all() } /// Buy a batch of items and add them to this player chest /// @@ -137,23 +137,17 @@ impl<'q> AsPlayer<'q> { let mut added_items: Vec = Vec::with_capacity(params.len()); for (item_id, price_mod) in params.into_iter() { if let Ok((item, diff)) = self.conn.transaction(|| { - use schema::looted::dsl::*; - let item = schema::items::table.find(item_id).first::(self.conn)?; - let new_item = models::item::NewLoot::to_player(self.id, &item); - diesel::insert_into(schema::looted::table) - .values(&new_item) - .execute(self.conn)?; - let added_item = models::Item::owned_by(self.id) - .order(id.desc()) - .first(self.conn)?; + // 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 }; - DbApi::with_conn(self.conn) - .as_player(self.id) + models::player::AsPlayer(self.conn, self.id) .update_wealth(-sell_price) - .map(|diff| (added_item, diff)) + .map(|diff| (new_item, diff.as_tuple())) }) { cumulated_diff.push(diff); added_items.push(item); @@ -175,25 +169,15 @@ impl<'q> AsPlayer<'q> { let mut all_results: Vec<(i32, i32, i32, i32)> = Vec::with_capacity(params.len()); for (loot_id, price_mod) in params.into_iter() { let res = self.conn.transaction(|| { - use schema::looted::dsl::*; - let loot = looted - .find(loot_id) - .first::(self.conn)?; - if loot.owner != self.id { - // If the item does not belong to player, - // it can't be what we're looking for - return Err(diesel::result::Error::NotFound); - } - let mut sell_value = loot.base_price as f32 / 2.0; + let deleted = models::item::LootManager(self.conn, self.id).remove(*loot_id)?; + let mut sell_value = deleted.base_price as f32 / 2.0; if let Some(modifier) = price_mod { sell_value *= modifier; } - let _deleted = diesel::delete(looted.find(loot_id)) - .execute(self.conn)?; - DbApi::with_conn(self.conn).as_player(self.id).update_wealth(sell_value) + models::player::AsPlayer(self.conn, self.id).update_wealth(sell_value) }); if let Ok(diff) = res { - all_results.push(diff) + all_results.push(diff.as_tuple()) } else { // TODO: need to find a better way to deal with errors return Err(diesel::result::Error::NotFound) @@ -209,54 +193,27 @@ impl<'q> AsPlayer<'q> { /// /// Value can be negative to substract wealth. 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) - .select((cp, sp, gp, pp)) - .first::(self.conn)?; - // TODO: improve thisdiesel dependant transaction - // should be move inside a WealthUpdate method - let updated_wealth = models::Wealth::from_gp(current_wealth.to_gp() + 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(self.id)) - .set(&updated_wealth) - .execute(self.conn) - .map(|r| match r { - 1 => diff, - _ => panic!("RuntimeError: UpdateWealth did no changes at all!"), - }) + 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<()> { - let exists: bool = diesel::select(models::Loot::exists(item)).get_result(self.conn)?; - if !exists { - 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(|rows_updated| match rows_updated { - 1 => (), - _ => panic!("RuntimeError: Claim did no change at all!"), + models::claim::Claims(self.conn) + .add(self.id, item) + .map(|claim| { + dbg!("created"); + dbg!(claim); }) } /// Withdraw claim pub fn unclaim(self, item: i32) -> ActionResult<()> { - use schema::claims::dsl::*; - diesel::delete( - claims - .filter(loot_id.eq(item)) - .filter(player_id.eq(self.id)), - ) - .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"), + models::claim::Claims(self.conn) + .remove(self.id, item) + .map(|c| { + dbg!("deleted"); + dbg!(c); }) } } @@ -279,19 +236,9 @@ impl<'q> AsAdmin<'q> { } /// Adds a list of items to the group loot - /// - /// This offers complete control other created items, so that unique - /// items can be easily added. A user interface shall deal with - /// filling theses values for known items in inventory. - /// - /// # Params - /// List of (name, base_price) values for the new items pub fn add_loot(self, items: Vec) -> ActionResult<()> { for item_desc in items.iter() { - let new_item = models::item::NewLoot::to_group(item_desc); - diesel::insert_into(schema::looted::table) - .values(&new_item) - .execute(self.0)?; + models::item::LootManager(self.0, 0).add_from(item_desc)?; } Ok(()) } @@ -301,7 +248,7 @@ impl<'q> AsAdmin<'q> { /// 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 loot = models::item::Loot::owned_by(0).load(self.0)?; let claims = schema::claims::table .load::(self.0)? .grouped_by(&loot); diff --git a/lootalot_db/src/models/claim.rs b/lootalot_db/src/models/claim.rs index ad785e5..e0f21b9 100644 --- a/lootalot_db/src/models/claim.rs +++ b/lootalot_db/src/models/claim.rs @@ -1,4 +1,7 @@ -use crate::models::item::Loot; +use diesel::prelude::*; +use crate::{DbConnection, QueryResult}; + +use crate::models::{self, item::Loot}; use crate::schema::claims; /// A Claim is a request by a single player on an item from group chest. @@ -15,6 +18,44 @@ pub struct Claim { pub resolve: i32, } +pub struct Claims<'q>(pub &'q DbConnection); + +impl<'q> Claims<'q> { + + /// Finds a single claim by id. + pub fn find(&self, player_id: i32, loot_id: i32) -> QueryResult { + Ok( + claims::table + .filter(claims::dsl::player_id.eq(player_id)) + .filter(claims::dsl::loot_id.eq(loot_id)) + .first(self.0)? + ) + } + + /// Adds a claim in database and returns it + pub fn add(self, player_id: i32, loot_id: i32) -> QueryResult { + // We need to validate that the claimed item exists, and is actually owned by group (id 0) + let exists: bool = diesel::select(Loot::exists(loot_id)).get_result(self.0)?; + if !exists { + return Err(diesel::result::Error::NotFound); + }; + let claim = NewClaim::new(player_id, loot_id); + diesel::insert_into(claims::table) + .values(&claim) + .execute(self.0)?; + // Return the created claim + Ok(claims::table.order(claims::dsl::id.desc()).first::(self.0)?) + } + + /// Removes a claim from database, returning it + pub fn remove(self, req_player_id: i32, req_loot_id: i32) -> QueryResult { + let claim = self.find(req_player_id, req_loot_id)?; + diesel::delete(claims::table.find(claim.id)) + .execute(self.0)?; + Ok(claim) + } +} + #[derive(Insertable, Debug)] #[table_name = "claims"] pub(crate) struct NewClaim { diff --git a/lootalot_db/src/models/item.rs b/lootalot_db/src/models/item.rs index 5389d4e..0c9a891 100644 --- a/lootalot_db/src/models/item.rs +++ b/lootalot_db/src/models/item.rs @@ -1,8 +1,11 @@ -use crate::schema::looted; + + use diesel::dsl::{exists, Eq, Filter, Find, Select}; use diesel::expression::exists::Exists; use diesel::prelude::*; +use crate::{DbConnection, QueryResult}; +use crate::schema::{items, looted}; type ItemColumns = (looted::id, looted::name, looted::base_price); const ITEM_COLUMNS: ItemColumns = (looted::id, looted::name, looted::base_price); type OwnedBy = Select; @@ -21,67 +24,117 @@ pub struct Item { impl Item { /// Public proxy for Loot::owned_by that selects only Item fields - pub fn owned_by(player: i32) -> OwnedBy { + fn owned_by(player: i32) -> OwnedBy { Loot::owned_by(player).select(ITEM_COLUMNS) } } +pub struct Inventory<'q>(pub &'q DbConnection); + +impl<'q> Inventory<'q> { + + pub fn all(&self) -> QueryResult> { + items::table.load::(self.0) + } + + pub fn find(&self, item_id: i32) -> QueryResult { + items::table + .find(item_id) + .first::(self.0) + } +} + type WithOwner = Eq; type OwnedLoot = Filter; /// Represents an item that has been looted #[derive(Identifiable, Debug, Queryable, Serialize)] #[table_name = "looted"] -pub(crate) struct Loot { +pub(super) struct Loot { id: i32, name: String, - pub(crate) base_price: i32, - pub(crate) owner: i32, + base_price: i32, + owner: i32, } impl Loot { /// A filter on Loot that is owned by given player - pub(crate) fn owned_by(id: i32) -> OwnedLoot { + pub(super) fn owned_by(id: i32) -> OwnedLoot { looted::table.filter(looted::owner_id.eq(id)) } - pub(crate) fn owns(player: i32, item: i32) -> Exists> { + fn owns(player: i32, item: i32) -> Exists> { exists(Loot::owned_by(player).find(item)) } - pub(crate) fn exists(id: i32) -> Exists> { + pub(super) fn exists(id: i32) -> Exists> { exists(looted::table.find(id)) } } +/// Manager for a player's loot +pub struct LootManager<'q>(pub &'q DbConnection, pub i32); + +impl<'q> LootManager<'q> { + /// All items from this player chest + pub fn all(&self) -> QueryResult> { + Ok(Item::owned_by(self.1).load(self.0)?) + } + + /// Finds an item by id + pub fn find(&self, loot_id: i32) -> QueryResult { + Ok( + looted::table + .find(loot_id) + .first::(self.0) + .and_then(|loot| { + if loot.owner != self.1 { + Err(diesel::result::Error::NotFound) + } else { + Ok( Item { id: loot.id, name: loot.name, base_price: loot.base_price } ) + } + })? + ) + + } + + /// The last item added to the chest + pub fn last(&self) -> QueryResult { + Ok( + Item::owned_by(self.1) + .order(looted::dsl::id.desc()) + .first(self.0)? + ) + } + + /// Adds a copy of the given item inside player chest + pub fn add_from(self, item: &Item) -> QueryResult { + let new_item = NewLoot { + name: &item.name, + base_price: item.base_price, + owner_id: self.1, + }; + diesel::insert_into(looted::table) + .values(&new_item) + .execute(self.0)?; + self.last() + } + + pub fn remove(self, item_id: i32) -> QueryResult { + let deleted = self.find(item_id)?; + diesel::delete(looted::table.find(deleted.id)).execute(self.0)?; + Ok(deleted) + } +} + /// An item being looted or bought. /// /// The owner is set to 0 in case of looting, /// to the id of buying player otherwise. #[derive(Insertable)] #[table_name = "looted"] -pub(crate) struct NewLoot<'a> { +struct NewLoot<'a> { name: &'a str, base_price: i32, owner_id: i32, } - -impl<'a> NewLoot<'a> { - /// A new loot going to the group (loot procedure) - pub(crate) fn to_group(desc: &'a Item) -> Self { - Self { - name: &desc.name, - base_price: desc.base_price, - owner_id: 0, - } - } - - /// A new loot going to a specific player (buy procedure) - pub(crate) fn to_player(player: i32, desc: &'a Item) -> Self { - Self { - name: &desc.name, - base_price: desc.base_price, - owner_id: player, - } - } -} diff --git a/lootalot_db/src/models/mod.rs b/lootalot_db/src/models/mod.rs index 33147a4..286f11d 100644 --- a/lootalot_db/src/models/mod.rs +++ b/lootalot_db/src/models/mod.rs @@ -1,8 +1,7 @@ -pub(super) mod claim; -pub(super) mod item; -pub(super) mod player; +pub mod claim; +pub mod item; +pub mod player; pub use claim::Claim; pub use item::{Item}; -pub(crate) use item::Loot; pub use player::{Player, Wealth}; diff --git a/lootalot_db/src/models/player.rs b/lootalot_db/src/models/player.rs index 83f42d8..38efb9a 100644 --- a/lootalot_db/src/models/player.rs +++ b/lootalot_db/src/models/player.rs @@ -1,3 +1,5 @@ +use diesel::prelude::*; +use crate::{DbConnection, QueryResult}; use crate::schema::players; /// Representation of a player in database @@ -20,6 +22,32 @@ pub struct Player { pub pp: i32, } +pub struct AsPlayer<'q>(pub &'q DbConnection, pub i32); + +impl<'q> AsPlayer<'q> { + pub fn update_wealth(&self, value_in_gp: f32) -> QueryResult { + use crate::schema::players::dsl::*; + let current_wealth = players + .find(self.1) + .select((cp, sp, gp, pp)) + .first::(self.0)?; + let updated_wealth = Wealth::from_gp(current_wealth.to_gp() + value_in_gp); + // Difference in coins that is sent back + let difference = Wealth { + cp: updated_wealth.cp - current_wealth.cp, + sp: updated_wealth.sp - current_wealth.sp, + gp: updated_wealth.gp - current_wealth.gp, + pp: updated_wealth.pp - current_wealth.pp, + }; + diesel::update(players) + .filter(id.eq(self.1)) + .set(&updated_wealth) + .execute(self.0)?; + Ok(difference) + } +} + + /// Unpack a floating value of gold pieces to integer /// values of copper, silver, gold and platinum pieces /// diff --git a/src/server.rs b/src/server.rs index 2dde379..67ea8d3 100644 --- a/src/server.rs +++ b/src/server.rs @@ -35,7 +35,6 @@ where J: serde::ser::Serialize + Send + 'static, Q: Fn(DbApi) -> QueryResult + Send + 'static, { - dbg!("db_call"); let conn = pool.get().unwrap(); web::block(move || { let api = DbApi::with_conn(&conn); From b2e319dc15654c17b2aa57457076ff068cd4d5e0 Mon Sep 17 00:00:00 2001 From: Artus Date: Mon, 14 Oct 2019 15:27:49 +0200 Subject: [PATCH 04/23] works on resolve_claims --- lootalot_db/src/lib.rs | 24 +++++------------ lootalot_db/src/models/claim.rs | 44 ++++++++++++++++++++++---------- lootalot_db/src/models/item.rs | 30 ++++++++++++++-------- lootalot_db/src/models/player.rs | 9 +++++++ 4 files changed, 65 insertions(+), 42 deletions(-) diff --git a/lootalot_db/src/lib.rs b/lootalot_db/src/lib.rs index 4c0bba4..2e2604b 100644 --- a/lootalot_db/src/lib.rs +++ b/lootalot_db/src/lib.rs @@ -247,13 +247,7 @@ impl<'q> AsAdmin<'q> { /// /// 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::item::Loot::owned_by(0).load(self.0)?; - let claims = schema::claims::table - .load::(self.0)? - .grouped_by(&loot); - // For each claimed item - let data = loot.into_iter().zip(claims).collect::>(); + let data = models::claim::Claims(self.0).grouped_by_loot()?; dbg!(&data); for (loot, claims) in data { @@ -262,18 +256,12 @@ impl<'q> AsAdmin<'q> { let claim = claims.get(0).unwrap(); let player_id = claim.player_id; self.0.transaction(|| { - use schema::looted::dsl::*; - diesel::update(looted.find(claim.loot_id)) - .set(owner_id.eq(player_id)) + loot.set_owner(claim.player_id) .execute(self.0)?; - diesel::delete(schema::claims::table.find(claim.id)) - .execute(self.0)?; - { - use schema::players::dsl::*; - diesel::update(players.find(player_id)) - .set(debt.eq(debt + (loot.base_price / 2))) - .execute(self.0) - } + models::player::AsPlayer(self.0, player_id) + .update_debt(loot.sell_value())?; + models::claim::Claims(self.0).remove(player_id, claim.loot_id)?; + Ok(()) })?; }, _ => (), diff --git a/lootalot_db/src/models/claim.rs b/lootalot_db/src/models/claim.rs index e0f21b9..e91d469 100644 --- a/lootalot_db/src/models/claim.rs +++ b/lootalot_db/src/models/claim.rs @@ -22,49 +22,65 @@ pub struct Claims<'q>(pub &'q DbConnection); impl<'q> Claims<'q> { - /// Finds a single claim by id. + /// Finds a single claim by association of player and loot ids. pub fn find(&self, player_id: i32, loot_id: i32) -> QueryResult { - Ok( claims::table .filter(claims::dsl::player_id.eq(player_id)) .filter(claims::dsl::loot_id.eq(loot_id)) - .first(self.0)? - ) + .first(self.0) } /// Adds a claim in database and returns it pub fn add(self, player_id: i32, loot_id: i32) -> QueryResult { - // We need to validate that the claimed item exists, and is actually owned by group (id 0) - let exists: bool = diesel::select(Loot::exists(loot_id)).get_result(self.0)?; - if !exists { - return Err(diesel::result::Error::NotFound); - }; + // We need to validate that the claimed item exists + // AND is actually owned by group (id 0) + let _item = models::item::LootManager(self.0, 0).find(loot_id)?; let claim = NewClaim::new(player_id, loot_id); diesel::insert_into(claims::table) .values(&claim) .execute(self.0)?; // Return the created claim - Ok(claims::table.order(claims::dsl::id.desc()).first::(self.0)?) + claims::table + .order(claims::dsl::id.desc()) + .first::(self.0) } /// Removes a claim from database, returning it - pub fn remove(self, req_player_id: i32, req_loot_id: i32) -> QueryResult { - let claim = self.find(req_player_id, req_loot_id)?; + pub fn remove(self, player_id: i32, loot_id: i32) -> QueryResult { + let claim = self.find(player_id, loot_id)?; diesel::delete(claims::table.find(claim.id)) .execute(self.0)?; Ok(claim) } + + pub fn filtered_by_loot(&self, loot_id: i32) -> QueryResult> { + claims::table + .filter(claims::dsl::loot_id.eq(loot_id)) + .load(self.0) + } + + pub(crate) fn grouped_by_loot(&self) -> QueryResult)>> { + let loot = models::item::Loot::owned_by(0).load(self.0)?; + let claims = claims::table + .load(self.0)? + .grouped_by(&loot); + Ok( + loot.into_iter() + .zip(claims) + .collect::>() + ) + } } #[derive(Insertable, Debug)] #[table_name = "claims"] -pub(crate) struct NewClaim { +struct NewClaim { player_id: i32, loot_id: i32, } impl NewClaim { - pub(crate) fn new(player_id: i32, loot_id: i32) -> Self { + fn new(player_id: i32, loot_id: i32) -> Self { Self { player_id, loot_id } } } diff --git a/lootalot_db/src/models/item.rs b/lootalot_db/src/models/item.rs index 0c9a891..49ca2e9 100644 --- a/lootalot_db/src/models/item.rs +++ b/lootalot_db/src/models/item.rs @@ -10,11 +10,7 @@ type ItemColumns = (looted::id, looted::name, looted::base_price); const ITEM_COLUMNS: ItemColumns = (looted::id, looted::name, looted::base_price); type OwnedBy = Select; -/// Represents a unique item in inventory -/// -/// It is also used as a public representation of Loot, since owner -/// information is implicit. -/// Or maybe this is a little too confusing ?? +/// Represents a basic item #[derive(Debug, Queryable, Serialize, Deserialize, Clone)] pub struct Item { pub id: i32, @@ -47,7 +43,10 @@ impl<'q> Inventory<'q> { type WithOwner = Eq; type OwnedLoot = Filter; -/// Represents an item that has been looted + + +/// Represents an item that has been looted, +/// hence has an owner. #[derive(Identifiable, Debug, Queryable, Serialize)] #[table_name = "looted"] pub(super) struct Loot { @@ -63,13 +62,24 @@ impl Loot { looted::table.filter(looted::owner_id.eq(id)) } - fn owns(player: i32, item: i32) -> Exists> { - exists(Loot::owned_by(player).find(item)) - } - pub(super) fn exists(id: i32) -> Exists> { exists(looted::table.find(id)) } + + pub(crate) fn set_owner(&self, owner: i32) -> () { + diesel::update(looted::table.find(self.id)) + .set(looted::dsl::owner_id.eq(owner)) + } + + /// TODO: should belong inside Item impl + pub(crate) fn value(&self) -> i32 { + self.base_price + } + + /// TODO: should belong inside Item impl + pub(crate) fn sell_value(&self) -> i32 { + self.base_price / 2 + } } /// Manager for a player's loot diff --git a/lootalot_db/src/models/player.rs b/lootalot_db/src/models/player.rs index 38efb9a..f61e3af 100644 --- a/lootalot_db/src/models/player.rs +++ b/lootalot_db/src/models/player.rs @@ -45,6 +45,15 @@ impl<'q> AsPlayer<'q> { .execute(self.0)?; Ok(difference) } + + pub fn update_debt(&self, value_in_gp: i32) -> QueryResult<()> { + diesel::update(players::table.find(self.1)) + .set(players::dsl::debt.eq( + players::dsl::debt + value_in_gp + )) + .execute(self.0)?; + Ok(()) + } } From c72169281d39374199f2fcc7b438fae7a016615a Mon Sep 17 00:00:00 2001 From: Artus Date: Mon, 14 Oct 2019 16:28:18 +0200 Subject: [PATCH 05/23] refactors, makes Loot private to module --- lootalot_db/src/lib.rs | 12 ++++------ lootalot_db/src/models/claim.rs | 27 +++++++++++++++++----- lootalot_db/src/models/item.rs | 41 ++++++++++++++++++++++----------- 3 files changed, 54 insertions(+), 26 deletions(-) diff --git a/lootalot_db/src/lib.rs b/lootalot_db/src/lib.rs index 2e2604b..b8f2af0 100644 --- a/lootalot_db/src/lib.rs +++ b/lootalot_db/src/lib.rs @@ -247,21 +247,19 @@ impl<'q> AsAdmin<'q> { /// /// When a player gets an item, it's debt is increased by this item sell value pub fn resolve_claims(self) -> ActionResult<()> { - let data = models::claim::Claims(self.0).grouped_by_loot()?; + let data = models::claim::Claims(self.0).grouped_by_item()?; dbg!(&data); - for (loot, claims) in data { + for (item, claims) in data { match claims.len() { 1 => { let claim = claims.get(0).unwrap(); let player_id = claim.player_id; self.0.transaction(|| { - loot.set_owner(claim.player_id) - .execute(self.0)?; + claim.resolve_claim(self.0)?; + //models::item::LootManager(self.0, 0).set_owner(claim.loot_id, claim.player_id)?; models::player::AsPlayer(self.0, player_id) - .update_debt(loot.sell_value())?; - models::claim::Claims(self.0).remove(player_id, claim.loot_id)?; - Ok(()) + .update_debt(item.sell_value()) })?; }, _ => (), diff --git a/lootalot_db/src/models/claim.rs b/lootalot_db/src/models/claim.rs index e91d469..cb10adf 100644 --- a/lootalot_db/src/models/claim.rs +++ b/lootalot_db/src/models/claim.rs @@ -18,6 +18,21 @@ pub struct Claim { pub resolve: i32, } +impl Claim { + pub fn resolve_claim(&self, conn: &DbConnection) -> QueryResult<()> { + let loot: Loot = Loot::find(self.loot_id).first(conn)?; + loot.set_owner(self.player_id, conn)?; + self.remove(conn)?; + Ok(()) + } + + fn remove(&self, conn: &DbConnection) -> QueryResult<()> { + diesel::delete(claims::table.find(self.id)) + .execute(conn)?; + Ok(()) + } +} + pub struct Claims<'q>(pub &'q DbConnection); impl<'q> Claims<'q> { @@ -48,8 +63,7 @@ impl<'q> Claims<'q> { /// Removes a claim from database, returning it pub fn remove(self, player_id: i32, loot_id: i32) -> QueryResult { let claim = self.find(player_id, loot_id)?; - diesel::delete(claims::table.find(claim.id)) - .execute(self.0)?; + claim.remove(self.0)?; Ok(claim) } @@ -59,13 +73,14 @@ impl<'q> Claims<'q> { .load(self.0) } - pub(crate) fn grouped_by_loot(&self) -> QueryResult)>> { - let loot = models::item::Loot::owned_by(0).load(self.0)?; + pub(crate) fn grouped_by_item(&self) -> QueryResult)>> { + let group_loot: Vec = Loot::owned_by(0).load(self.0)?; let claims = claims::table .load(self.0)? - .grouped_by(&loot); + .grouped_by(&group_loot); Ok( - loot.into_iter() + group_loot.into_iter() + .map(|loot| loot.into_item()) .zip(claims) .collect::>() ) diff --git a/lootalot_db/src/models/item.rs b/lootalot_db/src/models/item.rs index 49ca2e9..ad9c45b 100644 --- a/lootalot_db/src/models/item.rs +++ b/lootalot_db/src/models/item.rs @@ -19,7 +19,14 @@ pub struct Item { } impl Item { - /// Public proxy for Loot::owned_by that selects only Item fields + pub fn value(&self) -> i32 { + self.base_price + } + + pub fn sell_value(&self) -> i32 { + self.base_price / 2 + } + fn owned_by(player: i32) -> OwnedBy { Loot::owned_by(player).select(ITEM_COLUMNS) } @@ -62,24 +69,30 @@ impl Loot { looted::table.filter(looted::owner_id.eq(id)) } - pub(super) fn exists(id: i32) -> Exists> { + fn exists(id: i32) -> Exists> { exists(looted::table.find(id)) } - pub(crate) fn set_owner(&self, owner: i32) -> () { + pub(super) fn set_owner(&self, owner: i32, conn: &DbConnection) -> QueryResult<()> { diesel::update(looted::table.find(self.id)) .set(looted::dsl::owner_id.eq(owner)) + .execute(conn)?; + Ok(()) } - /// TODO: should belong inside Item impl - pub(crate) fn value(&self) -> i32 { - self.base_price + pub(super) fn into_item(self) -> Item { + Item { + id: self.id, + name: self.name, + base_price: self.base_price, + } } - /// TODO: should belong inside Item impl - pub(crate) fn sell_value(&self) -> i32 { - self.base_price / 2 + pub(super) fn find(id: i32) -> Find { + looted::table + .find(id) } + } /// Manager for a player's loot @@ -94,10 +107,10 @@ impl<'q> LootManager<'q> { /// Finds an item by id pub fn find(&self, loot_id: i32) -> QueryResult { Ok( - looted::table - .find(loot_id) - .first::(self.0) - .and_then(|loot| { + + Loot::find(loot_id) + .first(self.0) + .and_then(|loot: Loot| { if loot.owner != self.1 { Err(diesel::result::Error::NotFound) } else { @@ -135,6 +148,8 @@ impl<'q> LootManager<'q> { diesel::delete(looted::table.find(deleted.id)).execute(self.0)?; Ok(deleted) } + + } /// An item being looted or bought. From a05cab9974ad3cfdea1570c0e5251e9607ab75e9 Mon Sep 17 00:00:00 2001 From: Artus Date: Mon, 14 Oct 2019 23:38:29 +0200 Subject: [PATCH 06/23] fixes claims routes --- lootalot_front/src/AppStorage.js | 4 ++-- src/server.rs | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lootalot_front/src/AppStorage.js b/lootalot_front/src/AppStorage.js index 1c7c8d9..2e80e5e 100644 --- a/lootalot_front/src/AppStorage.js +++ b/lootalot_front/src/AppStorage.js @@ -35,11 +35,11 @@ export const Api = { }, putClaim (player_id, item_id) { const payload = { player_id, item_id }; - return this.__doFetch("claims", 'PUT', payload); + return this.__doFetch("players/" + player_id + "/claims", 'PUT', payload); }, unClaim (player_id, item_id) { const payload = { player_id, item_id }; - return this.__doFetch("claims", 'DELETE', payload); + return this.__doFetch("players/" + player_id + "/claims", 'DELETE', payload); }, updateWealth (player_id, value_in_gp) { const payload = { player_id, value_in_gp: Number(value_in_gp) }; diff --git a/src/server.rs b/src/server.rs index 67ea8d3..ad3a103 100644 --- a/src/server.rs +++ b/src/server.rs @@ -107,14 +107,14 @@ mod endpoints { db_call(pool, move |api| api.fetch_claims()) } - pub fn put_claim(pool: AppPool, data: web::Json) -> impl Future{ + pub fn put_claim(pool: AppPool, (player, loot): (web::Path, web::Json)) -> impl Future{ db_call(pool, move |api| { - api.as_player(data.player_id).claim(data.item_id) + api.as_player(*player).claim(loot.item_id) }) } - pub fn delete_claim(pool: AppPool, data: web::Json) -> impl Future{ + pub fn delete_claim(pool: AppPool, (player, data): (web::Path, web::Json)) -> impl Future{ db_call(pool, move |api| { - api.as_player(data.player_id).unclaim(data.item_id) + api.as_player(*player).unclaim(data.item_id) }) } } From de4aad567911864ef9dc221743a50b2da72f4b21 Mon Sep 17 00:00:00 2001 From: Artus Date: Mon, 14 Oct 2019 23:40:38 +0200 Subject: [PATCH 07/23] updates README --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 40d20c4..f306e0f 100644 --- a/README.md +++ b/README.md @@ -7,14 +7,14 @@ Un gestionnaire de trésors pour des joueurs de Donjon&Dragons(tm). ## Fonctionnalités prévues * Ajouter des objets - ☐ Acheter + ☑ Acheter ☐ Ajouter un trésor (objet par objet ou par liste) * Répartir les objets entre les joueurs et le groupe - ☐ Demander un objet + ☑ Demander un objet ☐ Résoudre un conflit ☐ Finaliser la répartition après un délai défini * Vendre les objets du groupe et répartir équitablement leur valeur entre les joueurs - ☐ Possibilité d'indiquer une variation du prix de vente globale et/ou pour chaque objet + ☑ Possibilité d'indiquer une variation du prix de vente globale et/ou pour chaque objet ☐ Possibilité d'indiquer des joueurs exclus de la répartition * Gérer les comptes du groupe et des joueurs ☑ Afficher le solde actuel et la dette envers le groupe From 8399ffebf7b710970764f17a078af816f97bf0a8 Mon Sep 17 00:00:00 2001 From: Artus Date: Tue, 15 Oct 2019 14:16:45 +0200 Subject: [PATCH 08/23] refactors claims unit tests --- lootalot_db/src/lib.rs | 20 +++++----- lootalot_db/src/models/claim.rs | 68 ++++++++++++++++++++++++++++++++ lootalot_db/src/models/player.rs | 19 +++++++++ 3 files changed, 98 insertions(+), 9 deletions(-) diff --git a/lootalot_db/src/lib.rs b/lootalot_db/src/lib.rs index b8f2af0..3cfa983 100644 --- a/lootalot_db/src/lib.rs +++ b/lootalot_db/src/lib.rs @@ -15,6 +15,12 @@ pub mod models; //mod updates; mod schema; +pub use models::{ + item::{Item, LootManager}, + claim::{Claim, Claims}, + player::{Player, Players}, +}; + /// The connection used pub type DbConnection = SqliteConnection; /// A pool of connections @@ -226,13 +232,9 @@ impl<'q> AsAdmin<'q> { /// /// Takes the player name and starting wealth (in gold value). pub fn add_player(self, name: &str, start_wealth: f32) -> ActionResult<()> { - diesel::insert_into(schema::players::table) - .values(&models::player::NewPlayer::create(name, start_wealth)) - .execute(self.0) - .map(|rows_updated| match rows_updated { - 1 => (), - _ => panic!("RuntimeError: AddPlayer did not make expected changes"), - }) + models::player::Players(self.0) + .add(name, start_wealth)?; + Ok(()) } /// Adds a list of items to the group loot @@ -280,8 +282,8 @@ pub fn create_pool() -> Pool { .expect("Failed to create pool.") } -#[cfg(test)] -mod tests { +#[cfg(none)] +mod tests_old { use super::*; type TestConnection = DbConnection; diff --git a/lootalot_db/src/models/claim.rs b/lootalot_db/src/models/claim.rs index cb10adf..3a1c826 100644 --- a/lootalot_db/src/models/claim.rs +++ b/lootalot_db/src/models/claim.rs @@ -37,6 +37,11 @@ pub struct Claims<'q>(pub &'q DbConnection); impl<'q> Claims<'q> { + pub fn all(&self) -> QueryResult> { + claims::table + .load(self.0) + } + /// Finds a single claim by association of player and loot ids. pub fn find(&self, player_id: i32, loot_id: i32) -> QueryResult { claims::table @@ -50,6 +55,11 @@ impl<'q> Claims<'q> { // We need to validate that the claimed item exists // AND is actually owned by group (id 0) let _item = models::item::LootManager(self.0, 0).find(loot_id)?; + // We also check if claims does not already exists + if let Ok(_) = self.find(player_id, loot_id) { + return Err(diesel::result::Error::RollbackTransaction); + } + let claim = NewClaim::new(player_id, loot_id); diesel::insert_into(claims::table) .values(&claim) @@ -99,3 +109,61 @@ impl NewClaim { Self { player_id, loot_id } } } + +#[cfg(test)] +mod tests { + use super::*; + + type TestResult = Result<(), diesel::result::Error>; + + fn test_connection() -> Result { + let conn = DbConnection::establish(":memory:") + .map_err(|_| diesel::result::Error::NotFound)?; + diesel_migrations::run_pending_migrations(&conn) + .map_err(|_| diesel::result::Error::NotFound)?; + let manager = models::player::Players(&conn); + manager.add("Player1", 0.0)?; + manager.add("Player2", 0.0)?; + crate::LootManager(&conn, 0) + .add_from(&crate::Item{ id: 0, name: "Epee".to_string(), base_price: 30 })?; + crate::LootManager(&conn, 1) + .add_from(&crate::Item{ id: 0, name: "Arc".to_string(), base_price: 20 })?; + Ok(conn) + } + + #[test] + fn add_claim() -> TestResult { + let conn = test_connection()?; + Claims(&conn).add(1, 1)?; + assert_eq!(Claims(&conn).all()?.len(), 1); + Ok(()) + } + + #[test] + fn cannot_duplicate_by_adding() -> TestResult { + let conn = test_connection()?; + Claims(&conn).add(1, 1)?; + let res = Claims(&conn).add(1, 1); + assert_eq!(res.is_err(), true); + assert_eq!(Claims(&conn).all()?.len(), 1); + Ok(()) + } + + #[test] + fn remove_claim() -> TestResult { + let conn = test_connection()?; + let claim = Claims(&conn).add(1, 1)?; + claim.remove(&conn); + assert_eq!(Claims(&conn).all()?.len(), 0); + Ok(()) + } + + #[test] + fn cannot_only_claim_from_group() -> TestResult { + let conn = test_connection()?; + let claim = Claims(&conn).add(1, 2); + assert_eq!(claim.is_err(), true); + assert_eq!(Claims(&conn).all()?.len(), 0); + Ok(()) + } +} diff --git a/lootalot_db/src/models/player.rs b/lootalot_db/src/models/player.rs index f61e3af..c9fb23f 100644 --- a/lootalot_db/src/models/player.rs +++ b/lootalot_db/src/models/player.rs @@ -22,6 +22,25 @@ pub struct Player { pub pp: i32, } +pub struct Players<'q>(pub &'q DbConnection); + +impl<'q> Players<'q> { + + pub fn all(&self) -> QueryResult> { + players::table + .load(self.0) + } + + pub fn add(&self, name: &str, wealth: f32) -> QueryResult { + diesel::insert_into(players::table) + .values(&NewPlayer::create(name, wealth)) + .execute(self.0)?; + players::table + .order(players::dsl::id.desc()) + .first(self.0) + } +} + pub struct AsPlayer<'q>(pub &'q DbConnection, pub i32); impl<'q> AsPlayer<'q> { From 6101aaa9e91b7e7226aa938f829d68e7e872fdac Mon Sep 17 00:00:00 2001 From: Artus Date: Tue, 15 Oct 2019 14:42:57 +0200 Subject: [PATCH 09/23] thoughts on new api structure, formats code --- lootalot_db/src/lib.rs | 118 ++++++++++++++++++++++--------- lootalot_db/src/models/claim.rs | 42 +++++------ lootalot_db/src/models/item.rs | 57 ++++++--------- lootalot_db/src/models/mod.rs | 2 +- lootalot_db/src/models/player.rs | 17 ++--- lootalot_db/src/schema.rs | 7 +- lootalot_db/src/updates.rs | 12 ++-- src/server.rs | 60 +++++++++------- 8 files changed, 176 insertions(+), 139 deletions(-) diff --git a/lootalot_db/src/lib.rs b/lootalot_db/src/lib.rs index 3cfa983..2284e8b 100644 --- a/lootalot_db/src/lib.rs +++ b/lootalot_db/src/lib.rs @@ -4,8 +4,10 @@ //! 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; @@ -16,8 +18,8 @@ pub mod models; mod schema; pub use models::{ - item::{Item, LootManager}, claim::{Claim, Claims}, + item::{Item, LootManager}, player::{Player, Players}, }; @@ -30,6 +32,33 @@ pub type QueryResult = Result; /// The result of an action provided by DbApi pub type ActionResult = Result; +pub enum ApiError { + DieselError(diesel::result::Error), + InvalidAction(String), +} + +pub type ApiResult = Result; + +pub enum ApiActions<'a> { + FetchPlayers, + FetchInventory, + // Player actions + FetchLoot(i32), + UpdateWealth(i32, f32), + BuyItems(i32, &'a Vec<(i32, Option)>), + SellItems(i32, &'a Vec<(i32, Option)>), + ClaimItem(i32, i32), + UnclaimItem(i32, i32), + // Group actions + AddLoot(&'a Vec), +} + +pub enum AdminActions { + AddPlayer(String, f32), + //AddInventoryItem(pub String, pub i32), + ResolveClaims, + //SetClaimsTimeout(pub i32), +} /// A wrapper providing an API over the database /// It offers a convenient way to deal with connection. @@ -138,18 +167,20 @@ impl<'q> AsPlayer<'q> { /// /// # Returns /// Result containing the difference in coins after operation - pub fn buy<'a>(self, params: &Vec<(i32, Option)>) -> ActionResult<(Vec, (i32, i32, i32, i32))> { + pub fn buy<'a>( + self, + params: &Vec<(i32, Option)>, + ) -> ActionResult<(Vec, (i32, i32, i32, i32))> { let mut cumulated_diff: Vec<(i32, i32, i32, i32)> = Vec::with_capacity(params.len()); let mut added_items: Vec = 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 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 + None => item.base_price as f32, }; models::player::AsPlayer(self.conn, self.id) .update_wealth(-sell_price) @@ -159,8 +190,13 @@ impl<'q> AsPlayer<'q> { added_items.push(item); } } - 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) + 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)) } @@ -168,10 +204,7 @@ impl<'q> AsPlayer<'q> { /// /// # Returns /// Result containing the difference in coins after operation - pub fn sell( - self, - params: &Vec<(i32, Option)>, - ) -> ActionResult<(i32, i32, i32, i32)> { + pub fn sell(self, params: &Vec<(i32, Option)>) -> ActionResult<(i32, i32, i32, i32)> { let mut all_results: Vec<(i32, i32, i32, i32)> = Vec::with_capacity(params.len()); for (loot_id, price_mod) in params.into_iter() { let res = self.conn.transaction(|| { @@ -186,13 +219,17 @@ impl<'q> AsPlayer<'q> { all_results.push(diff.as_tuple()) } else { // TODO: need to find a better way to deal with errors - return Err(diesel::result::Error::NotFound) + return Err(diesel::result::Error::NotFound); } } - 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) + 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, + ) })) - } /// Adds the value in gold to the player's wealth. @@ -202,7 +239,6 @@ impl<'q> AsPlayer<'q> { 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<()> { @@ -232,8 +268,7 @@ impl<'q> AsAdmin<'q> { /// /// Takes the player name and starting wealth (in gold value). pub fn add_player(self, name: &str, start_wealth: f32) -> ActionResult<()> { - models::player::Players(self.0) - .add(name, start_wealth)?; + models::player::Players(self.0).add(name, start_wealth)?; Ok(()) } @@ -260,10 +295,9 @@ impl<'q> AsAdmin<'q> { self.0.transaction(|| { claim.resolve_claim(self.0)?; //models::item::LootManager(self.0, 0).set_owner(claim.loot_id, claim.player_id)?; - models::player::AsPlayer(self.0, player_id) - .update_debt(item.sell_value()) + models::player::AsPlayer(self.0, player_id).update_debt(item.sell_value()) })?; - }, + } _ => (), } } @@ -355,13 +389,22 @@ mod tests_old { assert_eq!(claims.len(), 0); // Add items - assert_eq!(DbApi::with_conn(&conn).as_admin().add_loot(vec![ - ("Épée", 40), - ("Arc", 40), - ]).is_ok(), true); + assert_eq!( + DbApi::with_conn(&conn) + .as_admin() + .add_loot(vec![("Épée", 40), ("Arc", 40),]) + .is_ok(), + true + ); // Add players - DbApi::with_conn(&conn).as_admin().add_player("Player1", 0.0).unwrap(); - DbApi::with_conn(&conn).as_admin().add_player("Player2", 0.0).unwrap(); + DbApi::with_conn(&conn) + .as_admin() + .add_player("Player1", 0.0) + .unwrap(); + DbApi::with_conn(&conn) + .as_admin() + .add_player("Player2", 0.0) + .unwrap(); // Put claims on one different item each DbApi::with_conn(&conn).as_player(1).claim(1).unwrap(); DbApi::with_conn(&conn).as_player(2).claim(2).unwrap(); @@ -370,7 +413,10 @@ mod tests_old { // Check that both players received an item let players = DbApi::with_conn(&conn).fetch_players().unwrap(); for &i in [1, 2].into_iter() { - assert_eq!(DbApi::with_conn(&conn).as_player(i).loot().unwrap().len(), 1); + assert_eq!( + DbApi::with_conn(&conn).as_player(i).loot().unwrap().len(), + 1 + ); let player = players.get(i as usize).unwrap(); assert_eq!(player.debt, 20); } @@ -448,9 +494,7 @@ mod tests_old { .add_player("Player", 1000.0) .unwrap(); // Buy an item - let bought = DbApi::with_conn(&conn) - .as_player(1) - .buy(&vec![(1, None)]); + let bought = DbApi::with_conn(&conn).as_player(1).buy(&vec![(1, None)]); 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); @@ -461,10 +505,14 @@ mod tests_old { let player = players.get(1).unwrap(); assert_eq!(player.pp, 2); // A player cannot sell loot from an other's chest - let result = DbApi::with_conn(&conn).as_player(0).sell(&vec![(loot.id, None)]); + let result = DbApi::with_conn(&conn) + .as_player(0) + .sell(&vec![(loot.id, None)]); assert_eq!(result.is_ok(), false); // Sell back - let sold = DbApi::with_conn(&conn).as_player(1).sell(&vec![(loot.id, None)]); + let sold = DbApi::with_conn(&conn) + .as_player(1) + .sell(&vec![(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); diff --git a/lootalot_db/src/models/claim.rs b/lootalot_db/src/models/claim.rs index 3a1c826..c3037c8 100644 --- a/lootalot_db/src/models/claim.rs +++ b/lootalot_db/src/models/claim.rs @@ -1,5 +1,5 @@ -use diesel::prelude::*; use crate::{DbConnection, QueryResult}; +use diesel::prelude::*; use crate::models::{self, item::Loot}; use crate::schema::claims; @@ -27,8 +27,7 @@ impl Claim { } fn remove(&self, conn: &DbConnection) -> QueryResult<()> { - diesel::delete(claims::table.find(self.id)) - .execute(conn)?; + diesel::delete(claims::table.find(self.id)).execute(conn)?; Ok(()) } } @@ -36,10 +35,8 @@ impl Claim { pub struct Claims<'q>(pub &'q DbConnection); impl<'q> Claims<'q> { - pub fn all(&self) -> QueryResult> { - claims::table - .load(self.0) + claims::table.load(self.0) } /// Finds a single claim by association of player and loot ids. @@ -85,15 +82,12 @@ impl<'q> Claims<'q> { pub(crate) fn grouped_by_item(&self) -> QueryResult)>> { let group_loot: Vec = Loot::owned_by(0).load(self.0)?; - let claims = claims::table - .load(self.0)? - .grouped_by(&group_loot); - Ok( - group_loot.into_iter() - .map(|loot| loot.into_item()) - .zip(claims) - .collect::>() - ) + let claims = claims::table.load(self.0)?.grouped_by(&group_loot); + Ok(group_loot + .into_iter() + .map(|loot| loot.into_item()) + .zip(claims) + .collect::>()) } } @@ -117,17 +111,23 @@ mod tests { type TestResult = Result<(), diesel::result::Error>; fn test_connection() -> Result { - let conn = DbConnection::establish(":memory:") - .map_err(|_| diesel::result::Error::NotFound)?; + let conn = + DbConnection::establish(":memory:").map_err(|_| diesel::result::Error::NotFound)?; diesel_migrations::run_pending_migrations(&conn) .map_err(|_| diesel::result::Error::NotFound)?; let manager = models::player::Players(&conn); manager.add("Player1", 0.0)?; manager.add("Player2", 0.0)?; - crate::LootManager(&conn, 0) - .add_from(&crate::Item{ id: 0, name: "Epee".to_string(), base_price: 30 })?; - crate::LootManager(&conn, 1) - .add_from(&crate::Item{ id: 0, name: "Arc".to_string(), base_price: 20 })?; + crate::LootManager(&conn, 0).add_from(&crate::Item { + id: 0, + name: "Epee".to_string(), + base_price: 30, + })?; + crate::LootManager(&conn, 1).add_from(&crate::Item { + id: 0, + name: "Arc".to_string(), + base_price: 20, + })?; Ok(conn) } diff --git a/lootalot_db/src/models/item.rs b/lootalot_db/src/models/item.rs index ad9c45b..01bed64 100644 --- a/lootalot_db/src/models/item.rs +++ b/lootalot_db/src/models/item.rs @@ -1,11 +1,9 @@ - - use diesel::dsl::{exists, Eq, Filter, Find, Select}; use diesel::expression::exists::Exists; use diesel::prelude::*; -use crate::{DbConnection, QueryResult}; use crate::schema::{items, looted}; +use crate::{DbConnection, QueryResult}; type ItemColumns = (looted::id, looted::name, looted::base_price); const ITEM_COLUMNS: ItemColumns = (looted::id, looted::name, looted::base_price); type OwnedBy = Select; @@ -35,23 +33,18 @@ impl Item { pub struct Inventory<'q>(pub &'q DbConnection); impl<'q> Inventory<'q> { - pub fn all(&self) -> QueryResult> { items::table.load::(self.0) } pub fn find(&self, item_id: i32) -> QueryResult { - items::table - .find(item_id) - .first::(self.0) + items::table.find(item_id).first::(self.0) } } type WithOwner = Eq; type OwnedLoot = Filter; - - /// Represents an item that has been looted, /// hence has an owner. #[derive(Identifiable, Debug, Queryable, Serialize)] @@ -89,10 +82,8 @@ impl Loot { } pub(super) fn find(id: i32) -> Find { - looted::table - .find(id) + looted::table.find(id) } - } /// Manager for a player's loot @@ -106,28 +97,24 @@ impl<'q> LootManager<'q> { /// Finds an item by id pub fn find(&self, loot_id: i32) -> QueryResult { - Ok( - - Loot::find(loot_id) - .first(self.0) - .and_then(|loot: Loot| { - if loot.owner != self.1 { - Err(diesel::result::Error::NotFound) - } else { - Ok( Item { id: loot.id, name: loot.name, base_price: loot.base_price } ) - } - })? - ) - + Ok(Loot::find(loot_id).first(self.0).and_then(|loot: Loot| { + if loot.owner != self.1 { + Err(diesel::result::Error::NotFound) + } else { + Ok(Item { + id: loot.id, + name: loot.name, + base_price: loot.base_price, + }) + } + })?) } /// The last item added to the chest pub fn last(&self) -> QueryResult { - Ok( - Item::owned_by(self.1) - .order(looted::dsl::id.desc()) - .first(self.0)? - ) + Ok(Item::owned_by(self.1) + .order(looted::dsl::id.desc()) + .first(self.0)?) } /// Adds a copy of the given item inside player chest @@ -137,10 +124,10 @@ impl<'q> LootManager<'q> { base_price: item.base_price, owner_id: self.1, }; - diesel::insert_into(looted::table) - .values(&new_item) - .execute(self.0)?; - self.last() + diesel::insert_into(looted::table) + .values(&new_item) + .execute(self.0)?; + self.last() } pub fn remove(self, item_id: i32) -> QueryResult { @@ -148,8 +135,6 @@ impl<'q> LootManager<'q> { diesel::delete(looted::table.find(deleted.id)).execute(self.0)?; Ok(deleted) } - - } /// An item being looted or bought. diff --git a/lootalot_db/src/models/mod.rs b/lootalot_db/src/models/mod.rs index 286f11d..b69d581 100644 --- a/lootalot_db/src/models/mod.rs +++ b/lootalot_db/src/models/mod.rs @@ -3,5 +3,5 @@ pub mod item; pub mod player; pub use claim::Claim; -pub use item::{Item}; +pub use item::Item; pub use player::{Player, Wealth}; diff --git a/lootalot_db/src/models/player.rs b/lootalot_db/src/models/player.rs index c9fb23f..c2598bf 100644 --- a/lootalot_db/src/models/player.rs +++ b/lootalot_db/src/models/player.rs @@ -1,6 +1,6 @@ -use diesel::prelude::*; -use crate::{DbConnection, QueryResult}; use crate::schema::players; +use crate::{DbConnection, QueryResult}; +use diesel::prelude::*; /// Representation of a player in database #[derive(Debug, Queryable, Serialize)] @@ -25,19 +25,15 @@ pub struct Player { pub struct Players<'q>(pub &'q DbConnection); impl<'q> Players<'q> { - pub fn all(&self) -> QueryResult> { - players::table - .load(self.0) + players::table.load(self.0) } pub fn add(&self, name: &str, wealth: f32) -> QueryResult { diesel::insert_into(players::table) .values(&NewPlayer::create(name, wealth)) .execute(self.0)?; - players::table - .order(players::dsl::id.desc()) - .first(self.0) + players::table.order(players::dsl::id.desc()).first(self.0) } } @@ -67,15 +63,12 @@ impl<'q> AsPlayer<'q> { pub fn update_debt(&self, value_in_gp: i32) -> QueryResult<()> { diesel::update(players::table.find(self.1)) - .set(players::dsl::debt.eq( - players::dsl::debt + value_in_gp - )) + .set(players::dsl::debt.eq(players::dsl::debt + value_in_gp)) .execute(self.0)?; Ok(()) } } - /// Unpack a floating value of gold pieces to integer /// values of copper, silver, gold and platinum pieces /// diff --git a/lootalot_db/src/schema.rs b/lootalot_db/src/schema.rs index 5e7b23e..3989f64 100644 --- a/lootalot_db/src/schema.rs +++ b/lootalot_db/src/schema.rs @@ -40,9 +40,4 @@ joinable!(claims -> looted (loot_id)); joinable!(claims -> players (player_id)); joinable!(looted -> players (owner_id)); -allow_tables_to_appear_in_same_query!( - claims, - items, - looted, - players, -); +allow_tables_to_appear_in_same_query!(claims, items, looted, players,); diff --git a/lootalot_db/src/updates.rs b/lootalot_db/src/updates.rs index 5be23f5..c879d13 100644 --- a/lootalot_db/src/updates.rs +++ b/lootalot_db/src/updates.rs @@ -3,8 +3,8 @@ //! //! Contains semantic mutations of database //! -use crate::DbConnection; use crate::models::player::Wealth; +use crate::DbConnection; type PlayerId = i32; type ItemId = i32; @@ -16,7 +16,11 @@ enum LootUpdate { } impl LootUpdate { - fn add_loot(conn: &DbConnection, to_player: PlayerId, item_desc: Item) -> Result { + fn add_loot( + conn: &DbConnection, + to_player: PlayerId, + item_desc: Item, + ) -> Result { use schema::looted::dsl::*; let new_item = models::item::NewLoot::to_player(to_player, &item_desc); diesel::insert_into(looted) @@ -41,10 +45,10 @@ impl LootUpdate { match self { LootUpdate::AddedItem(item_id) => { // Remove the item - }, + } LootUpdate::RemovedItem(item) => { // Add the item back - }, + } LootUpdate::GivenToPlayer(item_id) => { // Change owner to group } diff --git a/src/server.rs b/src/server.rs index ad3a103..fe6709a 100644 --- a/src/server.rs +++ b/src/server.rs @@ -2,8 +2,8 @@ use actix_cors::Cors; use actix_files as fs; use actix_web::{web, App, Error, HttpResponse, HttpServer}; use futures::Future; -use lootalot_db::models::Item; use lootalot_db::{DbApi, Pool, QueryResult}; +use lootalot_db::{Item, LootManager}; use serde::{Deserialize, Serialize}; use std::env; @@ -33,14 +33,10 @@ type AppPool = web::Data; pub fn db_call(pool: AppPool, query: Q) -> impl Future where J: serde::ser::Serialize + Send + 'static, - Q: Fn(DbApi) -> QueryResult + Send + 'static, + Q: Fn(DbConnection) -> QueryResult + Send + 'static, { let conn = pool.get().unwrap(); - web::block(move || { - let api = DbApi::with_conn(&conn); - query(api) - }) - .then(|res| match res { + web::block(move || query(conn)).then(|res| match res { Ok(r) => HttpResponse::Ok().json(r), Err(e) => { dbg!(&e); @@ -77,42 +73,58 @@ mod endpoints { items: Vec<(i32, Option)>, } - pub fn players_list(pool: AppPool) -> impl Future{ + pub fn players_list(pool: AppPool) -> impl Future { db_call(pool, move |api| api.fetch_players()) } - pub fn player_loot(pool: AppPool, player_id: web::Path) -> impl Future{ - db_call(pool, move |api| api.as_player(*player_id).loot()) + pub fn player_loot( + pool: AppPool, + player_id: web::Path, + ) -> impl Future { + db_call(pool, move |conn| LootManager(&conn, *player_id).all()) } - pub fn update_wealth(pool: AppPool, data: web::Json) -> impl Future{ + pub fn update_wealth( + pool: AppPool, + data: web::Json, + ) -> impl Future { db_call(pool, move |api| { api.as_player(data.player_id) .update_wealth(data.value_in_gp) }) } - pub fn buy_item(pool: AppPool, data: web::Json) -> impl Future{ + pub fn buy_item( + pool: AppPool, + data: web::Json, + ) -> impl Future { db_call(pool, move |api| { api.as_player(data.player_id).buy(&data.items) }) } - pub fn sell_item(pool: AppPool, data: web::Json) -> impl Future{ + pub fn sell_item( + pool: AppPool, + data: web::Json, + ) -> impl Future { db_call(pool, move |api| { api.as_player(data.player_id).sell(&data.items) }) } - pub fn player_claims(pool: AppPool) -> impl Future{ + pub fn player_claims(pool: AppPool) -> impl Future { db_call(pool, move |api| api.fetch_claims()) } - pub fn put_claim(pool: AppPool, (player, loot): (web::Path, web::Json)) -> impl Future{ - db_call(pool, move |api| { - api.as_player(*player).claim(loot.item_id) - }) + pub fn put_claim( + pool: AppPool, + (player, loot): (web::Path, web::Json), + ) -> impl Future { + db_call(pool, move |api| api.as_player(*player).claim(loot.item_id)) } - pub fn delete_claim(pool: AppPool, (player, data): (web::Path, web::Json)) -> impl Future{ + pub fn delete_claim( + pool: AppPool, + (player, data): (web::Path, web::Json), + ) -> impl Future { db_call(pool, move |api| { api.as_player(*player).unclaim(data.item_id) }) @@ -137,7 +149,10 @@ pub(crate) fn serve() -> std::io::Result<()> { web::scope("/api") .service( web::scope("/players") - .service( web::resource("/").route(web::get().to_async(endpoints::players_list))) // List of players + .service( + web::resource("/") + .route(web::get().to_async(endpoints::players_list)), + ) // List of players //.route(web::put().to_async(endpoints::new_player)) // Create/Update player .service( web::scope("/{player_id}") @@ -161,10 +176,7 @@ pub(crate) fn serve() -> std::io::Result<()> { ), ), ) - .route( - "/claims", - web::get().to_async(endpoints::player_claims) - ) + .route("/claims", web::get().to_async(endpoints::player_claims)) .route( "/items", web::get().to_async(move |pool: AppPool| { From 8af7790d172e26f052af7ec3c76039780f083209 Mon Sep 17 00:00:00 2001 From: Artus Date: Tue, 15 Oct 2019 16:22:46 +0200 Subject: [PATCH 10/23] try out a generic ApiResponse --- src/server.rs | 64 +++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 57 insertions(+), 7 deletions(-) diff --git a/src/server.rs b/src/server.rs index fe6709a..b08d727 100644 --- a/src/server.rs +++ b/src/server.rs @@ -4,11 +4,30 @@ use actix_web::{web, App, Error, HttpResponse, HttpServer}; use futures::Future; use lootalot_db::{DbApi, Pool, QueryResult}; use lootalot_db::{Item, LootManager}; +use lootalot_db as db; use serde::{Deserialize, Serialize}; use std::env; +use std::default; type AppPool = web::Data; +#[derive(Serialize, Deserialize, Debug)] +enum Update { + NoUpdate, + Wealth, + ItemAdded, + ItemRemoved, + ClaimAdded, + ClaimRemoved, +} + +#[derive(Serialize, Deserialize, Debug, Default)] +struct ApiResponse { + value: Option, // The value requested, if any + notify: Option, // A text to notify user, if relevant + updates: Option>, // A list of updates, if any + errors: Option, // A text describing errors, if any +} /// Wraps call to the DbApi and process its result as a async HttpResponse /// /// Provides a convenient way to call the api inside a route definition. Given a connection pool, @@ -33,7 +52,7 @@ type AppPool = web::Data; pub fn db_call(pool: AppPool, query: Q) -> impl Future where J: serde::ser::Serialize + Send + 'static, - Q: Fn(DbConnection) -> QueryResult + Send + 'static, + Q: Fn(DbConnection) -> ApiResponse + Send + 'static, { let conn = pool.get().unwrap(); web::block(move || query(conn)).then(|res| match res { @@ -74,23 +93,54 @@ mod endpoints { } pub fn players_list(pool: AppPool) -> impl Future { - db_call(pool, move |api| api.fetch_players()) + db_call(pool, move |conn| { + let (value, errors) = match db::Players(conn).all() { + Ok(v) => (Some(v), None), + Err(e) => (None, Some(e.to_string())), + }; + ApiResponse { + value, + errors, + ..Default::default() + } + }) } pub fn player_loot( pool: AppPool, player_id: web::Path, ) -> impl Future { - db_call(pool, move |conn| LootManager(&conn, *player_id).all()) + db_call(pool, move |conn| { + let (value, errors) = { + match db::LootManager(&conn, *player_id).all() { + Ok(v) => (Some(v), None), + Err(e) => (None, Some(e.to_string())), + } + }; + ApiResponse { + value, + errors, + ..Default::default() + } + }) } pub fn update_wealth( pool: AppPool, - data: web::Json, + (player, data): (web::Path, web::Json), ) -> impl Future { - db_call(pool, move |api| { - api.as_player(data.player_id) - .update_wealth(data.value_in_gp) + db_call(pool, move |conn| { + let (updates, errors) = + match db::AsPlayer(conn, player) + .update_wealth(data.value_in_gp) { + Ok(w) => (Some(vec![w.as_tuple(),]), None), + Err(e) => (None, Some(e.to_string())), + }; + ApiResponse { + updates, + errors, + ..Default::default() + } }) } From 8cfa21eccfa650926e15fb6db8a2e2525c869636 Mon Sep 17 00:00:00 2001 From: Artus Date: Wed, 16 Oct 2019 22:29:38 +0200 Subject: [PATCH 11/23] stablizing ApiResponse inside lootalot_db crate --- lootalot_db/src/lib.rs | 351 +++++++++++++++---------------- lootalot_db/src/models/claim.rs | 2 +- lootalot_db/src/models/item.rs | 2 +- lootalot_db/src/models/player.rs | 4 +- 4 files changed, 170 insertions(+), 189 deletions(-) diff --git a/lootalot_db/src/lib.rs b/lootalot_db/src/lib.rs index 2284e8b..6f35223 100644 --- a/lootalot_db/src/lib.rs +++ b/lootalot_db/src/lib.rs @@ -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 = Result; /// The result of an action provided by DbApi pub type ActionResult = Result; +#[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), + ClaimList(Vec), + PlayerList(Vec), +} + +#[derive(Serialize, Deserialize, Debug, Default)] +pub struct ApiResponse { + pub value: Option, // The value requested, if any + pub notify: Option, // A text to notify user, if relevant + pub updates: Option>, // A list of updates, if any + pub errors: Option, // 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>(&mut self, text: S) { + self.notify = Some(text.into()); + } +} + + pub enum ApiError { DieselError(diesel::result::Error), InvalidAction(String), } -pub type ApiResult = Result; - 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> { - Ok(schema::players::table.load::(self.0)?) - } - /// Fetch the inventory of items - /// - /// TODO: remove limit used for debug - pub fn fetch_inventory(self) -> QueryResult> { - models::item::Inventory(self.0).all() - } - /// Fetch all existing claims - pub fn fetch_claims(self) -> QueryResult> { - Ok(schema::claims::table.load::(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> { - 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) - /// - /// # Returns - /// Result containing the difference in coins after operation - pub fn buy<'a>( - self, - params: &Vec<(i32, Option)>, - ) -> ActionResult<(Vec, (i32, i32, i32, i32))> { - let mut cumulated_diff: Vec<(i32, i32, i32, i32)> = Vec::with_capacity(params.len()); - let mut added_items: Vec = 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 { + 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::(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 = Vec::with_capacity(params.len()); + let mut added_items: Vec = 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> { + schema::claims::table.load::(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)>) -> 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)>, + response: &mut ApiResponse, +) { + let mut all_results: Vec = 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); diff --git a/lootalot_db/src/models/claim.rs b/lootalot_db/src/models/claim.rs index c3037c8..107a1c7 100644 --- a/lootalot_db/src/models/claim.rs +++ b/lootalot_db/src/models/claim.rs @@ -5,7 +5,7 @@ use crate::models::{self, item::Loot}; use crate::schema::claims; /// 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)] pub struct Claim { /// DB Identifier diff --git a/lootalot_db/src/models/item.rs b/lootalot_db/src/models/item.rs index 01bed64..3ac9fd4 100644 --- a/lootalot_db/src/models/item.rs +++ b/lootalot_db/src/models/item.rs @@ -47,7 +47,7 @@ type OwnedLoot = Filter; /// Represents an item that has been looted, /// hence has an owner. -#[derive(Identifiable, Debug, Queryable, Serialize)] +#[derive(Identifiable, Debug, Queryable)] #[table_name = "looted"] pub(super) struct Loot { id: i32, diff --git a/lootalot_db/src/models/player.rs b/lootalot_db/src/models/player.rs index c2598bf..2b79383 100644 --- a/lootalot_db/src/models/player.rs +++ b/lootalot_db/src/models/player.rs @@ -3,7 +3,7 @@ use crate::{DbConnection, QueryResult}; use diesel::prelude::*; /// Representation of a player in database -#[derive(Debug, Queryable, Serialize)] +#[derive(Queryable, Serialize, Deserialize, Debug)] pub struct Player { /// DB Identitier pub id: i32, @@ -91,7 +91,7 @@ fn unpack_gold_value(gold: f32) -> (i32, i32, i32, i32) { /// /// Values are held as individual pieces counts. /// Allows conversion from and to a floating amount of gold pieces. -#[derive(Queryable, AsChangeset, Debug)] +#[derive(Queryable, AsChangeset, Serialize, Deserialize, Debug)] #[table_name = "players"] pub struct Wealth { pub cp: i32, From 2222422f8a66daf70ed11f353cea748ae3949dd7 Mon Sep 17 00:00:00 2001 From: Artus Date: Wed, 16 Oct 2019 22:32:58 +0200 Subject: [PATCH 12/23] removes ApiResponse from server --- src/server.rs | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/server.rs b/src/server.rs index b08d727..2ead15a 100644 --- a/src/server.rs +++ b/src/server.rs @@ -11,23 +11,6 @@ use std::default; type AppPool = web::Data; -#[derive(Serialize, Deserialize, Debug)] -enum Update { - NoUpdate, - Wealth, - ItemAdded, - ItemRemoved, - ClaimAdded, - ClaimRemoved, -} - -#[derive(Serialize, Deserialize, Debug, Default)] -struct ApiResponse { - value: Option, // The value requested, if any - notify: Option, // A text to notify user, if relevant - updates: Option>, // A list of updates, if any - errors: Option, // A text describing errors, if any -} /// Wraps call to the DbApi and process its result as a async HttpResponse /// /// Provides a convenient way to call the api inside a route definition. Given a connection pool, From 3cc45397da4dd95a0e3983c9ffaba0e4711903af Mon Sep 17 00:00:00 2001 From: Artus Date: Thu, 17 Oct 2019 11:58:00 +0200 Subject: [PATCH 13/23] fixes errors, cleans up --- lootalot_db/src/lib.rs | 244 +++++++++++++-------------------- src/server.rs | 302 ++++++++++++----------------------------- 2 files changed, 182 insertions(+), 364 deletions(-) diff --git a/lootalot_db/src/lib.rs b/lootalot_db/src/lib.rs index 6f35223..1626c37 100644 --- a/lootalot_db/src/lib.rs +++ b/lootalot_db/src/lib.rs @@ -33,8 +33,7 @@ pub type QueryResult = Result; pub type ActionResult = Result; #[derive(Serialize, Deserialize, Debug)] -enum Update { - NoUpdate, +pub enum Update { Wealth(Wealth), ItemAdded(Item), ItemRemoved(Item), @@ -43,7 +42,7 @@ enum Update { } #[derive(Serialize, Deserialize, Debug)] -enum Value { +pub enum Value { Item(Item), Claim(Claim), ItemList(Vec), @@ -53,10 +52,10 @@ enum Value { #[derive(Serialize, Deserialize, Debug, Default)] pub struct ApiResponse { - pub value: Option, // The value requested, if any - pub notify: Option, // A text to notify user, if relevant + pub value: Option, // The value requested, if any + pub notify: Option, // A text to notify user, if relevant pub updates: Option>, // A list of updates, if any - pub errors: Option, // A text describing errors, if any + pub errors: Option, // A text describing errors, if any } impl ApiResponse { @@ -68,6 +67,14 @@ impl ApiResponse { } } + fn push_error>(&mut self, error: S) { + if let Some(errors) = self.errors.as_mut() { + *errors = format!("{}\n{}", errors, error.into()); + } else { + self.errors = Some(error.into()) + } + } + fn set_value(&mut self, value: Value) { self.value = Some(value); } @@ -77,24 +84,23 @@ impl ApiResponse { } } - pub enum ApiError { DieselError(diesel::result::Error), InvalidAction(String), } -pub enum ApiActions<'a> { +pub enum ApiActions { FetchPlayers, FetchInventory, // Player actions FetchLoot(i32), UpdateWealth(i32, f32), - BuyItems(i32, &'a Vec<(i32, Option)>), - SellItems(i32, &'a Vec<(i32, Option)>), + BuyItems(i32, Vec<(i32, Option)>), + SellItems(i32, Vec<(i32, Option)>), ClaimItem(i32, i32), UnclaimItem(i32, i32), // Group actions - AddLoot(&'a Vec), + AddLoot(Vec), } pub enum AdminActions { @@ -104,38 +110,26 @@ pub enum AdminActions { //SetClaimsTimeout(pub i32), } - -pub fn execute<'a>(pool: Pool, query: ApiActions<'a>) -> Result { - let conn = pool.get().map_err(|e| {dbg!(e); diesel::result::Error::NotFound })?; +pub fn execute( + conn: &DbConnection, + query: ApiActions, +) -> Result { let mut response = ApiResponse::default(); match query { ApiActions::FetchPlayers => { - response.set_value( - Value::PlayerList( - schema::players::table.load::(conn)? - ) - ); - }, + response.set_value(Value::PlayerList(models::player::Players(conn).all()?)); + } ApiActions::FetchInventory => { - response.set_value( - Value::ItemList( - models::item::Inventory(conn).all()?)); + response.set_value(Value::ItemList(models::item::Inventory(conn).all()?)); } ApiActions::FetchLoot(id) => { - response.set_value( - Value::ItemList( - models::item::LootManager(conn, id).all()? - ) - ); - }, + 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)? - ) - ); - }, + response.push_update(Update::Wealth( + models::player::AsPlayer(conn, id).update_wealth(amount)?, + )); + } ApiActions::BuyItems(id, params) => { let mut cumulated_diff: Vec = Vec::with_capacity(params.len()); let mut added_items: Vec = Vec::with_capacity(params.len()); @@ -143,7 +137,7 @@ pub fn execute<'a>(pool: Pool, query: ApiActions<'a>) -> Result item.base_price as f32 * modifier, @@ -156,134 +150,92 @@ pub fn execute<'a>(pool: Pool, query: ApiActions<'a>) -> Result { - sell(conn, id, params, &mut response); - }, + let mut all_results: Vec = Vec::with_capacity(params.len()); + for (loot_id, price_mod) in params.into_iter() { + 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(conn, id) + .update_wealth(sell_value) + .map(|diff| (deleted, diff)) + }); + if let Ok((deleted, diff)) = res { + all_results.push(diff); + response.push_update(Update::ItemRemoved(deleted)); + } else { + response.push_error(format!("Error selling {}", loot_id)); + } + } + 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)); + } ApiActions::ClaimItem(id, item) => { - response.push_update( - Update::ClaimAdded( - models::claim::Claims(conn) - .add(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)? - ) - ); - }, + response.push_update(Update::ClaimRemoved( + models::claim::Claims(conn).remove(id, item)?, + )); + } // Group actions - ApiActions::AddLoot(items) => {}, + ApiActions::AddLoot(items) => {} } Ok(response) } - /// Fetch all existing claims - pub fn fetch_claims(conn: &DbConnection) -> QueryResult> { - schema::claims::table.load::(conn) - } +/// Fetch all existing claims +pub fn fetch_claims(conn: &DbConnection) -> QueryResult> { + schema::claims::table.load::(conn) +} - /// Sell a set of items from this player chest - /// - /// # Returns - /// Result containing the difference in coins after operation -pub fn sell( - conn: &DbConnection, - id: i32, - params: &Vec<(i32, Option)>, - response: &mut ApiResponse, -) { - let mut all_results: Vec = Vec::with_capacity(params.len()); - for (loot_id, price_mod) in params.into_iter() { - 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(conn, id) - .update_wealth(sell_value) - .map(|diff| (deleted, diff)) - }); - if let Ok((deleted, diff)) = res { - all_results.push(diff); - response.push_update( - Update::ItemRemoved(deleted) - ); - } else { - response.errors = Some(format!("Error selling {}", loot_id)); +/// 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(conn: &DbConnection) -> QueryResult<()> { + let data = models::claim::Claims(conn).grouped_by_item()?; + dbg!(&data); + + for (item, claims) in data { + match claims.len() { + 1 => { + let claim = claims.get(0).unwrap(); + let player_id = claim.player_id; + conn.transaction(|| { + claim.resolve_claim(conn)?; + //models::item::LootManager(self.0, 0).set_owner(claim.loot_id, claim.player_id)?; + models::player::AsPlayer(conn, player_id).update_debt(item.sell_value()) + })?; } + _ => (), } - 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)); - } - -/// Wrapper for interactions of admins with the DB. -pub struct AsAdmin<'q>(&'q DbConnection); - -impl<'q> AsAdmin<'q> { - /// Adds a player to the database - /// - /// Takes the player name and starting wealth (in gold value). - pub fn add_player(self, name: &str, start_wealth: f32) -> ActionResult<()> { - models::player::Players(self.0).add(name, start_wealth)?; - Ok(()) - } - - /// Adds a list of items to the group loot - pub fn add_loot(self, items: Vec) -> ActionResult<()> { - for item_desc in items.iter() { - models::item::LootManager(self.0, 0).add_from(item_desc)?; - } - 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<()> { - let data = models::claim::Claims(self.0).grouped_by_item()?; - dbg!(&data); - - for (item, claims) in data { - match claims.len() { - 1 => { - let claim = claims.get(0).unwrap(); - let player_id = claim.player_id; - self.0.transaction(|| { - claim.resolve_claim(self.0)?; - //models::item::LootManager(self.0, 0).set_owner(claim.loot_id, claim.player_id)?; - models::player::AsPlayer(self.0, player_id).update_debt(item.sell_value()) - })?; - } - _ => (), - } - } - Ok(()) } + Ok(()) } /// Sets up a connection pool and returns it. diff --git a/src/server.rs b/src/server.rs index 2ead15a..1093337 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,44 +1,24 @@ use actix_cors::Cors; use actix_files as fs; -use actix_web::{web, App, Error, HttpResponse, HttpServer}; +use actix_web::{web, middleware, App, Error, HttpResponse, HttpServer}; use futures::Future; -use lootalot_db::{DbApi, Pool, QueryResult}; -use lootalot_db::{Item, LootManager}; -use lootalot_db as db; -use serde::{Deserialize, Serialize}; use std::env; -use std::default; -type AppPool = web::Data; +use lootalot_db as db; -/// Wraps call to the DbApi and process its result as a async HttpResponse -/// -/// Provides a convenient way to call the api inside a route definition. Given a connection pool, -/// access to the api is granted in a closure. The closure is called in a blocking way and should -/// return a QueryResult. -/// If the query succeeds, it's result is returned as JSON data. Otherwise, an InternalServerError -/// is returned. -/// -/// # Usage -/// ``` -/// (...) -/// .route("path/to/", -/// move |pool: web::Data| { -/// // user data can be processed here -/// // ... -/// db_call(pool, move |api| { -/// // ...do what you want with the api -/// } -/// } -/// ) -/// ``` -pub fn db_call(pool: AppPool, query: Q) -> impl Future -where - J: serde::ser::Serialize + Send + 'static, - Q: Fn(DbConnection) -> ApiResponse + Send + 'static, +type AppPool = web::Data; +type PlayerId = web::Path; +type ItemId = web::Json; +type ItemListWithMods = web::Json)>>; + +/// Wraps call to the database query and convert its result as a async HttpResponse +pub fn db_call( + pool: AppPool, + query: db::ApiActions, +) -> impl Future { let conn = pool.get().unwrap(); - web::block(move || query(conn)).then(|res| match res { + web::block(move || db::execute(&conn, query)).then(|res| match res { Ok(r) => HttpResponse::Ok().json(r), Err(e) => { dbg!(&e); @@ -47,205 +27,91 @@ where }) } -mod endpoints { - - use super::*; - - #[derive(Serialize, Deserialize, Debug)] - pub struct PlayerClaim { - player_id: i32, - item_id: i32, - } - - #[derive(Serialize, Deserialize, Debug)] - pub struct WealthUpdate { - player_id: i32, - value_in_gp: f32, - } - - #[derive(Serialize, Deserialize, Debug)] - pub struct NewPlayer { - pub name: String, - pub wealth: f32, - } - - #[derive(Serialize, Deserialize, Debug)] - pub struct LootUpdate { - player_id: i32, - items: Vec<(i32, Option)>, - } - - pub fn players_list(pool: AppPool) -> impl Future { - db_call(pool, move |conn| { - let (value, errors) = match db::Players(conn).all() { - Ok(v) => (Some(v), None), - Err(e) => (None, Some(e.to_string())), - }; - ApiResponse { - value, - errors, - ..Default::default() - } - }) - } - - pub fn player_loot( - pool: AppPool, - player_id: web::Path, - ) -> impl Future { - db_call(pool, move |conn| { - let (value, errors) = { - match db::LootManager(&conn, *player_id).all() { - Ok(v) => (Some(v), None), - Err(e) => (None, Some(e.to_string())), - } - }; - ApiResponse { - value, - errors, - ..Default::default() - } - }) - } - - pub fn update_wealth( - pool: AppPool, - (player, data): (web::Path, web::Json), - ) -> impl Future { - db_call(pool, move |conn| { - let (updates, errors) = - match db::AsPlayer(conn, player) - .update_wealth(data.value_in_gp) { - Ok(w) => (Some(vec![w.as_tuple(),]), None), - Err(e) => (None, Some(e.to_string())), - }; - ApiResponse { - updates, - errors, - ..Default::default() - } - }) - } - - pub fn buy_item( - pool: AppPool, - data: web::Json, - ) -> impl Future { - db_call(pool, move |api| { - api.as_player(data.player_id).buy(&data.items) - }) - } - - pub fn sell_item( - pool: AppPool, - data: web::Json, - ) -> impl Future { - db_call(pool, move |api| { - api.as_player(data.player_id).sell(&data.items) - }) - } - pub fn player_claims(pool: AppPool) -> impl Future { - db_call(pool, move |api| api.fetch_claims()) - } - - pub fn put_claim( - pool: AppPool, - (player, loot): (web::Path, web::Json), - ) -> impl Future { - db_call(pool, move |api| api.as_player(*player).claim(loot.item_id)) - } - pub fn delete_claim( - pool: AppPool, - (player, data): (web::Path, web::Json), - ) -> impl Future { - db_call(pool, move |api| { - api.as_player(*player).unclaim(data.item_id) - }) - } +fn configure_app(config: &mut web::ServiceConfig) { + config.service( + web::scope("/api") + .service( + web::scope("/players") + .service( + web::resource("/").route( + web::get().to_async(|pool| db_call(pool, db::ApiActions::FetchPlayers)), + ), //.route(web::post().to_async(endpoints::new_player)) + ) // List of players + .service( + web::scope("/{player_id}") + //.route(web::get().to_async(...)) // Details of player + .service( + web::resource("/claims") + //.route(web::get().to_async(endpoints::player_claims)) + .route(web::put().to_async( + |pool, (player, data): (PlayerId, ItemId)| { + db_call(pool, db::ApiActions::ClaimItem(*player, *data)) + }, + )) + .route(web::delete().to_async( + |pool, (player, data): (PlayerId, ItemId)| { + db_call( + pool, + db::ApiActions::UnclaimItem(*player, *data), + ) + }, + )), + ) + .service( + web::resource("/wealth") + //.route(web::get().to_async(...)) + .route(web::put().to_async( + |pool, (player, data): (PlayerId, web::Json)| { + db_call( + pool, + db::ApiActions::UpdateWealth(*player, *data), + ) + }, + )), + ) + .service( + web::resource("/loot") + .route(web::get().to_async(|pool, player: PlayerId| { + db_call(pool, db::ApiActions::FetchLoot(*player)) + })) + .route(web::put().to_async( + move |pool, (player, data): (PlayerId, ItemListWithMods)| { + db_call(pool, db::ApiActions::BuyItems(*player, data.into_inner())) + }, + )) + .route(web::delete().to_async( + move |pool, (player, data): (PlayerId, ItemListWithMods)| { + db_call(pool, db::ApiActions::SellItems(*player, data.into_inner())) + }, + )), + ), + ), + ) + //.route("/claims", web::get().to_async(endpoints::player_claims)) + .route( + "/items", + web::get() + .to_async(move |pool: AppPool| db_call(pool, db::ApiActions::FetchInventory)), + ), + ); } -pub(crate) fn serve() -> std::io::Result<()> { +pub fn serve() -> std::io::Result<()> { let www_root: String = env::var("WWW_ROOT").expect("WWW_ROOT must be set"); + let pool = db::create_pool(); dbg!(&www_root); - let pool = lootalot_db::create_pool(); HttpServer::new(move || { App::new() .data(pool.clone()) + .configure(configure_app) .wrap( Cors::new() .allowed_origin("http://localhost:8080") .allowed_methods(vec!["GET", "POST", "PUT", "DELETE", "OPTIONS"]) .max_age(3600), ) - .service( - web::scope("/api") - .service( - web::scope("/players") - .service( - web::resource("/") - .route(web::get().to_async(endpoints::players_list)), - ) // List of players - //.route(web::put().to_async(endpoints::new_player)) // Create/Update player - .service( - web::scope("/{player_id}") - //.route(web::get().to_async(...)) // Details of player - .service( - web::resource("/claims") - //.route(web::get().to_async(endpoints::player_claims)) - .route(web::put().to_async(endpoints::put_claim)) - .route(web::delete().to_async(endpoints::delete_claim)), - ) - .service( - web::resource("/wealth") - //.route(web::get().to_async(...)) - .route(web::put().to_async(endpoints::update_wealth)), - ) - .service( - web::resource("/loot") - .route(web::get().to_async(endpoints::player_loot)) - .route(web::put().to_async(endpoints::buy_item)) - .route(web::delete().to_async(endpoints::sell_item)), - ), - ), - ) - .route("/claims", web::get().to_async(endpoints::player_claims)) - .route( - "/items", - web::get().to_async(move |pool: AppPool| { - db_call(pool, move |api| api.fetch_inventory()) - }), - ) - .service( - web::scope("/admin") - .route( - "/resolve-claims", - web::get().to_async(move |pool: AppPool| { - db_call(pool, move |api| api.as_admin().resolve_claims()) - }), - ) - .route( - "/add-loot", - web::post().to_async( - move |pool: AppPool, data: web::Json>| { - db_call(pool, move |api| { - api.as_admin().add_loot(data.to_vec()) - }) - }, - ), - ) - .route( - "/add-player", - web::get().to_async( - move |pool: AppPool, data: web::Json| { - db_call(pool, move |api| { - api.as_admin().add_player(&data.name, data.wealth) - }) - }, - ), - ), - ), - ) + .wrap(middleware::Logger::default()) .service(fs::Files::new("/", www_root.clone()).index_file("index.html")) }) .bind("127.0.0.1:8088")? From 185e1403e30260b0d3289a5ee8089285965b4997 Mon Sep 17 00:00:00 2001 From: Artus Date: Thu, 17 Oct 2019 12:56:07 +0200 Subject: [PATCH 14/23] moves API logic inside its own module --- lootalot_db/src/lib.rs | 240 ++++++++----------------------- lootalot_db/src/models/player.rs | 15 ++ src/api.rs | 166 +++++++++++++++++++++ src/lib.rs | 0 src/main.rs | 3 +- src/server.rs | 24 ++-- 6 files changed, 252 insertions(+), 196 deletions(-) create mode 100644 src/api.rs create mode 100644 src/lib.rs diff --git a/lootalot_db/src/lib.rs b/lootalot_db/src/lib.rs index 1626c37..bb55bef 100644 --- a/lootalot_db/src/lib.rs +++ b/lootalot_db/src/lib.rs @@ -19,8 +19,8 @@ mod schema; pub use models::{ claim::{Claim, Claims}, - item::{Item, LootManager}, - player::{Player, Players, Wealth}, + item::{Item, LootManager, Inventory}, + player::{Player, Wealth, Players, AsPlayer}, }; /// The connection used @@ -29,184 +29,66 @@ pub type DbConnection = SqliteConnection; pub type Pool = r2d2::Pool>; /// The result of a query on DB pub type QueryResult = Result; -/// The result of an action provided by DbApi -pub type ActionResult = Result; -#[derive(Serialize, Deserialize, Debug)] -pub enum Update { - Wealth(Wealth), - ItemAdded(Item), - ItemRemoved(Item), - ClaimAdded(Claim), - ClaimRemoved(Claim), +/// Sets up a connection pool and returns it. +/// Uses the DATABASE_URL environment variable (must be set) +pub fn create_pool() -> Pool { + let connspec = std::env::var("DATABASE_URL").expect("DATABASE_URL"); + dbg!(&connspec); + let manager = ConnectionManager::::new(connspec); + r2d2::Pool::builder() + .build(manager) + .expect("Failed to create pool.") } -#[derive(Serialize, Deserialize, Debug)] -pub enum Value { - Item(Item), - Claim(Claim), - ItemList(Vec), - ClaimList(Vec), - PlayerList(Vec), -} -#[derive(Serialize, Deserialize, Debug, Default)] -pub struct ApiResponse { - pub value: Option, // The value requested, if any - pub notify: Option, // A text to notify user, if relevant - pub updates: Option>, // A list of updates, if any - pub errors: Option, // 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 push_error>(&mut self, error: S) { - if let Some(errors) = self.errors.as_mut() { - *errors = format!("{}\n{}", errors, error.into()); - } else { - self.errors = Some(error.into()) - } - } - - fn set_value(&mut self, value: Value) { - self.value = Some(value); - } - - fn notifiy>(&mut self, text: S) { - self.notify = Some(text.into()); - } -} - -pub enum ApiError { - DieselError(diesel::result::Error), - InvalidAction(String), -} - -pub enum ApiActions { - FetchPlayers, - FetchInventory, - // Player actions - FetchLoot(i32), - UpdateWealth(i32, f32), - BuyItems(i32, Vec<(i32, Option)>), - SellItems(i32, Vec<(i32, Option)>), - ClaimItem(i32, i32), - UnclaimItem(i32, i32), - // Group actions - AddLoot(Vec), -} - -pub enum AdminActions { - AddPlayer(String, f32), - //AddInventoryItem(pub String, pub i32), - ResolveClaims, - //SetClaimsTimeout(pub i32), -} - -pub fn execute( +/// Sells a single item inside a transaction +/// +/// # Returns +/// The deleted entity and the updated Wealth (as a difference from previous value) +pub fn sell_item_transaction( conn: &DbConnection, - query: ApiActions, -) -> Result { - let mut response = ApiResponse::default(); - match query { - ApiActions::FetchPlayers => { - response.set_value(Value::PlayerList(models::player::Players(conn).all()?)); + id: i32, + loot_id: i32, + price_mod: Option, +) -> QueryResult<(Item, Wealth)> { + conn.transaction(|| { + let deleted = 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; } - ApiActions::FetchInventory => { - response.set_value(Value::ItemList(models::item::Inventory(conn).all()?)); - } - 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 = Vec::with_capacity(params.len()); - let mut added_items: Vec = 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.push_error(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) => { - let mut all_results: Vec = Vec::with_capacity(params.len()); - for (loot_id, price_mod) in params.into_iter() { - 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(conn, id) - .update_wealth(sell_value) - .map(|diff| (deleted, diff)) - }); - if let Ok((deleted, diff)) = res { - all_results.push(diff); - response.push_update(Update::ItemRemoved(deleted)); - } else { - response.push_error(format!("Error selling {}", loot_id)); - } - } - 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)); - } - 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) + let wealth = AsPlayer(conn, id) + .update_wealth(sell_value)?; + Ok((deleted, wealth)) + }) +} + +/// Buys a single item, copied from inventory. +/// Runs inside a transaction +/// +/// # Returns +/// The created entity and the updated Wealth (as a difference from previous value) +pub fn buy_item_from_inventory( + conn: &DbConnection, + id: i32, + item_id: i32, + price_mod: Option, +) -> QueryResult<(Item, Wealth)> { + conn.transaction(|| { + // Find item in inventory + let item = Inventory(conn).find(item_id)?; + let new_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, + }; + AsPlayer(conn, id) + .update_wealth(-sell_price) + .map(|diff| (new_item, diff)) + }) } /// Fetch all existing claims @@ -238,16 +120,6 @@ pub fn resolve_claims(conn: &DbConnection) -> QueryResult<()> { Ok(()) } -/// Sets up a connection pool and returns it. -/// Uses the DATABASE_URL environment variable (must be set) -pub fn create_pool() -> Pool { - let connspec = std::env::var("DATABASE_URL").expect("DATABASE_URL"); - dbg!(&connspec); - let manager = ConnectionManager::::new(connspec); - r2d2::Pool::builder() - .build(manager) - .expect("Failed to create pool.") -} #[cfg(none)] mod tests_old { diff --git a/lootalot_db/src/models/player.rs b/lootalot_db/src/models/player.rs index 2b79383..b0f95c0 100644 --- a/lootalot_db/src/models/player.rs +++ b/lootalot_db/src/models/player.rs @@ -132,6 +132,21 @@ impl Wealth { } } +use std::ops::Add; + +impl Add for Wealth { + type Output = Self; + + fn add(self, other: Self) -> Self { + Wealth { + cp: self.cp + other.cp, + sp: self.sp + other.sp, + gp: self.gp + other.gp, + pp: self.pp + other.pp + } + } +} + /// Representation of a new player record #[derive(Insertable)] #[table_name = "players"] diff --git a/src/api.rs b/src/api.rs new file mode 100644 index 0000000..e3fc9f6 --- /dev/null +++ b/src/api.rs @@ -0,0 +1,166 @@ +use lootalot_db::{self as db, DbConnection, QueryResult}; + +/// Every possible update which can happen during a query +#[derive(Serialize, Deserialize, Debug)] +pub enum Update { + Wealth(db::Wealth), + ItemAdded(db::Item), + ItemRemoved(db::Item), + ClaimAdded(db::Claim), + ClaimRemoved(db::Claim), +} + +/// Every value which can be queried +#[derive(Serialize, Deserialize, Debug)] +pub enum Value { + Item(db::Item), + Claim(db::Claim), + ItemList(Vec), + ClaimList(Vec), + PlayerList(Vec), +} + +/// A generic response for all queries +#[derive(Serialize, Deserialize, Debug, Default)] +pub struct ApiResponse { + /// The value requested, if any + pub value: Option, + /// A text to notify user, if relevant + pub notification: Option, + /// A list of updates, if any + pub updates: Option>, + /// A text describing errors, if any + pub errors: Option, +} + +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 push_error>(&mut self, error: S) { + if let Some(errors) = self.errors.as_mut() { + *errors = format!("{}\n{}", errors, error.into()); + } else { + self.errors = Some(error.into()) + } + } + + fn set_value(&mut self, value: Value) { + self.value = Some(value); + } + + fn notify>(&mut self, text: S) { + self.notification = Some(text.into()); + } +} + +pub enum ApiError { + DieselError(diesel::result::Error), + InvalidAction(String), +} + +/// Every allowed queries on the database +pub enum ApiActions { + FetchPlayers, + FetchInventory, + FetchClaims, + // Player actions + FetchLoot(i32), + UpdateWealth(i32, f32), + BuyItems(i32, Vec<(i32, Option)>), + SellItems(i32, Vec<(i32, Option)>), + ClaimItem(i32, i32), + UnclaimItem(i32, i32), + // Group actions + AddLoot(Vec), +} + +pub enum AdminActions { + AddPlayer(String, f32), + //AddInventoryItem(pub String, pub i32), + ResolveClaims, + //SetClaimsTimeout(pub i32), +} + +pub fn execute( + conn: &DbConnection, + query: ApiActions, +) -> Result { + let mut response = ApiResponse::default(); + match query { + ApiActions::FetchPlayers => { + response.set_value(Value::PlayerList(db::Players(conn).all()?)); + } + ApiActions::FetchInventory => { + response.set_value(Value::ItemList(db::Inventory(conn).all()?)); + } + ApiActions::FetchClaims => { + response.set_value(Value::ClaimList(db::fetch_claims(conn)?)); + } + ApiActions::FetchLoot(id) => { + response.set_value(Value::ItemList(db::LootManager(conn, id).all()?)); + } + ApiActions::UpdateWealth(id, amount) => { + response.push_update(Update::Wealth( + db::AsPlayer(conn, id).update_wealth(amount)?, + )); + } + ApiActions::BuyItems(id, params) => { + let mut cumulated_diff: Vec = Vec::with_capacity(params.len()); + let mut added_items: u16 = 0; + 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)) = db::buy_item_from_inventory(conn, id, item_id, price_mod) { + cumulated_diff.push(diff); + response.push_update(Update::ItemAdded(item)); + added_items += 1; + } else { + response.push_error(format!("Error adding {}", item_id)); + } + } + response.notify(format!("Added {} items", added_items)); + response.push_update(Update::Wealth( + cumulated_diff + .into_iter() + .fold(db::Wealth::from_gp(0.0), |acc, i| acc + i), + )); + } + ApiActions::SellItems(id, params) => { + let mut all_results: Vec = Vec::with_capacity(params.len()); + let mut sold_items: u16 = 0; + for (loot_id, price_mod) in params.into_iter() { + if let Ok((deleted, diff)) = db::sell_item_transaction(conn, id, loot_id, price_mod) { + all_results.push(diff); + response.push_update(Update::ItemRemoved(deleted)); + sold_items += 1; + } else { + response.push_error(format!("Error selling {}", loot_id)); + } + } + response.notify(format!("Sold {} items", sold_items)); + response.push_update(Update::Wealth( + all_results + .into_iter() + .fold(db::Wealth::from_gp(0.0), |acc, i| acc + i), + )); + } + ApiActions::ClaimItem(id, item) => { + response.push_update(Update::ClaimAdded( + db::Claims(conn).add(id, item)?, + )); + } + ApiActions::UnclaimItem(id, item) => { + response.push_update(Update::ClaimRemoved( + db::Claims(conn).remove(id, item)?, + )); + } + // Group actions + ApiActions::AddLoot(items) => {} + } + Ok(response) +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/main.rs b/src/main.rs index 089780e..8978b05 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,9 +2,10 @@ extern crate actix_web; extern crate dotenv; extern crate env_logger; extern crate lootalot_db; -extern crate serde; +#[macro_use] extern crate serde; mod server; +mod api; fn main() { std::env::set_var("RUST_LOG", "actix_web=info"); diff --git a/src/server.rs b/src/server.rs index 1093337..02e8901 100644 --- a/src/server.rs +++ b/src/server.rs @@ -5,6 +5,7 @@ use futures::Future; use std::env; use lootalot_db as db; +use crate::api; type AppPool = web::Data; type PlayerId = web::Path; @@ -14,11 +15,11 @@ type ItemListWithMods = web::Json)>>; /// Wraps call to the database query and convert its result as a async HttpResponse pub fn db_call( pool: AppPool, - query: db::ApiActions, + query: api::ApiActions, ) -> impl Future { let conn = pool.get().unwrap(); - web::block(move || db::execute(&conn, query)).then(|res| match res { + web::block(move || api::execute(&conn, query)).then(|res| match res { Ok(r) => HttpResponse::Ok().json(r), Err(e) => { dbg!(&e); @@ -28,13 +29,14 @@ pub fn db_call( } fn configure_app(config: &mut web::ServiceConfig) { + use api::ApiActions as Q; config.service( web::scope("/api") .service( web::scope("/players") .service( web::resource("/").route( - web::get().to_async(|pool| db_call(pool, db::ApiActions::FetchPlayers)), + web::get().to_async(|pool| db_call(pool, Q::FetchPlayers)), ), //.route(web::post().to_async(endpoints::new_player)) ) // List of players .service( @@ -45,14 +47,14 @@ fn configure_app(config: &mut web::ServiceConfig) { //.route(web::get().to_async(endpoints::player_claims)) .route(web::put().to_async( |pool, (player, data): (PlayerId, ItemId)| { - db_call(pool, db::ApiActions::ClaimItem(*player, *data)) + db_call(pool, Q::ClaimItem(*player, *data)) }, )) .route(web::delete().to_async( |pool, (player, data): (PlayerId, ItemId)| { db_call( pool, - db::ApiActions::UnclaimItem(*player, *data), + Q::UnclaimItem(*player, *data), ) }, )), @@ -64,7 +66,7 @@ fn configure_app(config: &mut web::ServiceConfig) { |pool, (player, data): (PlayerId, web::Json)| { db_call( pool, - db::ApiActions::UpdateWealth(*player, *data), + Q::UpdateWealth(*player, *data), ) }, )), @@ -72,26 +74,26 @@ fn configure_app(config: &mut web::ServiceConfig) { .service( web::resource("/loot") .route(web::get().to_async(|pool, player: PlayerId| { - db_call(pool, db::ApiActions::FetchLoot(*player)) + db_call(pool, Q::FetchLoot(*player)) })) .route(web::put().to_async( move |pool, (player, data): (PlayerId, ItemListWithMods)| { - db_call(pool, db::ApiActions::BuyItems(*player, data.into_inner())) + db_call(pool, Q::BuyItems(*player, data.into_inner())) }, )) .route(web::delete().to_async( move |pool, (player, data): (PlayerId, ItemListWithMods)| { - db_call(pool, db::ApiActions::SellItems(*player, data.into_inner())) + db_call(pool, Q::SellItems(*player, data.into_inner())) }, )), ), ), ) - //.route("/claims", web::get().to_async(endpoints::player_claims)) + .route("/claims", web::get().to_async(|pool| db_call(pool, Q::FetchClaims))) .route( "/items", web::get() - .to_async(move |pool: AppPool| db_call(pool, db::ApiActions::FetchInventory)), + .to_async(move |pool: AppPool| db_call(pool, Q::FetchInventory)), ), ); } From 80585eb1cdc9e41f587ebc9b5d8fbab175fbdd43 Mon Sep 17 00:00:00 2001 From: Artus Date: Thu, 17 Oct 2019 13:01:33 +0200 Subject: [PATCH 15/23] removes useless file --- src/lib.rs | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/lib.rs diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index e69de29..0000000 From ec36403dc33f1ebf4e265774bdf0b15be50a9aea Mon Sep 17 00:00:00 2001 From: Artus Date: Thu, 17 Oct 2019 13:25:07 +0200 Subject: [PATCH 16/23] adds custom serializer for api::Value --- src/api.rs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/api.rs b/src/api.rs index e3fc9f6..38d98d5 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1,7 +1,7 @@ use lootalot_db::{self as db, DbConnection, QueryResult}; /// Every possible update which can happen during a query -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Debug)] pub enum Update { Wealth(db::Wealth), ItemAdded(db::Item), @@ -11,7 +11,7 @@ pub enum Update { } /// Every value which can be queried -#[derive(Serialize, Deserialize, Debug)] +#[derive(Debug)] pub enum Value { Item(db::Item), Claim(db::Claim), @@ -20,8 +20,20 @@ pub enum Value { PlayerList(Vec), } +impl serde::Serialize for Value { + fn serialize(&self, serializer: S) -> Result { + match self { + Self::Item(v) => v.serialize(serializer), + Self::Claim(v) => v.serialize(serializer), + Self::ItemList(v) => v.serialize(serializer), + Self::ClaimList(v) => v.serialize(serializer), + Self::PlayerList(v) => v.serialize(serializer), + } + } +} + /// A generic response for all queries -#[derive(Serialize, Deserialize, Debug, Default)] +#[derive(Serialize, Debug, Default)] pub struct ApiResponse { /// The value requested, if any pub value: Option, From 08397e7b25deb8e0fe8067fb7af39379e0edc4fd Mon Sep 17 00:00:00 2001 From: Artus Date: Fri, 18 Oct 2019 12:42:27 +0200 Subject: [PATCH 17/23] fixes API consumer --- lootalot_front/src/AppStorage.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lootalot_front/src/AppStorage.js b/lootalot_front/src/AppStorage.js index 2e80e5e..a2f0b0f 100644 --- a/lootalot_front/src/AppStorage.js +++ b/lootalot_front/src/AppStorage.js @@ -34,23 +34,23 @@ export const Api = { .then(r => r.json()) }, putClaim (player_id, item_id) { - const payload = { player_id, item_id }; + const payload = item_id; return this.__doFetch("players/" + player_id + "/claims", 'PUT', payload); }, unClaim (player_id, item_id) { - const payload = { player_id, item_id }; + const payload = item_id; return this.__doFetch("players/" + player_id + "/claims", 'DELETE', payload); }, updateWealth (player_id, value_in_gp) { - const payload = { player_id, value_in_gp: Number(value_in_gp) }; + const payload = Number(value_in_gp); return this.__doFetch("players/" + player_id + "/wealth", 'PUT', payload); }, buyItems (player_id, items) { - const payload = { player_id, items }; + const payload = items; return this.__doFetch("players/" + player_id + "/loot", 'PUT', payload); }, sellItems (player_id, items) { - const payload = { player_id, items }; + const payload = items; return this.__doFetch("players/" + player_id + "/loot", 'DELETE', payload); }, newLoot (items) { @@ -84,10 +84,10 @@ export const AppStorage = { ]) .then(data => { const [players, claims, inventory, group_loot] = data; - this.__initPlayerList(players); - this.__initClaimsStore(claims); - Vue.set(this.state, 'group_loot', group_loot); - Vue.set(this.state, 'inventory', inventory); + this.__initPlayerList(players.value); + this.__initClaimsStore(claims.value); + Vue.set(this.state, 'group_loot', group_loot.value); + Vue.set(this.state, 'inventory', inventory.value); }) .then(_ => this.state.initiated = true) .catch(e => { alert(e); this.state.initiated = false }); From 60a6e69f67cc1372a341bfc31c6c82d45d4b5d79 Mon Sep 17 00:00:00 2001 From: Artus Date: Fri, 18 Oct 2019 14:35:06 +0200 Subject: [PATCH 18/23] refactor PlayerView, plans to replace ApiStorage.js by lootalot.js api module --- lootalot_db/src/models/player.rs | 6 +- lootalot_front/src/components/PlayerView.js | 99 +++++++++++---------- lootalot_front/src/lootalot.js | 22 +++++ src/api.rs | 17 ++-- src/server.rs | 4 +- 5 files changed, 93 insertions(+), 55 deletions(-) create mode 100644 lootalot_front/src/lootalot.js diff --git a/lootalot_db/src/models/player.rs b/lootalot_db/src/models/player.rs index b0f95c0..a3c0ad8 100644 --- a/lootalot_db/src/models/player.rs +++ b/lootalot_db/src/models/player.rs @@ -3,7 +3,7 @@ use crate::{DbConnection, QueryResult}; use diesel::prelude::*; /// Representation of a player in database -#[derive(Queryable, Serialize, Deserialize, Debug)] +#[derive(Identifiable, Queryable, Serialize, Deserialize, Debug)] pub struct Player { /// DB Identitier pub id: i32, @@ -29,6 +29,10 @@ impl<'q> Players<'q> { players::table.load(self.0) } + pub fn find(&self, id: i32) -> QueryResult { + players::table.find(id).first(self.0) + } + pub fn add(&self, name: &str, wealth: f32) -> QueryResult { diesel::insert_into(players::table) .values(&NewPlayer::create(name, wealth)) diff --git a/lootalot_front/src/components/PlayerView.js b/lootalot_front/src/components/PlayerView.js index cd76e0f..e7ae3fb 100644 --- a/lootalot_front/src/components/PlayerView.js +++ b/lootalot_front/src/components/PlayerView.js @@ -1,70 +1,73 @@ -import { Api, AppStorage } from '../AppStorage' +import { api } from '../lootalot.js' export default { props: ["id"], data () { return { + player: { + name: "Loading", + id: 0, + cp: '-', sp: '-', gp: '-', pp: '-', + debt: 0, + }, notifications: [], loot: [], }}, methods: { - updateWealth (value) { - AppStorage.updatePlayerWealth(value) - .then(_ => {if (AppStorage.debug) this.notifications.push("Wealth updated")}) - .catch(e => {if (AppStorage.debug) console.error("wealthUpdate Error", e)}) + parseUpdate (update) { + if (update.Wealth) { + var w = update.Wealth; + this.player.cp += w.cp; + this.player.sp += w.sp; + this.player.gp += w.gp; + this.player.pp += w.pp; + } + if (update.ItemAdded) { + var i = update.ItemAdded; + this.loot.push(i); + } + if (update.ItemRemoved) { + var i = update.ItemRemoved; + this.loot.splice(this.loot.indexOf(i), 1); + } + if (update.ClaimAdded || update.ClaimRemoved) { + console.error("cannot handle claim !", update); + } }, - putClaim (itemId) { - AppStorage.putRequest(itemId) - .then(_ => { if (AppStorage.debug) this.notifications.push("Claim put")}) - }, - withdrawClaim (itemId) { - AppStorage.cancelRequest(itemId) - .then(_ => { if (AppStorage.debug) this.notifications.push("Claim withdrawn")}) - - }, - buyItems(items) { - AppStorage.buyItems(items) - .then((items) => { - this.notifications.push(`Bought ${items.length} items`) - this.loot = this.loot.concat(items); + call (resource, method, payload) { + return api.fetch(`players/${this.id}/${resource}`, method, payload) + .then(response => { + if (response.notification) { + this.notifications.push(response.notification) + } + if (response.errors) { + this.notifications.push(response.errors) + } + if (response.updates) { + for (var idx in response.updates) { + this.parseUpdate(response.updates[idx]); + } + } + return response.value; }) }, - sellItems (items) { - AppStorage.sellItems(items) - .then(_ => { - this.notifications.push(`Sold ${items.length} items`) - for (var idx in items) { - var to_remove = items[idx][0]; - this.loot = this.loot.filter((item) => item.id != to_remove); - } - }) - }, - parseLoot (items) { - this.loot = []; - items.map(item => { - this.loot.push(item); - }); - } + updateWealth (value) { this.call("wealth", "PUT", Number(value)) }, + putClaim (itemId) { this.call("claims", "PUT", itemId) }, + withdrawClaim (itemId) { this.call("claims", "DELETE", itemId) }, + buyItems(items) { this.call("loot", "PUT", items) }, + sellItems (items) { this.call("loot", "DELETE", items) }, }, watch: { id: { immediate: true, handler: function(newId) { - Api.fetchLoot(newId).then(this.parseLoot); - } - }, - }, - computed: { - player () { - if (!AppStorage.state.initiated) { - return { name: "Loading", - id: 0, - cp: '-', sp: '-', gp: '-', pp: '-', - debt: 0 }; - } else { - return AppStorage.state.player_list[this.id]; + this.call("", "GET", null) + .then(p => this.player = p) + this.call("loot", "GET", null) + .then(l => this.loot = l) } }, }, + computed: {}, render () { return this.$scopedSlots.default({ player: this.player, diff --git a/lootalot_front/src/lootalot.js b/lootalot_front/src/lootalot.js new file mode 100644 index 0000000..a8049b2 --- /dev/null +++ b/lootalot_front/src/lootalot.js @@ -0,0 +1,22 @@ +import Vue from 'vue' + +const API_BASEURL = "http://localhost:8088/api/" +const API_ENDPOINT = function (tailString) { + return API_BASEURL + tailString; +} + +export const api = { + fetch: function(endpoint, method, payload) { + var config = { + method, + headers: { + 'Content-Type': 'application/json', + }, + }; + if (payload) { + config.body = JSON.stringify(payload); + } + return fetch(API_ENDPOINT(endpoint), config) + .then(r => r.json()); + } +} diff --git a/src/api.rs b/src/api.rs index 38d98d5..0956e2b 100644 --- a/src/api.rs +++ b/src/api.rs @@ -13,6 +13,7 @@ pub enum Update { /// Every value which can be queried #[derive(Debug)] pub enum Value { + Player(db::Player), Item(db::Item), Claim(db::Claim), ItemList(Vec), @@ -23,11 +24,12 @@ pub enum Value { impl serde::Serialize for Value { fn serialize(&self, serializer: S) -> Result { match self { - Self::Item(v) => v.serialize(serializer), - Self::Claim(v) => v.serialize(serializer), - Self::ItemList(v) => v.serialize(serializer), - Self::ClaimList(v) => v.serialize(serializer), - Self::PlayerList(v) => v.serialize(serializer), + Value::Player(v) => v.serialize(serializer), + Value::Item(v) => v.serialize(serializer), + Value::Claim(v) => v.serialize(serializer), + Value::ItemList(v) => v.serialize(serializer), + Value::ClaimList(v) => v.serialize(serializer), + Value::PlayerList(v) => v.serialize(serializer), } } } @@ -82,6 +84,7 @@ pub enum ApiActions { FetchInventory, FetchClaims, // Player actions + FetchPlayer(i32), FetchLoot(i32), UpdateWealth(i32, f32), BuyItems(i32, Vec<(i32, Option)>), @@ -114,6 +117,9 @@ pub fn execute( ApiActions::FetchClaims => { response.set_value(Value::ClaimList(db::fetch_claims(conn)?)); } + ApiActions::FetchPlayer(id) => { + response.set_value(Value::Player(db::Players(conn).find(id)?)); + } ApiActions::FetchLoot(id) => { response.set_value(Value::ItemList(db::LootManager(conn, id).all()?)); } @@ -121,6 +127,7 @@ pub fn execute( response.push_update(Update::Wealth( db::AsPlayer(conn, id).update_wealth(amount)?, )); + response.notify(format!("Money updated !")); } ApiActions::BuyItems(id, params) => { let mut cumulated_diff: Vec = Vec::with_capacity(params.len()); diff --git a/src/server.rs b/src/server.rs index 02e8901..870087f 100644 --- a/src/server.rs +++ b/src/server.rs @@ -41,7 +41,9 @@ fn configure_app(config: &mut web::ServiceConfig) { ) // List of players .service( web::scope("/{player_id}") - //.route(web::get().to_async(...)) // Details of player + .route("/", web::get().to_async(|pool, player: PlayerId| { + db_call(pool, Q::FetchPlayer(*player)) + })) .service( web::resource("/claims") //.route(web::get().to_async(endpoints::player_claims)) From 3fce1dc7d09015167ff192161290f139a4c5aefa Mon Sep 17 00:00:00 2001 From: Artus Date: Fri, 18 Oct 2019 14:48:27 +0200 Subject: [PATCH 19/23] adds nicer notifications to user --- src/api.rs | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/api.rs b/src/api.rs index 0956e2b..87154cf 100644 --- a/src/api.rs +++ b/src/api.rs @@ -133,7 +133,6 @@ pub fn execute( let mut cumulated_diff: Vec = Vec::with_capacity(params.len()); let mut added_items: u16 = 0; 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)) = db::buy_item_from_inventory(conn, id, item_id, price_mod) { cumulated_diff.push(diff); response.push_update(Update::ItemAdded(item)); @@ -142,12 +141,11 @@ pub fn execute( response.push_error(format!("Error adding {}", item_id)); } } - response.notify(format!("Added {} items", added_items)); - response.push_update(Update::Wealth( - cumulated_diff + let total_amount = cumulated_diff .into_iter() - .fold(db::Wealth::from_gp(0.0), |acc, i| acc + i), - )); + .fold(db::Wealth::from_gp(0.0), |acc, i| acc + i); + response.notify(format!("{} objets achetés pour {}po", added_items, total_amount.to_gp())); + response.push_update(Update::Wealth(total_amount)); } ApiActions::SellItems(id, params) => { let mut all_results: Vec = Vec::with_capacity(params.len()); @@ -161,22 +159,23 @@ pub fn execute( response.push_error(format!("Error selling {}", loot_id)); } } - response.notify(format!("Sold {} items", sold_items)); - response.push_update(Update::Wealth( - all_results + let total_amount = all_results .into_iter() - .fold(db::Wealth::from_gp(0.0), |acc, i| acc + i), - )); + .fold(db::Wealth::from_gp(0.0), |acc, i| acc + i); + response.notify(format!("{} objet(s) vendu(s) pour {} po", sold_items, total_amount.to_gp())); + response.push_update(Update::Wealth(total_amount)); } ApiActions::ClaimItem(id, item) => { response.push_update(Update::ClaimAdded( db::Claims(conn).add(id, item)?, )); + response.notify(format!("Pour moi !")); } ApiActions::UnclaimItem(id, item) => { response.push_update(Update::ClaimRemoved( db::Claims(conn).remove(id, item)?, )); + response.notify(format!("Bof! Finalement non.")); } // Group actions ApiActions::AddLoot(items) => {} From 61c9a4e6d44bea32d05e62709548ba2cd14c9894 Mon Sep 17 00:00:00 2001 From: Artus Date: Fri, 18 Oct 2019 16:21:00 +0200 Subject: [PATCH 20/23] moves all api logic inside PlayerView, found weird bug in unpack_gold_value (floating error) --- lootalot_db/src/models/player.rs | 15 ++++++++-- lootalot_front/src/App.vue | 8 ++++-- lootalot_front/src/AppStorage.js | 26 ----------------- lootalot_front/src/components/Chest.vue | 11 +++++++ lootalot_front/src/components/PlayerView.js | 32 +++++++++++++++++---- lootalot_front/src/components/Request.vue | 32 ++++++++++++++------- src/api.rs | 2 +- 7 files changed, 78 insertions(+), 48 deletions(-) diff --git a/lootalot_db/src/models/player.rs b/lootalot_db/src/models/player.rs index a3c0ad8..bb7fbe5 100644 --- a/lootalot_db/src/models/player.rs +++ b/lootalot_db/src/models/player.rs @@ -50,7 +50,11 @@ impl<'q> AsPlayer<'q> { .find(self.1) .select((cp, sp, gp, pp)) .first::(self.0)?; - let updated_wealth = Wealth::from_gp(current_wealth.to_gp() + value_in_gp); + dbg!(¤t_wealth); + dbg!(current_wealth.to_gp()); + dbg!(value_in_gp); + let updated_wealth = Wealth::from_gp(dbg!((current_wealth.to_gp() * 100.0 + value_in_gp * 100.0) / 100.0)); + dbg!(&updated_wealth); // Difference in coins that is sent back let difference = Wealth { cp: updated_wealth.cp - current_wealth.cp, @@ -58,6 +62,7 @@ impl<'q> AsPlayer<'q> { gp: updated_wealth.gp - current_wealth.gp, pp: updated_wealth.pp - current_wealth.pp, }; + diesel::update(players) .filter(id.eq(self.1)) .set(&updated_wealth) @@ -122,8 +127,8 @@ impl Wealth { /// # Examples /// ``` /// # use lootalot_db::models::Wealth; - /// let wealth = Wealth{ pp: 4, gp: 3, sp: 2, cp: 1}; - /// assert_eq!(wealth.to_gp(), 403.21); + /// let wealth = Wealth{ pp: 4, gp: 5, sp: 8, cp: 4}; + /// assert_eq!(wealth.to_gp(), 405.84); /// ``` pub fn to_gp(&self) -> f32 { let i = self.pp * 100 + self.gp; @@ -182,12 +187,16 @@ mod tests { fn test_unpack_gold_values() { use super::unpack_gold_value; let test_values = [ + (0.01, (1, 0, 0, 0)), + (0.1, (0, 1, 0, 0)), (1.0, (0, 0, 1, 0)), (1.23, (3, 2, 1, 0)), (1.03, (3, 0, 1, 0)), (100.23, (3, 2, 0, 1)), (-100.23, (-3, -2, -0, -1)), (10189.23, (3, 2, 89, 101)), + (141805.9, (0, 9, 5, 1418)), + (123141805.9, (0, 9, 5, 1231418)), (-8090.20, (0, -2, -90, -80)), ]; diff --git a/lootalot_front/src/App.vue b/lootalot_front/src/App.vue index 06aa34e..396b84b 100644 --- a/lootalot_front/src/App.vue +++ b/lootalot_front/src/App.vue @@ -1,7 +1,7 @@ - { + for (var idx in r.value) { + var claim = r.value[idx]; + if (!(claim.player_id in this.claims)) { + this.$set(this.claims, claim.player_id, []); + } + this.claims[claim.player_id].push(claim.loot_id); + } + }); + }, methods: { parseUpdate (update) { if (update.Wealth) { @@ -21,16 +34,24 @@ export default { this.player.gp += w.gp; this.player.pp += w.pp; } - if (update.ItemAdded) { + else if (update.ItemAdded) { var i = update.ItemAdded; this.loot.push(i); } - if (update.ItemRemoved) { + else if (update.ItemRemoved) { var i = update.ItemRemoved; this.loot.splice(this.loot.indexOf(i), 1); } - if (update.ClaimAdded || update.ClaimRemoved) { - console.error("cannot handle claim !", update); + else if (update.ClaimAdded) { + var c = update.ClaimAdded; + this.claims[c.player_id].push(c.loot_id); + } + else if (update.ClaimRemoved) { + var c = update.ClaimRemoved; + this.claims[c.player_id].splice( + this.claims[c.player_id].indexOf(c.loot_id), + 1 + ); } }, call (resource, method, payload) { @@ -79,7 +100,8 @@ export default { withdrawClaim: this.withdrawClaim, buyItems: this.buyItems, sellItems: this.sellItems, - } + }, + claims: this.claims, }) } } diff --git a/lootalot_front/src/components/Request.vue b/lootalot_front/src/components/Request.vue index f0ab45e..b1fe6c8 100644 --- a/lootalot_front/src/components/Request.vue +++ b/lootalot_front/src/components/Request.vue @@ -14,7 +14,7 @@ -