mirror of
https://github.com/Steffo99/distributed-arcade.git
synced 2024-11-23 16:44:24 +00:00
Complete functionality
This commit is contained in:
parent
b88f66e1e1
commit
58e975e7ee
6 changed files with 152 additions and 16 deletions
|
@ -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:
|
|
@ -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))
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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((
|
||||
|
|
Loading…
Reference in a new issue