mirror of
https://github.com/Steffo99/distributed-arcade.git
synced 2024-11-24 00:54:26 +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:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: "null"
|
nullable: true
|
||||||
|
example: null
|
||||||
patch:
|
patch:
|
||||||
summary: "Verify that everything is working as expected"
|
summary: "Verify that everything is working as expected"
|
||||||
tags: ["Home"]
|
tags: ["Home"]
|
||||||
|
@ -38,7 +39,8 @@ paths:
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: "null"
|
nullable: true
|
||||||
|
example: null
|
||||||
500:
|
500:
|
||||||
description: "Did not receive `PONG` from redis"
|
description: "Did not receive `PONG` from redis"
|
||||||
content:
|
content:
|
||||||
|
@ -52,6 +54,44 @@ paths:
|
||||||
$ref: "#/components/responses/RedisConnFailed"
|
$ref: "#/components/responses/RedisConnFailed"
|
||||||
|
|
||||||
/board/:
|
/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:
|
post:
|
||||||
summary: "Create a new board"
|
summary: "Create a new board"
|
||||||
tags: ["Board"]
|
tags: ["Board"]
|
||||||
|
@ -187,6 +227,20 @@ components:
|
||||||
in: query
|
in: query
|
||||||
schema:
|
schema:
|
||||||
type: string
|
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:
|
responses:
|
||||||
RedisCmdFailed:
|
RedisCmdFailed:
|
|
@ -23,6 +23,7 @@ async fn main() {
|
||||||
let webapp = axum::Router::new()
|
let webapp = axum::Router::new()
|
||||||
.route("/", get(routes::home::route_home_get))
|
.route("/", get(routes::home::route_home_get))
|
||||||
.route("/", patch(routes::home::route_home_patch))
|
.route("/", patch(routes::home::route_home_patch))
|
||||||
|
.route("/board/", get(routes::board::route_board_get))
|
||||||
.route("/board/", post(routes::board::route_board_post))
|
.route("/board/", post(routes::board::route_board_post))
|
||||||
.route("/score/", get(routes::score::route_score_get))
|
.route("/score/", get(routes::score::route_score_get))
|
||||||
.route("/score/", put(routes::score::route_score_put))
|
.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.
|
/// 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,
|
StatusCode::BAD_GATEWAY,
|
||||||
req_error!("Could not execute Redis command")
|
req_error!("Could not execute Redis command")
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
//! Module defining routes for `/board/`.
|
//! Module defining routes for `/board/`.
|
||||||
|
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
use axum::extract::{Extension, Json};
|
use axum::extract::{Extension, Json, Query};
|
||||||
use redis::AsyncCommands;
|
use redis::AsyncCommands;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
@ -13,7 +13,7 @@ use crate::utils::kebab::Skewer;
|
||||||
use crate::utils::token::SecureToken;
|
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)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub(crate) struct RouteBoardBody {
|
pub(crate) struct RouteBoardBody {
|
||||||
/// The name of the board to create.
|
/// 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.
|
/// 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> {
|
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...");
|
log::trace!("Ensuring that the Redis key `{key}` does not contain anything...");
|
||||||
|
|
||||||
redis::cmd("TYPE").arg(&key)
|
redis::cmd("TYPE").arg(&key)
|
||||||
.query_async::<redis::aio::Connection, String>(rconn).await
|
.query_async::<redis::aio::Connection, String>(rconn).await
|
||||||
.map_err(|_| outcome::redis_cmd_failed())?
|
.map_err(outcome::redis_cmd_failed)?
|
||||||
.eq("none")
|
.eq("none")
|
||||||
.then_some(())
|
.then_some(())
|
||||||
.ok_or((StatusCode::CONFLICT, outcome::req_error!("Board already exists")))
|
.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/`.
|
/// Handler for `POST /board/`.
|
||||||
///
|
///
|
||||||
/// Creates a new board, storing the details on [Redis].
|
/// 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...");
|
log::trace!("Starting Redis transaction...");
|
||||||
redis::cmd("MULTI").query_async(&mut rconn).await
|
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...");
|
log::trace!("Setting board order...");
|
||||||
rconn.set(&order_key, Into::<&str>::into(order)).await
|
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...");
|
log::trace!("Setting board token...");
|
||||||
rconn.set(&token_key, &token.0).await
|
rconn.set(&token_key, &token.0).await
|
||||||
.map_err(|_| outcome::redis_cmd_failed())?;
|
.map_err(outcome::redis_cmd_failed)?;
|
||||||
|
|
||||||
log::trace!("Executing Redis transaction...");
|
log::trace!("Executing Redis transaction...");
|
||||||
redis::cmd("EXEC").query_async(&mut rconn).await
|
redis::cmd("EXEC").query_async(&mut rconn).await
|
||||||
.map_err(|_| outcome::redis_cmd_failed())?;
|
.map_err(outcome::redis_cmd_failed)?;
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
StatusCode::CREATED,
|
StatusCode::CREATED,
|
||||||
|
|
|
@ -26,7 +26,7 @@ pub(crate) async fn route_home_patch(
|
||||||
log::trace!("Sending PING and expecting PONG...");
|
log::trace!("Sending PING and expecting PONG...");
|
||||||
redis::cmd("PING")
|
redis::cmd("PING")
|
||||||
.query_async::<redis::aio::Connection, String>(&mut rconn).await
|
.query_async::<redis::aio::Connection, String>(&mut rconn).await
|
||||||
.map_err(|_| outcome::redis_cmd_failed())?
|
.map_err(outcome::redis_cmd_failed)?
|
||||||
.eq("PONG")
|
.eq("PONG")
|
||||||
.then(outcome::success_null)
|
.then(outcome::success_null)
|
||||||
.ok_or_else(outcome::redis_unexpected_behaviour)
|
.ok_or_else(outcome::redis_unexpected_behaviour)
|
||||||
|
|
|
@ -14,6 +14,7 @@ use crate::utils::sorting::SortingOrder;
|
||||||
|
|
||||||
|
|
||||||
/// Query parameters for `/score/` routes.
|
/// Query parameters for `/score/` routes.
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub(crate) struct RouteScoreQuery {
|
pub(crate) struct RouteScoreQuery {
|
||||||
/// The board to access.
|
/// The board to access.
|
||||||
pub board: String,
|
pub board: String,
|
||||||
|
@ -39,7 +40,7 @@ pub(crate) async fn route_score_get(
|
||||||
|
|
||||||
log::trace!("Getting score...");
|
log::trace!("Getting score...");
|
||||||
let score = rconn.zscore(&scores_key, &player).await
|
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:?}");
|
log::trace!("Score is: {score:?}");
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
|
@ -73,7 +74,7 @@ pub(crate) async fn route_score_put(
|
||||||
|
|
||||||
log::trace!("Checking if the token exists and matches...");
|
log::trace!("Checking if the token exists and matches...");
|
||||||
let btoken = rconn.get::<&str, String>(&token_key).await
|
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() {
|
if btoken.is_empty() {
|
||||||
log::trace!("Token is not set, board does not exist...");
|
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...");
|
log::trace!("Determining sorting order...");
|
||||||
let order = rconn.get::<&str, String>(&order_key).await
|
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())
|
let order = SortingOrder::try_from(order.as_str())
|
||||||
.map_err(|_| outcome::redis_unexpected_behaviour())?;
|
.map_err(|_| outcome::redis_unexpected_behaviour())?;
|
||||||
log::trace!("Sorting order is: {order:?}");
|
log::trace!("Sorting order is: {order:?}");
|
||||||
|
@ -95,11 +96,11 @@ pub(crate) async fn route_score_put(
|
||||||
log::trace!("Inserting score: {score:?}");
|
log::trace!("Inserting score: {score:?}");
|
||||||
let changed = redis::cmd("ZADD").arg(&scores_key).arg(order.zadd_mode()).arg("CH").arg(&score).arg(&player)
|
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
|
.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...");
|
log::trace!("Getting the new score...");
|
||||||
let nscore = rconn.zscore::<&str, &str, f64>(&scores_key, &player).await
|
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:?}");
|
log::trace!("Received score: {nscore:?}");
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
|
|
Loading…
Reference in a new issue