From a013aa7ecf07fb9ad5c988ebe1c95bb3f740d58b Mon Sep 17 00:00:00 2001 From: Martin Indra Date: Tue, 13 Dec 2022 17:16:15 +0100 Subject: [PATCH] de_server: Implement leave game endpoint Relates to #255. --- crates/server/src/games/db.rs | 90 +++++++++++++++++++++++++++- crates/server/src/games/endpoints.rs | 20 ++++++- crates/server/src/games/init.sql | 1 + crates/server/src/games/model.rs | 2 + docs/content/lobby/openapi.yaml | 16 +++++ 5 files changed, 125 insertions(+), 4 deletions(-) diff --git a/crates/server/src/games/db.rs b/crates/server/src/games/db.rs index a66eaff04..32f605227 100644 --- a/crates/server/src/games/db.rs +++ b/crates/server/src/games/db.rs @@ -72,10 +72,12 @@ impl Games { ); result.map_err(CreationError::Database)?; + let mut author = true; for username in game.players() { - Self::add_player_inner(&mut transaction, username, game_config.name()) + Self::add_player_inner(&mut transaction, author, username, game_config.name()) .await .map_err(CreationError::AdditionError)?; + author = false; } transaction @@ -88,13 +90,15 @@ impl Games { async fn add_player_inner<'c, E>( executor: E, + author: bool, username: &str, game: &str, ) -> Result<(), AdditionError> where E: SqliteExecutor<'c>, { - let result = query("INSERT INTO players (username, game) VALUES (?, ?);") + let result = query("INSERT INTO players (author, username, game) VALUES (?, ?, ?);") + .bind(author) .bind(username) .bind(game) .execute(executor) @@ -109,6 +113,80 @@ impl Games { Ok(()) } + + pub(super) async fn remove_player( + &self, + username: &str, + game: &str, + ) -> Result { + let mut transaction = self.pool.begin().await.map_err(RemovalError::Database)?; + + let mut rows = query("SELECT author FROM players WHERE username = ? AND game = ?;") + .bind(username) + .bind(game) + .fetch(self.pool); + + let action = match rows.try_next().await.map_err(RemovalError::Database)? { + Some(row) => { + let author: bool = row.try_get("author").map_err(RemovalError::Database)?; + if author { + RemovalAction::Abandoned + } else { + RemovalAction::Removed + } + } + None => return Err(RemovalError::NotInTheGame), + }; + + match action { + RemovalAction::Abandoned => { + query("DELETE FROM games WHERE name = ?;") + .bind(game) + .execute(&mut transaction) + .await + .map_err(RemovalError::Database)?; + } + RemovalAction::Removed => { + Self::remove_player_inner(&mut transaction, username, game)?; + } + } + + transaction + .commit() + .await + .map_err(CreationError::Database)?; + + Ok(action) + } + + async fn remove_player_inner<'c, E>( + executor: E, + username: &str, + game: &str, + ) -> Result<(), RemovalError> + where + E: SqliteExecutor<'c>, + { + let query_result = query("DELETE FROM players WHERE username = ? AND game = ?;") + .bind(username) + .bind(game) + .execute(executor) + .await + .map_err(RemovalError::Database)?; + + let rows_affected = query_result.rows_affected; + assert!(rows_affected <= 1); + if rows_affected == 0 { + return Err(RemovalError::NotInTheGame); + } + + Ok(()) + } +} + +pub(super) enum RemovalAction { + Abandoned, + Removed, } #[derive(Error, Debug)] @@ -133,6 +211,14 @@ pub(super) enum AdditionError { Other(#[from] anyhow::Error), } +#[derive(Error, Debug)] +pub(super) enum RemovalError { + #[error("User is not in the game")] + NotInTheGame, + #[error("A database error encountered")] + Database(#[source] sqlx::Error), +} + impl TryFrom for GameConfig { type Error = anyhow::Error; diff --git a/crates/server/src/games/endpoints.rs b/crates/server/src/games/endpoints.rs index 960d989ea..e22fa918c 100644 --- a/crates/server/src/games/endpoints.rs +++ b/crates/server/src/games/endpoints.rs @@ -1,4 +1,4 @@ -use actix_web::{get, post, web, HttpResponse, Responder}; +use actix_web::{get, post, put, web, HttpResponse, Responder}; use log::{error, warn}; use super::{ @@ -9,7 +9,12 @@ use crate::auth::Claims; /// Registers all authentication endpoints. pub(super) fn configure(cfg: &mut web::ServiceConfig) { - cfg.service(web::scope("/games").service(create).service(list)); + cfg.service( + web::scope("/games") + .service(create) + .service(list) + .service(leave), + ); } #[post("/")] @@ -52,3 +57,14 @@ async fn list(games: web::Data) -> impl Responder { } } } + +#[put("/{name}/leave")] +async fn leave( + claims: web::ReqData, + games: web::Data, + path: web::Path, +) -> impl Responder { + let name = path.into_inner(); + + // TODO +} diff --git a/crates/server/src/games/init.sql b/crates/server/src/games/init.sql index f2df59ec7..7d1c20504 100644 --- a/crates/server/src/games/init.sql +++ b/crates/server/src/games/init.sql @@ -6,6 +6,7 @@ CREATE TABLE IF NOT EXISTS games ( CREATE TABLE IF NOT EXISTS players ( ordinal INTEGER PRIMARY KEY AUTOINCREMENT, + author BOOLEAN NOT NULL, username CHARACTER({username_len}) NOT NULL UNIQUE, game CHARACTER({game_name_len}) NOT NULL, diff --git a/crates/server/src/games/model.rs b/crates/server/src/games/model.rs index 43897a372..63d43568c 100644 --- a/crates/server/src/games/model.rs +++ b/crates/server/src/games/model.rs @@ -25,6 +25,8 @@ impl Game { &self.config } + /// Returns a slice with usernames of the game players. The first user is + /// game author. pub(super) fn players(&self) -> &[String] { self.players.as_slice() } diff --git a/docs/content/lobby/openapi.yaml b/docs/content/lobby/openapi.yaml index 882361966..78a9cf1ae 100644 --- a/docs/content/lobby/openapi.yaml +++ b/docs/content/lobby/openapi.yaml @@ -100,6 +100,22 @@ paths: "409": description: A different game with the same name already exists. + /a/games/{name}/leave: + put: + summary: Leave or abandon a game. + description: >- + Leave the game. The client needs to be part of the game. The game is + abandoned / canceled when the author leaves the game. + security: + - bearerAuth: [] + parameters: + - name: name + in: path + required: true + responses: + "200": + description: The user successfully left the game. + components: securitySchemes: bearerAuth: