diff --git a/.gitignore b/.gitignore index 9fcc6d7..75a8337 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ node_modules fontawesome +package-lock.json Cargo.lock **/*.sqlite3 diff --git a/lootalot_db/src/lib.rs b/lootalot_db/src/lib.rs index 1fe5f89..c4b149d 100644 --- a/lootalot_db/src/lib.rs +++ b/lootalot_db/src/lib.rs @@ -1,3 +1,8 @@ +//! Loot-a-lot Database module +//! +//! # Description +//! This module wraps all needed database operations. +//! It exports a public API for integration with various clients (REST Api, CLI, ...) extern crate dotenv; #[macro_use] extern crate diesel; @@ -86,25 +91,19 @@ impl<'q> DbApi<'q> { Self(conn) } /// Fetch the list of all players - /// - /// This method consumes the DbApi object. pub fn fetch_players(self) -> QueryResult> { Ok(schema::players::table.load::(self.0)?) } /// Fetch the inventory of items - /// - /// Consumes the DbApi instance pub fn fetch_inventory(self) -> QueryResult> { Ok(schema::items::table.load::(self.0)?) } - + /// Fetch all existing claims pub fn fetch_claims(self) -> QueryResult> { Ok(schema::claims::table.load::(self.0)?) } /// Wrapper for acting as a specific player /// - /// The DbApi is moved inside a new AsPlayer object. - /// /// # Usage /// ``` /// # use lootalot_db::{DbConnection, DbApi}; @@ -124,7 +123,7 @@ impl<'q> DbApi<'q> { } } -/// A wrapper for interactions of players with the database +/// A wrapper for interactions of players with the database. /// Possible actions are exposed as methods pub struct AsPlayer<'q> { id: i32, @@ -157,10 +156,10 @@ impl<'q> AsPlayer<'q> { let current_wealth = players .find(self.id) .select((cp, sp, gp, pp)) - .first::(self.conn)?; + .first::(self.conn)?; // TODO: improve this // should be move inside a WealthUpdate method - let update = models::WealthUpdate::from_gp(current_wealth.to_gp() + value_in_gp); + let update = 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(), update.as_tuple()); let diff = (new.0 - old.0, new.1 - old.1, new.2 - old.2, new.3 - old.3); @@ -187,12 +186,9 @@ impl<'q> AsPlayer<'q> { if !exists { return Ok(ActionStatus::nop()); }; - let request = models::NewClaim { - player_id: self.id, - loot_id: item, - }; + let claim = models::claim::NewClaim::new(self.id, item); diesel::insert_into(schema::claims::table) - .values(&request) + .values(&claim) .execute(self.conn) .map(|r| match r { 1 => ActionStatus::ok(), @@ -221,7 +217,7 @@ pub struct AsAdmin<'q>(&'q DbConnection); impl<'q> AsAdmin<'q> { pub fn add_player(self, name: String, start_wealth: f32) -> ActionResult<()> { diesel::insert_into(schema::players::table) - .values(&models::NewPlayer::create(&name, start_wealth)) + .values(&models::player::NewPlayer::create(&name, start_wealth)) .execute(self.0) .map(|r| match r { 1 => ActionStatus::ok(), @@ -256,4 +252,39 @@ pub fn create_pool() -> Pool { } #[cfg(test)] -mod tests {} +mod tests { + use super::*; + type TestConnection = DbConnection; + + fn test_connection() -> TestConnection { + let test_conn = DbConnection::establish(":memory:").unwrap(); + diesel_migrations::run_pending_migrations(&test_conn).unwrap(); + test_conn + } + + #[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()); + } + + #[test] + fn test_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)); + } +} + + diff --git a/lootalot_db/src/models/claim.rs b/lootalot_db/src/models/claim.rs index dfed683..b9a2f7e 100644 --- a/lootalot_db/src/models/claim.rs +++ b/lootalot_db/src/models/claim.rs @@ -1,19 +1,32 @@ use crate::models::item::Loot; use crate::schema::claims; -use diesel::prelude::*; +/// A Claim is a request by a single player on an item from group chest. #[derive(Identifiable, Queryable, Associations, Serialize, Debug)] #[belongs_to(Loot)] pub struct Claim { - id: i32, - player_id: i32, - loot_id: i32, - resolve: i32, + /// DB Identifier + pub id: i32, + /// ID that references the player making this claim + pub player_id: i32, + /// ID that references the loot claimed + pub loot_id: i32, + /// WIP: How bad the player wants this item + pub resolve: i32, } #[derive(Insertable, Debug)] #[table_name = "claims"] -pub struct NewClaim { - pub player_id: i32, - pub loot_id: i32, +pub(crate) struct NewClaim { + player_id: i32, + loot_id: i32, +} + +impl NewClaim { + pub(crate) 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 6d9d323..9b53b6f 100644 --- a/lootalot_db/src/models/item.rs +++ b/lootalot_db/src/models/item.rs @@ -1,5 +1,4 @@ -use crate::schema::{items, looted}; -use crate::DbConnection; +use crate::schema::{looted}; use diesel::dsl::{exists, Eq, Filter, Find, Select}; use diesel::expression::exists::Exists; use diesel::prelude::*; @@ -60,7 +59,7 @@ type ItemDesc<'a> = (&'a str, i32); /// to the id of buying player otherwise. #[derive(Insertable)] #[table_name = "looted"] -struct NewLoot<'a> { +pub(crate) struct NewLoot<'a> { name: &'a str, base_price: i32, owner_id: i32, @@ -68,7 +67,7 @@ struct NewLoot<'a> { impl<'a> NewLoot<'a> { /// A new loot going to the group (loot procedure) - fn to_group(desc: ItemDesc<'a>) -> Self { + pub(crate) fn to_group(desc: ItemDesc<'a>) -> Self { Self { name: desc.0, base_price: desc.1, @@ -77,7 +76,7 @@ impl<'a> NewLoot<'a> { } /// A new loot going to a specific player (buy procedure) - fn to_player(player: i32, desc: ItemDesc<'a>) -> Self { + pub(crate) fn to_player(player: i32, desc: ItemDesc<'a>) -> Self { Self { name: desc.0, base_price: desc.1, diff --git a/lootalot_db/src/models/mod.rs b/lootalot_db/src/models/mod.rs index e238d48..32afa0f 100644 --- a/lootalot_db/src/models/mod.rs +++ b/lootalot_db/src/models/mod.rs @@ -1,8 +1,8 @@ -mod claim; -mod item; -mod player; +pub(super) mod claim; +pub(super) mod item; +pub(super) mod player; -pub use claim::{Claim, NewClaim}; +pub use claim::Claim; pub use item::Item; pub(crate) use item::Loot; -pub use player::{NewPlayer, Player, WealthUpdate}; +pub use player::{Player, Wealth}; diff --git a/lootalot_db/src/models/player.rs b/lootalot_db/src/models/player.rs index aa6e493..8ed7b20 100644 --- a/lootalot_db/src/models/player.rs +++ b/lootalot_db/src/models/player.rs @@ -1,17 +1,23 @@ use crate::schema::players; -use crate::DbConnection; -use diesel::prelude::*; /// Representation of a player in database #[derive(Debug, Queryable, Serialize)] pub struct Player { - id: i32, - name: String, - debt: i32, - cp: i32, - sp: i32, - gp: i32, - pp: i32, + /// DB Identitier + pub id: i32, + /// Full name of the character + pub name: String, + /// Amount of gold coins owed to the group. + /// Taking a looted items will increase the debt by it's sell value + pub debt: i32, + /// Count of copper pieces + pub cp: i32, + /// Count of silver pieces + pub sp: i32, + /// Count of gold pieces + pub gp: i32, + /// Count of platinum pieces + pub pp: i32, } /// Unpack a floating value in gold pieces to integer @@ -32,21 +38,28 @@ fn unpack_gold_value(gold: f32) -> (i32, i32, i32, i32) { (cp, sp, gp, pp) } -/// Represent an update on a player's wealth +/// State of a player's wealth /// -/// The values held here are the amount of pieces to add or -/// substract to player wealth. +/// Values are held as individual pieces counts. +/// Allows conversion from and to a floating amount of gold pieces. #[derive(Queryable, AsChangeset, Debug)] #[table_name = "players"] -pub struct WealthUpdate { - cp: i32, - sp: i32, - gp: i32, - pp: i32, +pub struct Wealth { + pub cp: i32, + pub sp: i32, + pub gp: i32, + pub pp: i32, } -impl WealthUpdate { +impl Wealth { /// Unpack individual pieces counts from gold value + /// + /// # Examples + /// ``` + /// # use lootalot_db::models::Wealth; + /// let wealth = Wealth::from_gp(403.21); + /// assert_eq!(wealth.as_tuple(), (1, 2, 3, 4)); + /// ``` pub fn from_gp(gp: f32) -> Self { let (cp, sp, gp, pp) = unpack_gold_value(gp); Self { cp, sp, gp, pp } @@ -55,8 +68,8 @@ impl WealthUpdate { /// /// # Examples /// ``` - /// # use lootalot_db::models::WealthUpdate; - /// let wealth = WealthUpdate::from_gp(403.21); + /// # use lootalot_db::models::Wealth; + /// let wealth = Wealth{ pp: 4, gp: 3, sp: 2, cp: 1}; /// assert_eq!(wealth.to_gp(), 403.21); /// ``` pub fn to_gp(&self) -> f32 { @@ -73,7 +86,7 @@ impl WealthUpdate { /// Representation of a new player record #[derive(Insertable)] #[table_name = "players"] -pub struct NewPlayer<'a> { +pub(crate) struct NewPlayer<'a> { name: &'a str, cp: i32, sp: i32, @@ -82,22 +95,17 @@ pub struct NewPlayer<'a> { } impl<'a> NewPlayer<'a> { - pub fn create(name: &'a str, wealth: f32) -> Self { - let wealth = WealthUpdate::from_gp(wealth); + pub(crate) fn create(name: &'a str, wealth_in_gp: f32) -> Self { + let (cp, sp, gp, pp) = Wealth::from_gp(wealth_in_gp).as_tuple(); Self { name, - cp: wealth.cp, - sp: wealth.sp, - gp: wealth.gp, - pp: wealth.pp, + cp, sp, gp, pp, } } } #[cfg(test)] mod tests { - use super::*; - use crate::tests; #[test] fn test_unpack_gold_values() { diff --git a/src/server.rs b/src/server.rs index d51bfcf..f6a760c 100644 --- a/src/server.rs +++ b/src/server.rs @@ -63,53 +63,48 @@ pub(crate) fn serve() -> std::io::Result<()> { .allowed_methods(vec!["GET", "POST"]) .max_age(3600), ) - .route( - "/api/players", - web::get() - .to_async(move |pool: AppPool| db_call(pool, move |api| api.fetch_players())), - ) - .route( - "/api/claims", - web::get() - .to_async(move |pool: AppPool| db_call(pool, move |api| api.fetch_claims())), - ) - .route( - "/api/{player_id}/update-wealth/{amount}", - web::get().to_async(move |pool: AppPool, data: web::Path<(i32, f32)>| { - db_call(pool, move |api| api.as_player(data.0).update_wealth(data.1)) - }), - ) - .route( - "/api/{player_id}/loot", - web::get().to_async(move |pool: AppPool, player_id: web::Path| { - db_call(pool, move |api| api.as_player(*player_id).loot()) - }), - ) - .route( - "/api/{player_id}/claim/{item_id}", - web::get().to_async(move |pool: AppPool, data: web::Path<(i32, i32)>| { - db_call(pool, move |api| api.as_player(data.0).claim(data.1)) - }), - ) - .route( - "/api/{player_id}/unclaim/{item_id}", - web::get().to_async(move |pool: AppPool, data: web::Path<(i32, i32)>| { - db_call(pool, move |api| api.as_player(data.0).unclaim(data.1)) - }), - ) - .route( - "/api/admin/resolve-claims", - web::get().to_async(move |pool: AppPool| { - db_call(pool, move |api| api.as_admin().resolve_claims()) - }), - ) - .route( - "/api/admin/add-player/{name}/{wealth}", - web::get().to_async(move |pool: AppPool, data: web::Path<(String, f32)>| { - db_call(pool, move |api| { - api.as_admin().add_player(data.0.clone(), data.1) - }) - }), + .service( + web::scope("/api") + .route("/players", web::get().to_async(move |pool: AppPool| db_call(pool, move |api| api.fetch_players()))) + .route("/claims", web::get().to_async(move |pool: AppPool| db_call(pool, move |api| api.fetch_claims()))) + .route( + "/{player_id}/update-wealth/{amount}", + web::get().to_async(move |pool: AppPool, data: web::Path<(i32, f32)>| { + db_call(pool, move |api| api.as_player(data.0).update_wealth(data.1)) + }), + ) + .route( + "/{player_id}/loot", + web::get().to_async(move |pool: AppPool, player_id: web::Path| { + db_call(pool, move |api| api.as_player(*player_id).loot()) + }), + ) + .route( + "/{player_id}/claim/{item_id}", + web::get().to_async(move |pool: AppPool, data: web::Path<(i32, i32)>| { + db_call(pool, move |api| api.as_player(data.0).claim(data.1)) + }), + ) + .route( + "/{player_id}/unclaim/{item_id}", + web::get().to_async(move |pool: AppPool, data: web::Path<(i32, i32)>| { + db_call(pool, move |api| api.as_player(data.0).unclaim(data.1)) + }), + ) + .route( + "/admin/resolve-claims", + web::get().to_async(move |pool: AppPool| { + db_call(pool, move |api| api.as_admin().resolve_claims()) + }), + ) + .route( + "/admin/add-player/{name}/{wealth}", + web::get().to_async(move |pool: AppPool, data: web::Path<(String, f32)>| { + db_call(pool, move |api| { + api.as_admin().add_player(data.0.clone(), data.1) + }) + }), + ) ) .service(fs::Files::new("/", www_root.clone()).index_file("index.html")) })