331 lines
11 KiB
Rust
331 lines
11 KiB
Rust
//! 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;
|
|
|
|
/// The connection used
|
|
pub type DbConnection = SqliteConnection;
|
|
/// A pool of connections
|
|
pub type Pool = r2d2::Pool<ConnectionManager<DbConnection>>;
|
|
/// The result of a query on DB
|
|
pub type QueryResult<T> = Result<T, diesel::result::Error>;
|
|
/// The result of an action provided by DbApi
|
|
pub type ActionResult<R> = QueryResult<ActionStatus<R>>;
|
|
|
|
/// Return status of an API Action
|
|
#[derive(Serialize, Debug)]
|
|
pub struct ActionStatus<R: serde::Serialize> {
|
|
/// Has the action made changes ?
|
|
pub executed: bool,
|
|
/// Response payload
|
|
pub response: R,
|
|
}
|
|
|
|
impl ActionStatus<()> {
|
|
fn was_updated(updated_lines: usize) -> Self {
|
|
match updated_lines {
|
|
1 => Self::ok(),
|
|
_ => Self::nop(),
|
|
}
|
|
}
|
|
fn ok() -> ActionStatus<()> {
|
|
Self {
|
|
executed: true,
|
|
response: (),
|
|
}
|
|
}
|
|
fn nop() -> ActionStatus<()> {
|
|
Self {
|
|
executed: false,
|
|
response: (),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A wrapper providing an API over the database
|
|
/// It offers a convenient way to deal with connection
|
|
///
|
|
/// # Todo list
|
|
/// ```text
|
|
/// struct DbApi<'q>(&'q DbConnection);
|
|
/// ::new() -> DbApi<'q> (Db finds a connection by itself, usefull for cli)
|
|
/// ::with_conn(conn) -> DbApi<'q> (uses a user-defined connection)
|
|
/// v .fetch_players()
|
|
/// v .fetch_inventory()
|
|
/// v .fetch_claims()
|
|
/// v .as_player(player_id) -> AsPlayer<'q>
|
|
/// v .loot() -> List of items owned (Vec<Item>)
|
|
/// v .claim(loot_id) -> Success status (bool)
|
|
/// v .unclaim(loot_id) -> Success status (bool)
|
|
/// x .sell(loot_id) -> Success status (bool, earned)
|
|
/// x .buy(item_desc) -> Success status (bool, cost)
|
|
/// v .update_wealth(value_in_gold) -> Success status (bool, new_wealth)
|
|
/// v .as_admin()
|
|
/// x .add_loot(identifier, [items_desc]) -> Success status
|
|
/// x .sell_loot([players], [excluded_item_ids]) -> Success status (bool, player_share)
|
|
/// x .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<Vec<models::Player>> {
|
|
Ok(schema::players::table.load::<models::Player>(self.0)?)
|
|
}
|
|
/// Fetch the inventory of items
|
|
pub fn fetch_inventory(self) -> QueryResult<Vec<models::Item>> {
|
|
Ok(schema::items::table.load::<models::Item>(self.0)?)
|
|
}
|
|
/// Fetch all existing claims
|
|
pub fn fetch_claims(self) -> QueryResult<Vec<models::Claim>> {
|
|
Ok(schema::claims::table.load::<models::Claim>(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<Vec<models::Item>> {
|
|
Ok(models::Item::owned_by(self.id).load(self.conn)?)
|
|
}
|
|
/// 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<Option<(i32, i32, i32, i32)>> {
|
|
use schema::players::dsl::*;
|
|
let current_wealth = players
|
|
.find(self.id)
|
|
.select((cp, sp, gp, pp))
|
|
.first::<models::Wealth>(self.conn)?;
|
|
// TODO: improve this
|
|
// 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 => ActionStatus {
|
|
executed: true,
|
|
response: Some(diff),
|
|
},
|
|
_ => ActionStatus {
|
|
executed: false,
|
|
response: None,
|
|
},
|
|
})
|
|
}
|
|
/// Put a claim on a specific item
|
|
pub fn claim(self, item: i32) -> ActionResult<()> {
|
|
// TODO: check that looted item exists
|
|
let exists: bool = diesel::select(models::Loot::exists(item)).get_result(self.conn)?;
|
|
if !exists {
|
|
return Ok(ActionStatus::nop());
|
|
};
|
|
let claim = models::claim::NewClaim::new(self.id, item);
|
|
diesel::insert_into(schema::claims::table)
|
|
.values(&claim)
|
|
.execute(self.conn)
|
|
.map(ActionStatus::was_updated)
|
|
}
|
|
/// 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)
|
|
.map(ActionStatus::was_updated)
|
|
}
|
|
}
|
|
|
|
/// 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: String, start_wealth: f32) -> ActionResult<()> {
|
|
diesel::insert_into(schema::players::table)
|
|
.values(&models::player::NewPlayer::create(&name, start_wealth))
|
|
.execute(self.0)
|
|
.map(ActionStatus::was_updated)
|
|
}
|
|
|
|
/// Resolve all pending claims and dispatch claimed items.
|
|
///
|
|
/// When a player gets an item, it's debt is increased by this item sell value
|
|
pub fn resolve_claims(self) -> ActionResult<()> {
|
|
// Fetch all claims, grouped by items.
|
|
let loot = models::Loot::owned_by(0).load(self.0)?;
|
|
let claims = schema::claims::table
|
|
.load::<models::Claim>(self.0)?
|
|
.grouped_by(&loot);
|
|
// For each claimed item
|
|
let data = loot.into_iter().zip(claims).collect::<Vec<_>>();
|
|
dbg!(data);
|
|
// If mutiples claims -> find highest resolve, give to this player
|
|
// If only one claim -> give to claiming
|
|
Ok(ActionStatus::nop())
|
|
}
|
|
}
|
|
|
|
/// Sets up a connection pool and returns it.
|
|
/// Uses the DATABASE_URL environment variable (must be set)
|
|
pub fn create_pool() -> Pool {
|
|
let connspec = std::env::var("DATABASE_URL").expect("DATABASE_URL");
|
|
dbg!(&connspec);
|
|
let manager = ConnectionManager::<DbConnection>::new(connspec);
|
|
r2d2::Pool::builder()
|
|
.build(manager)
|
|
.expect("Failed to create pool.")
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
type TestConnection = DbConnection;
|
|
|
|
/// Return a connection to a fresh database (stored in memory)
|
|
fn test_connection() -> TestConnection {
|
|
let test_conn = DbConnection::establish(":memory:").unwrap();
|
|
diesel_migrations::run_pending_migrations(&test_conn).unwrap();
|
|
test_conn
|
|
}
|
|
|
|
/// When migrations are run, a special player with id 0 and name "Groupe"
|
|
/// must be created.
|
|
#[test]
|
|
fn test_group_is_autocreated() {
|
|
let conn = test_connection();
|
|
let players = DbApi::with_conn(&conn).fetch_players().unwrap();
|
|
assert_eq!(players.len(), 1);
|
|
let group = players.get(0).unwrap();
|
|
assert_eq!(group.id, 0);
|
|
assert_eq!(group.name, "Groupe".to_string());
|
|
}
|
|
|
|
/// When a player updates wealth, a difference is returned by API.
|
|
/// Added to the previous amount of coins, it should equal the updated weath.
|
|
#[test]
|
|
fn test_player_updates_wealth() {
|
|
let conn = test_connection();
|
|
DbApi::with_conn(&conn).as_admin()
|
|
.add_player("PlayerName".to_string(), 403.21)
|
|
.unwrap();
|
|
let diff = DbApi::with_conn(&conn).as_player(1)
|
|
.update_wealth(-401.21).unwrap()
|
|
.response.unwrap();
|
|
// Check the returned diff
|
|
assert_eq!(diff, (-1, -2, -1, -4));
|
|
let players = DbApi::with_conn(&conn).fetch_players().unwrap();
|
|
let player = players.get(1).unwrap();
|
|
// Check that we can add old value to return diff to get resulting value
|
|
assert_eq!(
|
|
(player.cp, player.sp, player.gp, player.pp),
|
|
(1 + diff.0, 2 + diff.1, 3 + diff.2, 4 + diff.3)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_admin_add_player() {
|
|
let conn = test_connection();
|
|
let result = DbApi::with_conn(&conn).as_admin()
|
|
.add_player("PlayerName".to_string(), 403.21)
|
|
.unwrap();
|
|
assert_eq!(result.executed, true);
|
|
let players = DbApi::with_conn(&conn).fetch_players().unwrap();
|
|
assert_eq!(players.len(), 2);
|
|
let new_player = players.get(1).unwrap();
|
|
assert_eq!(new_player.name, "PlayerName");
|
|
assert_eq!((new_player.cp, new_player.sp, new_player.gp, new_player.pp), (1, 2, 3, 4));
|
|
}
|
|
|
|
#[test]
|
|
fn test_admin_resolve_claims() {
|
|
|
|
}
|
|
|
|
#[test]
|
|
fn test_player_claim_item() {
|
|
|
|
}
|
|
|
|
#[test]
|
|
fn test_player_unclaim_item() {
|
|
|
|
}
|
|
}
|
|
|
|
|