1
Fork 0
mirror of https://github.com/Steffo99/distributed-arcade.git synced 2024-10-16 06:27:30 +00:00

Complete functionality

This commit is contained in:
Steffo 2022-11-12 17:38:01 +01:00
parent b88f66e1e1
commit 58e975e7ee
Signed by: steffo
GPG key ID: 6965406171929D01
6 changed files with 152 additions and 16 deletions

View file

@ -28,7 +28,8 @@ paths:
content:
application/json:
schema:
type: "null"
nullable: true
example: null
patch:
summary: "Verify that everything is working as expected"
tags: ["Home"]
@ -38,7 +39,8 @@ paths:
content:
application/json:
schema:
type: "null"
nullable: true
example: null
500:
description: "Did not receive `PONG` from redis"
content:
@ -52,6 +54,44 @@ paths:
$ref: "#/components/responses/RedisConnFailed"
/board/:
get:
summary: "Get the scores of a board"
tags: ["Board"]
parameters:
- $ref: "#/components/parameters/board"
- $ref: "#/components/parameters/offset"
- $ref: "#/components/parameters/size"
responses:
200:
description: "Scores retrieved successfully"
content:
application/json:
schema:
type: object
properties:
offset:
type: integer
description: "The offset to pass to get the next page, or 0 if there are no more results."
example: 1
scores:
type: array
items:
type: object
description: "A score submitted by an user."
properties:
name:
type: string
description: "The name of the user who submitted the score."
example: "Steffo"
score:
type: number
description: "The submitted score."
example: 1234.56
502:
$ref: "#/components/responses/RedisCmdFailed"
504:
$ref: "#/components/responses/RedisConnFailed"
post:
summary: "Create a new board"
tags: ["Board"]
@ -187,6 +227,20 @@ components:
in: query
schema:
type: string
offset:
name: "offset"
description: "The offset to start returning results from."
in: query
schema:
type: integer
size:
name: "size"
description: "How many results to return."
in: query
schema:
type: integer
minimum: 0
maximum: 500
responses:
RedisCmdFailed:

View file

