From 6101aaa9e91b7e7226aa938f829d68e7e872fdac Mon Sep 17 00:00:00 2001 From: Artus Date: Tue, 15 Oct 2019 14:42:57 +0200 Subject: [PATCH] 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| {