//! Loot-a-lot Database module //! //! # Description //! This module wraps all needed database operations. //! It exports a public API for integration with various clients (REST Api, CLI, ...) extern crate dotenv; #[macro_use] extern crate diesel; #[macro_use] extern crate serde_derive; use diesel::prelude::*; use diesel::query_dsl::RunQueryDsl; use diesel::r2d2::{self, ConnectionManager}; pub mod models; mod schema; pub use models::{ claim::{Claim, Claims}, item::{Item, LootManager, Inventory}, player::{Player, Wealth, Players, AsPlayer}, }; /// The connection used pub type DbConnection = SqliteConnection; /// A pool of connections pub type Pool = r2d2::Pool>; /// The result of a query on DB pub type QueryResult = Result; /// 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.") } /// 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, 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.sell_value() as f64; if let Some(modifier) = price_mod { sell_value *= modifier; } 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.value() as f64 * modifier, None => item.value() as f64, }; AsPlayer(conn, id) .update_wealth(-sell_price) .map(|diff| (new_item, diff)) }) } /// Fetch all existing claims pub fn fetch_claims(conn: &DbConnection) -> QueryResult> { schema::claims::table.load::(conn) } /// 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); conn.transaction(move || { for (item, mut claims) in data { if claims.len() > 1 { // TODO: better sorting mechanism :) claims.sort_by(|a,b| a.resolve.cmp(&b.resolve)); } let winner = claims.get(0).expect("Claims should not be empty !"); let player_id = winner.player_id; winner.resolve_claim(conn)?; models::player::AsPlayer(conn, player_id) .update_debt(item.sell_value())?; } Ok(()) }) } /// Split up and share group money among selected players pub fn split_and_share(conn: &DbConnection, amount: i32, players: Vec) -> QueryResult { let share = ( amount / (players.len() + 1) as i32 // +1 share for the group ) as f64; conn.transaction(|| { for p in players.into_iter() { let player = Players(conn).find(p)?; // Take debt into account match share - player.debt as f64 { rest if rest > 0.0 => { AsPlayer(conn, p).update_debt(-player.debt)?; AsPlayer(conn, p).update_wealth(rest)?; AsPlayer(conn, 0).update_wealth(-rest)?; }, _ => { AsPlayer(conn, p).update_debt(-share as i32)?; } } } Ok(Wealth::from_gp(share)) }) } #[cfg(none)] mod tests_old { use super::*; type TestConnection = DbConnection; /// Return a connection to a fresh database (stored in memory) fn test_connection() -> TestConnection { let test_conn = DbConnection::establish(":memory:").unwrap(); diesel_migrations::run_pending_migrations(&test_conn).unwrap(); test_conn } /// When migrations are run, a special player with id 0 and name "Groupe" /// must be created. #[test] fn global_group_is_autocreated() { let conn = test_connection(); let players = DbApi::with_conn(&conn).fetch_players().unwrap(); assert_eq!(players.len(), 1); let group = players.get(0).unwrap(); assert_eq!(group.id, 0); assert_eq!(group.name, "Groupe".to_string()); } /// When a player updates wealth, a difference is returned by API. /// Added to the previous amount of coins, it should equal the updated weath. #[test] fn as_player_updates_wealth() { let conn = test_connection(); DbApi::with_conn(&conn) .as_admin() .add_player("PlayerName", 403.21) .unwrap(); let diff = DbApi::with_conn(&conn) .as_player(1) .update_wealth(-401.21) .ok(); // Check the returned diff assert_eq!(diff, Some((-1, -2, -1, -4))); let diff = diff.unwrap(); let players = DbApi::with_conn(&conn).fetch_players().unwrap(); let player = players.get(1).unwrap(); // Check that we can add old value to return diff to get resulting value assert_eq!( (player.cp, player.sp, player.gp, player.pp), (1 + diff.0, 2 + diff.1, 3 + diff.2, 4 + diff.3) ); } #[test] fn as_admin_add_player() { let conn = test_connection(); let result = DbApi::with_conn(&conn) .as_admin() .add_player("PlayerName", 403.21); assert_eq!(result.is_ok(), true); let players = DbApi::with_conn(&conn).fetch_players().unwrap(); assert_eq!(players.len(), 2); let new_player = players.get(1).unwrap(); assert_eq!(new_player.name, "PlayerName"); assert_eq!( (new_player.cp, new_player.sp, new_player.gp, new_player.pp), (1, 2, 3, 4) ); } #[test] fn as_admin_resolve_claims() { let conn = test_connection(); let claims = DbApi::with_conn(&conn).fetch_claims().unwrap(); 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 ); // 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(); // 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(); let result = DbApi::with_conn(&conn).as_admin().resolve_claims(); assert_eq!(result.is_ok(), true); // 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 ); let player = players.get(i as usize).unwrap(); assert_eq!(player.debt, 20); } } #[test] fn as_player_claim_item() { let conn = test_connection(); DbApi::with_conn(&conn) .as_admin() .add_player("Player", 0.0) .unwrap(); DbApi::with_conn(&conn) .as_admin() .add_loot(vec![("Épée", 25)]) .unwrap(); // Claim an existing item let result = DbApi::with_conn(&conn).as_player(1).claim(1); assert_eq!(result.is_ok(), true); let claims = DbApi::with_conn(&conn).fetch_claims().unwrap(); assert_eq!(claims.len(), 1); let claim = claims.get(0).unwrap(); assert_eq!(claim.player_id, 1); assert_eq!(claim.loot_id, 1); // Claim an inexistant item let result = DbApi::with_conn(&conn).as_player(1).claim(2); assert_eq!(result.is_ok(), false); } #[test] fn as_player_unclaim_item() { let conn = test_connection(); DbApi::with_conn(&conn) .as_admin() .add_player("Player", 0.0) .unwrap(); DbApi::with_conn(&conn) .as_admin() .add_loot(vec![("Épée", 25)]) .unwrap(); // Claim an existing item let result = DbApi::with_conn(&conn).as_player(1).claim(1); assert_eq!(result.is_ok(), true); // Claiming twice is an error let result = DbApi::with_conn(&conn).as_player(1).claim(1); assert_eq!(result.is_ok(), false); // Unclaiming and item let result = DbApi::with_conn(&conn).as_player(1).unclaim(1); assert_eq!(result.is_ok(), true); // Check that not claimed items will not be unclaimed... let result = DbApi::with_conn(&conn).as_player(1).unclaim(1); assert_eq!(result.is_ok(), false); let claims = DbApi::with_conn(&conn).fetch_claims().unwrap(); assert_eq!(claims.len(), 0); } /// All-in-one checks one a simple buy/sell procedure /// /// Checks that player's chest and wealth are updated. /// Checks that items are sold at half their value. /// Checks that a player cannot sell item he does not own. #[test] fn as_player_simple_buy_sell() { let conn = test_connection(); // Adds a sword into inventory { use schema::items::dsl::*; diesel::insert_into(items) .values((name.eq("Sword"), base_price.eq(800))) .execute(&conn) .expect("Could not set up items table"); } DbApi::with_conn(&conn) .as_admin() .add_player("Player", 1000.0) .unwrap(); // Buy an item 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); let loot = chest.get(0).unwrap(); assert_eq!(loot.name, "Sword"); assert_eq!(loot.base_price, 800); let players = DbApi::with_conn(&conn).fetch_players().unwrap(); let player = players.get(1).unwrap(); assert_eq!(player.pp, 2); // A player cannot sell loot from an other's chest 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)]); 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); let players = DbApi::with_conn(&conn).fetch_players().unwrap(); let player = players.get(1).unwrap(); assert_eq!(player.pp, 6); } #[test] fn as_admin_add_loot() { let conn = test_connection(); assert_eq!( 0, DbApi::with_conn(&conn).as_player(0).loot().unwrap().len() ); let loot_to_add = vec![("Cape d'invisibilité", 8000), ("Arc long", 25)]; let result = DbApi::with_conn(&conn) .as_admin() .add_loot(loot_to_add.clone()); assert_eq!(result.is_ok(), true); let looted = DbApi::with_conn(&conn).as_player(0).loot().unwrap(); assert_eq!(looted.len(), 2); // NB: Not a problem now, but this adds constraints of items being // created in the same order. for (added, to_add) in looted.into_iter().zip(loot_to_add) { assert_eq!(added.name, to_add.0); assert_eq!(added.base_price, to_add.1); } } }