@ -23,6 +23,7 @@ async fn main() {
let webapp = axum::Router::new()
.route("/", get(routes::home::route_home_get))
.route("/", patch(routes::home::route_home_patch))
.route("/board/", get(routes::board::route_board_get))
.route("/board/", post(routes::board::route_board_post))
.route("/score/", get(routes::score::route_score_get))
.route("/score/", put(routes::score::route_score_put))

View file

@ -27,7 +27,8 @@ pub(crate) use req_success;
/// The execution of a command in Redis failed.
pub(crate) fn redis_cmd_failed() -> RequestTuple {
pub(crate) fn redis_cmd_failed(err: redis::RedisError) -> RequestTuple {
log::error!("{err:#?}");
(
StatusCode::BAD_GATEWAY,
req_error!("Could not execute Redis command")

View file

@ -1,7 +1,7 @@
//! Module defining routes for `/board/`.
use axum::http::StatusCode;
use axum::extract::{Extension, Json};
use axum::extract::{Extension, Json, Query};
use redis::AsyncCommands;
use serde::Serialize;
use serde::Deserialize;
@ -13,7 +13,7 @@ use crate::utils::kebab::Skewer;
use crate::utils::token::SecureToken;
/// Expected input data for [`POST /board/`](route_board_post).
/// Expected body for [`POST /board/`](route_board_post).
#[derive(Clone, Debug, Serialize, Deserialize)]
pub(crate) struct RouteBoardBody {
/// The name of the board to create.
@ -23,18 +23,97 @@ pub(crate) struct RouteBoardBody {
}
/// Expected query params for [`GET /board/`](route_board_get).
#[derive(Clone, Debug, Serialize, Deserialize)]
pub(crate) struct RouteBoardQuery {
/// The name of the board to access.
pub(crate) board: String,
/// The offset to start returning scores from.
pub(crate) offset: usize,
/// How many scores to return.
pub(crate) size: usize,
}
/// Expected response for [`GET /board/`](route_board_get).
#[derive(Clone, Debug, Serialize, Deserialize)]
pub(crate) struct RouteBoardResponse {
/// The offset of the next page.
pub(crate) offset: usize,
/// The scores of the current page.
pub(crate) scores: Vec<ScoreObject>,
}
/// A score set by a player, as a serializable struct.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub(crate) struct ScoreObject {
/// The name of the player who set the score.
pub(crate) name: String,
/// The score that the player set.
pub(crate) score: f64,
}
impl From<(String, f64)> for ScoreObject {
fn from(t: (String, f64)) -> Self {
ScoreObject {name: t.0, score: t.1}
}
}
impl From<(usize, Vec<(String, f64)>)> for RouteBoardResponse {
fn from(t: (usize, Vec<(String, f64)>)) -> Self {
RouteBoardResponse {
offset: t.0,
scores: t.1.into_iter().map(From::from).collect()
}
}
}
/// Ensure that there is nothing stored at a certain Redis key.
async fn ensure_key_is_empty(rconn: &mut redis::aio::Connection, key: &str) -> Result<(), outcome::RequestTuple> {
log::trace!("Ensuring that the Redis key `{key}` does not contain anything...");
redis::cmd("TYPE").arg(&key)
.query_async::<redis::aio::Connection, String>(rconn).await
.map_err(|_| outcome::redis_cmd_failed())?
.map_err(outcome::redis_cmd_failed)?
.eq("none")
.then_some(())
.ok_or((StatusCode::CONFLICT, outcome::req_error!("Board already exists")))
}
/// Handler for `GET /board/`.
pub(crate) async fn route_board_get(
// Request query
Query(RouteBoardQuery {board, offset, size}): Query<RouteBoardQuery>,
// Redis client
Extension(rclient): Extension<redis::Client>,
) -> outcome::RequestResult {
let board = board.to_kebab_lowercase();
log::trace!("Ensuring the size is within limits...");
if size > 500 {
return Err((
StatusCode::BAD_REQUEST,
outcome::req_error!("Cannot request more than 500 scores at a time")
))
}
log::trace!("Determining the Redis key name...");
let scores_key = format!("board:{board}:scores");
let mut rconn = rclient.get_connection_or_504().await?;
log::trace!("Retrieving scores from {board}...");
let result: RouteBoardResponse = redis::cmd("ZSCAN").arg(&scores_key).arg(offset).arg("COUNT").arg(&size)
.query_async::<redis::aio::Connection, (usize, Vec<(String, f64)>)>(&mut rconn).await
.map_err(outcome::redis_cmd_failed)?
.into();
Ok((StatusCode::OK, outcome::req_success!(result)))
}
/// Handler for `POST /board/`.
///
/// Creates a new board, storing the details on [Redis].
@ -69,19 +148,19 @@ pub(crate) async fn route_board_post(
log::trace!("Starting Redis transaction...");
redis::cmd("MULTI").query_async(&mut rconn).await
.map_err(|_| outcome::redis_cmd_failed())?;
.map_err(outcome::redis_cmd_failed)?;
log::trace!("Setting board order...");
rconn.set(&order_key, Into::<&str>::into(order)).await
.map_err(|_| outcome::redis_cmd_failed())?;
.map_err(outcome::redis_cmd_failed)?;
log::trace!("Setting board token...");
rconn.set(&token_key, &token.0).await
.map_err(|_| outcome::redis_cmd_failed())?;
.map_err(outcome::redis_cmd_failed)?;
log::trace!("Executing Redis transaction...");
redis::cmd("EXEC").query_async(&mut rconn).await
.map_err(|_| outcome::redis_cmd_failed())?;
.map_err(outcome::redis_cmd_failed)?;
Ok((
StatusCode::CREATED,

View file

@ -26,7 +26,7 @@ pub(crate) async fn route_home_patch(
log::trace!("Sending PING and expecting PONG...");
redis::cmd("PING")
.query_async::<redis::aio::Connection, String>(&mut rconn).await
.map_err(|_| outcome::redis_cmd_failed())?
.map_err(outcome::redis_cmd_failed)?
.eq("PONG")
.then(outcome::success_null)
.ok_or_else(outcome::redis_unexpected_behaviour)

View file

@ -14,6 +14,7 @@ use crate::utils::sorting::SortingOrder;
/// Query parameters for `/score/` routes.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub(crate) struct RouteScoreQuery {
/// The board to access.
pub board: String,
@ -39,7 +40,7 @@ pub(crate) async fn route_score_get(
log::trace!("Getting score...");
let score = rconn.zscore(&scores_key, &player).await
.map_err(|_| outcome::redis_cmd_failed())?;
.map_err(outcome::redis_cmd_failed)?;
log::trace!("Score is: {score:?}");
Ok((
@ -73,7 +74,7 @@ pub(crate) async fn route_score_put(
log::trace!("Checking if the token exists and matches...");
let btoken = rconn.get::<&str, String>(&token_key).await
.map_err(|_| outcome::redis_cmd_failed())?;
.map_err(outcome::redis_cmd_failed)?;
if btoken.is_empty() {
log::trace!("Token is not set, board does not exist...");
@ -87,7 +88,7 @@ pub(crate) async fn route_score_put(
log::trace!("Determining sorting order...");
let order = rconn.get::<&str, String>(&order_key).await
.map_err(|_| outcome::redis_cmd_failed())?;
.map_err(outcome::redis_cmd_failed)?;
let order = SortingOrder::try_from(order.as_str())
.map_err(|_| outcome::redis_unexpected_behaviour())?;
log::trace!("Sorting order is: {order:?}");
@ -95,11 +96,11 @@ pub(crate) async fn route_score_put(
log::trace!("Inserting score: {score:?}");
let changed = redis::cmd("ZADD").arg(&scores_key).arg(order.zadd_mode()).arg("CH").arg(&score).arg(&player)
.query_async::<redis::aio::Connection, i32>(&mut rconn).await
.map_err(|_| outcome::redis_cmd_failed())?;
.map_err(outcome::redis_cmd_failed)?;
log::trace!("Getting the new score...");
let nscore = rconn.zscore::<&str, &str, f64>(&scores_key, &player).await
.map_err(|_| outcome::redis_cmd_failed())?;
.map_err(outcome::redis_cmd_failed)?;
log::trace!("Received score: {nscore:?}");
Ok((