Skip to content

Commit

Permalink
de_server: Implement leave game endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
Indy2222 committed Dec 13, 2022
1 parent f56193c commit b04eb9f
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 5 deletions.
91 changes: 89 additions & 2 deletions crates/server/src/games/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -109,6 +113,81 @@ impl Games {

Ok(())
}

/// Removes a player from a game. Deletes the game if the player was the
/// game author.
pub(super) async fn remove_player(
&self,
username: &str,
game: &str,
) -> Result<(), RemovalError> {
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).await?;
}
}

transaction.commit().await.map_err(RemovalError::Database)?;
Ok(())
}

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(())
}
}

/// Action taken during removal of a player from a game.
enum RemovalAction {
/// The game was abandoned and all players removed from the game.
Abandoned,
/// The player left the game without any further action taken.
Removed,
}

#[derive(Error, Debug)]
Expand All @@ -133,6 +212,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<SqliteRow> for GameConfig {
type Error = anyhow::Error;

Expand Down
33 changes: 30 additions & 3 deletions crates/server/src/games/endpoints.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
use actix_web::{get, post, web, HttpResponse, Responder};
use actix_web::{get, post, put, web, HttpResponse, Responder};
use log::{error, warn};

use super::{
db::{AdditionError, CreationError, Games},
db::{AdditionError, CreationError, Games, RemovalError},
model::{Game, GameConfig},
};
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("/")]
Expand Down Expand Up @@ -52,3 +57,25 @@ async fn list(games: web::Data<Games>) -> impl Responder {
}
}
}

#[put("/{name}/leave")]
async fn leave(
claims: web::ReqData<Claims>,
games: web::Data<Games>,
path: web::Path<String>,
) -> impl Responder {
let name = path.into_inner();

match games.remove_player(claims.username(), name.as_str()).await {
Ok(_) => HttpResponse::Ok().finish(),

Err(RemovalError::NotInTheGame) => {
warn!("Game leaving error: the user is not in the game.");
HttpResponse::Forbidden().json("The user is not in the game.")
}
Err(error) => {
error!("Error while removing a player from a game: {:?}", error);
HttpResponse::InternalServerError().finish()
}
}
}
1 change: 1 addition & 0 deletions crates/server/src/games/init.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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,

Expand Down
2 changes: 2 additions & 0 deletions crates/server/src/games/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
20 changes: 20 additions & 0 deletions docs/content/lobby/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,26 @@ 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
schema:
type: string
responses:
"200":
description: The user successfully left the game.
"403":
description: The user is not part of the game.

components:
securitySchemes:
bearerAuth:
Expand Down

0 comments on commit b04eb9f

Please sign in to comment.