mirror of
https://github.com/Steffo99/distributed-arcade.git
synced 2024-11-21 15:44:26 +00:00
Some more final improvements
This commit is contained in:
parent
1a34bb1699
commit
67c94fdf50
11 changed files with 252 additions and 103 deletions
3
Cargo.lock
generated
3
Cargo.lock
generated
|
@ -143,7 +143,7 @@ checksum = "338089f42c427b86394a5ee60ff321da23a5c89c9d89514c829687b26359fcff"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "distributed_arcade"
|
name = "distributed_arcade"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
|
@ -157,6 +157,7 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tower-http",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "distributed_arcade"
|
name = "distributed_arcade"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
@ -17,4 +17,5 @@ log = { version = "0.4.17" }
|
||||||
pretty_env_logger = { version = "0.4.0" }
|
pretty_env_logger = { version = "0.4.0" }
|
||||||
rand = { version = "0.8.5" }
|
rand = { version = "0.8.5" }
|
||||||
regex = { version = "1.7.0" }
|
regex = { version = "1.7.0" }
|
||||||
async-trait = { version = "0.1.58" }
|
async-trait = { version = "0.1.58" }
|
||||||
|
tower-http = { version = "0.3.4", features=["cors"] }
|
||||||
|
|
7
README.md
Normal file
7
README.md
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
# Distributed Arcade
|
||||||
|
|
||||||
|
Fast and simple scoreboard service for games
|
||||||
|
|
||||||
|
\[ [**Swagger UI**]() \]
|
||||||
|
|
||||||
|
Developed for [`Steffo99/unimore-bda-2`](https://github.com/Steffo99/unimore-bda-2)
|
51
docs/examples.http
Normal file
51
docs/examples.http
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
### Verify that the webserver is working
|
||||||
|
GET http://localhost:30000/
|
||||||
|
|
||||||
|
### Verify that everything is working
|
||||||
|
POST http://localhost:30000/
|
||||||
|
|
||||||
|
### Create a board
|
||||||
|
POST http://localhost:30000/board/
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "example",
|
||||||
|
"order": "Descending"
|
||||||
|
}
|
||||||
|
|
||||||
|
### Set a score on the board
|
||||||
|
PUT http://localhost:30000/score/?board=example&player=steffo
|
||||||
|
Content-Type: application/json
|
||||||
|
Authorization: Bearer adz313TlarO98B0P
|
||||||
|
|
||||||
|
1234.56
|
||||||
|
|
||||||
|
### Set another score on the board
|
||||||
|
PUT http://localhost:30000/score/?board=example&player=offets
|
||||||
|
Content-Type: application/json
|
||||||
|
Authorization: Bearer adz313TlarO98B0P
|
||||||
|
|
||||||
|
2412.25
|
||||||
|
|
||||||
|
### Improve the first score
|
||||||
|
PUT http://localhost:30000/score/?board=example&player=steffo
|
||||||
|
Content-Type: application/json
|
||||||
|
Authorization: Bearer adz313TlarO98B0P
|
||||||
|
|
||||||
|
6666.66
|
||||||
|
|
||||||
|
### Set a third score
|
||||||
|
PUT http://localhost:30000/score/?board=example&player=oooooo
|
||||||
|
Content-Type: application/json
|
||||||
|
Authorization: Bearer adz313TlarO98B0P
|
||||||
|
|
||||||
|
3333.33
|
||||||
|
|
||||||
|
### Get the leaderboards
|
||||||
|
GET http://localhost:30000/board/?board=example&offset=0&size=10
|
||||||
|
|
||||||
|
### Get a player's rank
|
||||||
|
GET http://localhost:30000/score/?board=example&player=steffo
|
||||||
|
|
||||||
|
### Get another player's rank
|
||||||
|
GET http://localhost:30000/score/?board=example&player=offets
|
3
docs/index.html
Normal file
3
docs/index.html
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
</html>
|
|
@ -2,16 +2,32 @@ openapi: 3.0.3
|
||||||
|
|
||||||
info:
|
info:
|
||||||
title: "Distributed Arcade"
|
title: "Distributed Arcade"
|
||||||
|
version: 0.2.0
|
||||||
description: |-
|
description: |-
|
||||||
A super-fast high score gatherer using Rust and Redis.
|
A super-fast high score gatherer using Rust and Redis.
|
||||||
version: 0.1.0
|
|
||||||
|
It allows the creation of _boards_, [sorted sets](https://redis.io/docs/data-types/sorted-sets/) that can be used to track the scores of players in videogames.
|
||||||
|
|
||||||
|
It is written to be extremely fast and scalable: it should be able to receive bursts of many requests.
|
||||||
|
|
||||||
|
Errors can be distinguished from successful requests via HTTP status codes >=400.
|
||||||
|
contact:
|
||||||
|
name: "Stefano Pigozzi"
|
||||||
|
url: "https://www.steffo.eu"
|
||||||
|
email: "me@steffo.eu"
|
||||||
|
license:
|
||||||
|
name: "GNU Affero General Public License v3.0 or later"
|
||||||
|
url: https://github.com/Steffo99/distributed-arcade/blob/main/LICENSE.txt"
|
||||||
|
|
||||||
servers:
|
servers:
|
||||||
|
- url: "https://arcade.steffo.eu"
|
||||||
|
description: "Experimental production server"
|
||||||
- url: "http://127.0.0.1:30000"
|
- url: "http://127.0.0.1:30000"
|
||||||
|
description: "Local development server"
|
||||||
|
|
||||||
tags:
|
tags:
|
||||||
- name: "Home"
|
- name: "Home"
|
||||||
description: "Miscellaneous routes"
|
description: "Launch checklist"
|
||||||
- name: "Board"
|
- name: "Board"
|
||||||
description: "About boards"
|
description: "About boards"
|
||||||
- name: "Score"
|
- name: "Score"
|
||||||
|
@ -20,27 +36,23 @@ tags:
|
||||||
paths:
|
paths:
|
||||||
/:
|
/:
|
||||||
get:
|
get:
|
||||||
|
operationId: "getHome"
|
||||||
summary: "Verify that the web server is working as expected"
|
summary: "Verify that the web server is working as expected"
|
||||||
|
description: |-
|
||||||
|
This method simply echoes back a response, and can be used to verify that the API web server is working.
|
||||||
tags: ["Home"]
|
tags: ["Home"]
|
||||||
responses:
|
responses:
|
||||||
200:
|
204:
|
||||||
description: "Working as expected"
|
description: "Working as expected"
|
||||||
content:
|
post:
|
||||||
application/json:
|
operationId: "postHome"
|
||||||
schema:
|
|
||||||
nullable: true
|
|
||||||
example: null
|
|
||||||
patch:
|
|
||||||
summary: "Verify that everything is working as expected"
|
summary: "Verify that everything is working as expected"
|
||||||
|
description: |-
|
||||||
|
This method is like `GET /`, but it also `PING`s the Redis server to ensure it is working and configured properly.
|
||||||
tags: ["Home"]
|
tags: ["Home"]
|
||||||
responses:
|
responses:
|
||||||
200:
|
204:
|
||||||
description: "Working as expected"
|
description: "Working as expected"
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
nullable: true
|
|
||||||
example: null
|
|
||||||
500:
|
500:
|
||||||
description: "Did not receive `PONG` from redis"
|
description: "Did not receive `PONG` from redis"
|
||||||
content:
|
content:
|
||||||
|
@ -55,7 +67,13 @@ paths:
|
||||||
|
|
||||||
/board/:
|
/board/:
|
||||||
get:
|
get:
|
||||||
|
operationId: "getBoard"
|
||||||
summary: "Get the scores of a board"
|
summary: "Get the scores of a board"
|
||||||
|
description: |-
|
||||||
|
This method requests a page of scores from a board using the [`ZSCAN`](https://redis.io/commands/zscan/) Redis command.
|
||||||
|
|
||||||
|
An offset must be specified to start returning scores from a certain index.
|
||||||
|
The number of responses to return must be specified as well.
|
||||||
tags: ["Board"]
|
tags: ["Board"]
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: "#/components/parameters/board"
|
- $ref: "#/components/parameters/board"
|
||||||
|
@ -67,33 +85,37 @@ paths:
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: object
|
type: array
|
||||||
properties:
|
items:
|
||||||
offset:
|
type: object
|
||||||
type: integer
|
description: "A score submitted by an user."
|
||||||
description: "The offset to pass to get the next page, or 0 if there are no more results."
|
properties:
|
||||||
example: 1
|
name:
|
||||||
scores:
|
type: string
|
||||||
type: array
|
description: "The name of the user who submitted the score."
|
||||||
items:
|
example: "Steffo"
|
||||||
type: object
|
score:
|
||||||
description: "A score submitted by an user."
|
type: number
|
||||||
properties:
|
description: "The submitted score."
|
||||||
name:
|
example: 1234.56
|
||||||
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:
|
502:
|
||||||
$ref: "#/components/responses/RedisCmdFailed"
|
$ref: "#/components/responses/RedisCmdFailed"
|
||||||
504:
|
504:
|
||||||
$ref: "#/components/responses/RedisConnFailed"
|
$ref: "#/components/responses/RedisConnFailed"
|
||||||
|
|
||||||
post:
|
post:
|
||||||
|
operationId: "postBoard"
|
||||||
summary: "Create a new board"
|
summary: "Create a new board"
|
||||||
|
description: |-
|
||||||
|
This method creates a new board.
|
||||||
|
|
||||||
|
It returns its _score submission token_ (`XBoardToken` in this spec), which is required to submit new scores to the board.
|
||||||
|
|
||||||
|
Boards can use two different orders:
|
||||||
|
|
||||||
|
- using the `Ascending` order, lower scores are better ranked than higher scores, like in racing games or golf;
|
||||||
|
- using the `Descending` order, higher scores are better ranked than lower scores, like in arcade games or athletics.
|
||||||
|
|
||||||
|
**WARNING: Once created, a board cannot be edited or deleted, and its token will not be accessible any longer!**
|
||||||
tags: ["Board"]
|
tags: ["Board"]
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
|
@ -104,15 +126,15 @@ paths:
|
||||||
properties:
|
properties:
|
||||||
name:
|
name:
|
||||||
type: string
|
type: string
|
||||||
description: "The name of the board to create."
|
description: "The name of the board to create. It will be converted to kebab-case."
|
||||||
example: "gravityfusion"
|
example: "gravityfusion"
|
||||||
order:
|
order:
|
||||||
type: string
|
type: string
|
||||||
example: "DSC"
|
example: "Descending"
|
||||||
description: "The ordering of the board, either ascending or descending."
|
description: "The ordering of the board, either ascending or descending."
|
||||||
enum:
|
enum:
|
||||||
- "ASC"
|
- "Ascending"
|
||||||
- "DSC"
|
- "Descending"
|
||||||
responses:
|
responses:
|
||||||
201:
|
201:
|
||||||
description: "Board created successfully"
|
description: "Board created successfully"
|
||||||
|
@ -121,6 +143,15 @@ paths:
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
example: "W4SbhbJ3tnGaIM1S"
|
example: "W4SbhbJ3tnGaIM1S"
|
||||||
|
links:
|
||||||
|
getScore:
|
||||||
|
operationId: "getScore"
|
||||||
|
parameters:
|
||||||
|
board: "$response.body/name"
|
||||||
|
putScore:
|
||||||
|
operationId: "putScore"
|
||||||
|
parameters:
|
||||||
|
board: "$response.body/name"
|
||||||
409:
|
409:
|
||||||
description: "Board already exists"
|
description: "Board already exists"
|
||||||
content:
|
content:
|
||||||
|
@ -142,7 +173,10 @@ paths:
|
||||||
|
|
||||||
/score/:
|
/score/:
|
||||||
get:
|
get:
|
||||||
|
operationId: "getScore"
|
||||||
summary: "Get a score from a board"
|
summary: "Get a score from a board"
|
||||||
|
description: |-
|
||||||
|
Retrieve the score and the position that the given user has on the leaderboard.
|
||||||
tags: ["Score"]
|
tags: ["Score"]
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: "#/components/parameters/board"
|
- $ref: "#/components/parameters/board"
|
||||||
|
@ -153,15 +187,23 @@ paths:
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: number
|
type: object
|
||||||
description: "The score of the specified player."
|
properties:
|
||||||
example: 1234.56
|
score:
|
||||||
|
type: number
|
||||||
|
description: "The score of the specified player."
|
||||||
|
example: 1234.56
|
||||||
|
rank:
|
||||||
|
type: integer
|
||||||
|
description: "The zero-indexed rank of the specified player. (You may probably want to add `1` before displaying it to an user.)"
|
||||||
|
example: 0
|
||||||
502:
|
502:
|
||||||
$ref: "#/components/responses/RedisCmdFailed"
|
$ref: "#/components/responses/RedisCmdFailed"
|
||||||
504:
|
504:
|
||||||
$ref: "#/components/responses/RedisConnFailed"
|
$ref: "#/components/responses/RedisConnFailed"
|
||||||
|
|
||||||
put:
|
put:
|
||||||
|
operationId: "putScore"
|
||||||
summary: "Submit a score to a board"
|
summary: "Submit a score to a board"
|
||||||
tags: ["Score"]
|
tags: ["Score"]
|
||||||
parameters:
|
parameters:
|
||||||
|
@ -183,17 +225,31 @@ paths:
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: number
|
type: object
|
||||||
description: "The previous score."
|
properties:
|
||||||
example: 2468.12
|
score:
|
||||||
|
type: number
|
||||||
|
description: "The score of the specified player."
|
||||||
|
example: 1234.56
|
||||||
|
rank:
|
||||||
|
type: integer
|
||||||
|
description: "The zero-indexed rank of the specified player. (You may probably want to add `1` before displaying it to an user.)"
|
||||||
|
example: 0
|
||||||
201:
|
201:
|
||||||
description: "Score submitted and updated"
|
description: "Score submitted and updated"
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: number
|
type: object
|
||||||
description: "The new score."
|
properties:
|
||||||
example: 1234.56
|
score:
|
||||||
|
type: number
|
||||||
|
description: "The score of the specified player."
|
||||||
|
example: 2468.13
|
||||||
|
rank:
|
||||||
|
type: integer
|
||||||
|
description: "The zero-indexed rank of the specified player. (You may probably want to add `1` before displaying it to an user.)"
|
||||||
|
example: 0
|
||||||
401:
|
401:
|
||||||
description: "Missing, invalid or malformed Authorization header"
|
description: "Missing, invalid or malformed Authorization header"
|
||||||
content:
|
content:
|
||||||
|
@ -210,9 +266,9 @@ paths:
|
||||||
components:
|
components:
|
||||||
securitySchemes:
|
securitySchemes:
|
||||||
XBoardToken:
|
XBoardToken:
|
||||||
type: apiKey
|
type: http
|
||||||
in: header
|
scheme: "Bearer"
|
||||||
name: "Authorization"
|
bearerFormat: "gVsuzIxgVfRx4RNl"
|
||||||
|
|
||||||
parameters:
|
parameters:
|
||||||
board:
|
board:
|
|
@ -5,7 +5,7 @@ mod routes;
|
||||||
mod shortcuts;
|
mod shortcuts;
|
||||||
|
|
||||||
|
|
||||||
use axum::routing::{get, post, put, patch};
|
use axum::routing::{get, post, put};
|
||||||
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
|
@ -22,12 +22,15 @@ 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("/", post(routes::home::route_home_post))
|
||||||
.route("/board/", get(routes::board::route_board_get))
|
.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))
|
||||||
.layer(axum::Extension(rclient));
|
.layer(axum::Extension(rclient))
|
||||||
|
.layer(tower_http::cors::CorsLayer::new()
|
||||||
|
.allow_origin("*")
|
||||||
|
);
|
||||||
|
|
||||||
log::info!("Starting Axum server...");
|
log::info!("Starting Axum server...");
|
||||||
|
|
||||||
|
|
|
@ -42,11 +42,3 @@ pub(crate) fn redis_unexpected_behaviour() -> RequestTuple {
|
||||||
req_error!("Redis gave an unexpected response")
|
req_error!("Redis gave an unexpected response")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The request succeeded, and there's no data to be returned.
|
|
||||||
pub(crate) fn success_null() -> RequestTuple {
|
|
||||||
(
|
|
||||||
StatusCode::OK,
|
|
||||||
req_success!(null)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
|
@ -35,16 +35,6 @@ pub(crate) struct RouteBoardQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// 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.
|
/// A score set by a player, as a serializable struct.
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub(crate) struct ScoreObject {
|
pub(crate) struct ScoreObject {
|
||||||
|
@ -56,19 +46,14 @@ pub(crate) struct ScoreObject {
|
||||||
|
|
||||||
impl From<(String, f64)> for ScoreObject {
|
impl From<(String, f64)> for ScoreObject {
|
||||||
fn from(t: (String, f64)) -> Self {
|
fn from(t: (String, f64)) -> Self {
|
||||||
ScoreObject {name: t.0, score: t.1}
|
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...");
|
||||||
|
@ -100,15 +85,33 @@ pub(crate) async fn route_board_get(
|
||||||
}
|
}
|
||||||
|
|
||||||
log::trace!("Determining the Redis key name...");
|
log::trace!("Determining the Redis key name...");
|
||||||
|
let order_key = format!("board:{board}:order");
|
||||||
let scores_key = format!("board:{board}:scores");
|
let scores_key = format!("board:{board}:scores");
|
||||||
|
|
||||||
let mut rconn = rclient.get_connection_or_504().await?;
|
let mut rconn = rclient.get_connection_or_504().await?;
|
||||||
|
|
||||||
|
log::trace!("Determining sorting order...");
|
||||||
|
let order = rconn.get::<&str, String>(&order_key).await
|
||||||
|
.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:?}");
|
||||||
|
|
||||||
|
log::trace!("Building score retrieval command...");
|
||||||
|
let mut cmd = redis::Cmd::new();
|
||||||
|
let mut cmd_with_args = cmd.arg("ZRANGE").arg(&scores_key).arg(&offset).arg(&offset + &size);
|
||||||
|
if let SortingOrder::Descending = &order {
|
||||||
|
cmd_with_args = cmd_with_args.arg("REV");
|
||||||
|
}
|
||||||
|
cmd_with_args = cmd_with_args.arg("WITHSCORES");
|
||||||
|
|
||||||
log::trace!("Retrieving scores from {board}...");
|
log::trace!("Retrieving scores from {board}...");
|
||||||
let result: RouteBoardResponse = redis::cmd("ZSCAN").arg(&scores_key).arg(offset).arg("COUNT").arg(&size)
|
let result: Vec<ScoreObject> = cmd_with_args
|
||||||
.query_async::<redis::aio::Connection, (usize, Vec<(String, f64)>)>(&mut rconn).await
|
.query_async::<redis::aio::Connection, Vec<(String, f64)>>(&mut rconn).await
|
||||||
.map_err(outcome::redis_cmd_failed)?
|
.map_err(outcome::redis_cmd_failed)?
|
||||||
.into();
|
.into_iter()
|
||||||
|
.map(From::<(String, f64)>::from)
|
||||||
|
.collect();
|
||||||
|
|
||||||
Ok((StatusCode::OK, outcome::req_success!(result)))
|
Ok((StatusCode::OK, outcome::req_success!(result)))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,25 +1,22 @@
|
||||||
//! Module defining routes for `/`.
|
//! Module defining routes for `/`.
|
||||||
|
|
||||||
use axum::Extension;
|
use axum::Extension;
|
||||||
|
use axum::http::StatusCode;
|
||||||
use crate::outcome;
|
use crate::outcome;
|
||||||
use crate::shortcuts::redis::RedisConnectOr504;
|
use crate::shortcuts::redis::RedisConnectOr504;
|
||||||
|
|
||||||
|
|
||||||
/// Handler for `GET /`.
|
/// Handler for `GET /`.
|
||||||
///
|
pub(crate) async fn route_home_get() -> StatusCode {
|
||||||
/// Verifies that the web server is working correctly.
|
|
||||||
pub(crate) async fn route_home_get() -> outcome::RequestResult {
|
|
||||||
log::trace!("Echoing back a success...");
|
log::trace!("Echoing back a success...");
|
||||||
Ok(outcome::success_null())
|
StatusCode::NO_CONTENT
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Handler for `PATCH /`.
|
/// Handler for `POST /`.
|
||||||
///
|
pub(crate) async fn route_home_post(
|
||||||
/// Pings Redis to verify that everything is working correctly.
|
|
||||||
pub(crate) async fn route_home_patch(
|
|
||||||
Extension(rclient): Extension<redis::Client>
|
Extension(rclient): Extension<redis::Client>
|
||||||
) -> outcome::RequestResult {
|
) -> Result<StatusCode, outcome::RequestTuple> {
|
||||||
|
|
||||||
let mut rconn = rclient.get_connection_or_504().await?;
|
let mut rconn = rclient.get_connection_or_504().await?;
|
||||||
|
|
||||||
|
@ -28,6 +25,6 @@ pub(crate) async fn route_home_patch(
|
||||||
.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(|| StatusCode::NO_CONTENT)
|
||||||
.ok_or_else(outcome::redis_unexpected_behaviour)
|
.ok_or_else(outcome::redis_unexpected_behaviour)
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,15 @@ pub(crate) struct RouteScoreQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub(crate) struct RouteScoreResponse {
|
||||||
|
/// The score the user has on the board.
|
||||||
|
pub score: f64,
|
||||||
|
/// The position of the user relative to the other users on the board, zero-based.
|
||||||
|
pub rank: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Handler for `GET /score/`.
|
/// Handler for `GET /score/`.
|
||||||
pub(crate) async fn route_score_get(
|
pub(crate) async fn route_score_get(
|
||||||
// Request query
|
// Request query
|
||||||
|
@ -33,7 +42,8 @@ pub(crate) async fn route_score_get(
|
||||||
let board = board.to_kebab_lowercase();
|
let board = board.to_kebab_lowercase();
|
||||||
let player = player.to_kebab_lowercase();
|
let player = player.to_kebab_lowercase();
|
||||||
|
|
||||||
log::trace!("Determining the Redis key name...");
|
log::trace!("Determining the Redis key names...");
|
||||||
|
let order_key = format!("board:{board}:order");
|
||||||
let scores_key = format!("board:{board}:scores");
|
let scores_key = format!("board:{board}:scores");
|
||||||
|
|
||||||
let mut rconn = rclient.get_connection_or_504().await?;
|
let mut rconn = rclient.get_connection_or_504().await?;
|
||||||
|
@ -43,9 +53,25 @@ pub(crate) async fn route_score_get(
|
||||||
.map_err(outcome::redis_cmd_failed)?;
|
.map_err(outcome::redis_cmd_failed)?;
|
||||||
log::trace!("Score is: {score:?}");
|
log::trace!("Score is: {score:?}");
|
||||||
|
|
||||||
|
log::trace!("Determining sorting order...");
|
||||||
|
let order = rconn.get::<&str, String>(&order_key).await
|
||||||
|
.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:?}");
|
||||||
|
|
||||||
|
log::trace!("Getting rank...");
|
||||||
|
let rank = match order {
|
||||||
|
SortingOrder::Ascending => rconn.zrank::<&str, &str, usize>(&scores_key, &player),
|
||||||
|
SortingOrder::Descending => rconn.zrevrank::<&str, &str, usize>(&scores_key, &player),
|
||||||
|
}.await.map_err(outcome::redis_cmd_failed)?;
|
||||||
|
log::trace!("Rank is: {rank:?}");
|
||||||
|
|
||||||
|
let result = RouteScoreResponse {score, rank};
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
outcome::req_success!(score)
|
outcome::req_success!(result)
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,7 +95,7 @@ pub(crate) async fn route_score_put(
|
||||||
let token_key = format!("board:{board}:token");
|
let token_key = format!("board:{board}:token");
|
||||||
let scores_key = format!("board:{board}:scores");
|
let scores_key = format!("board:{board}:scores");
|
||||||
|
|
||||||
let token = headers.get_authorization_or_401("X-Board-Token")?;
|
let token = headers.get_authorization_or_401("Bearer")?;
|
||||||
let mut rconn = rclient.get_connection_or_504().await?;
|
let mut rconn = rclient.get_connection_or_504().await?;
|
||||||
|
|
||||||
log::trace!("Checking if the token exists and matches...");
|
log::trace!("Checking if the token exists and matches...");
|
||||||
|
@ -102,12 +128,21 @@ pub(crate) async fn route_score_put(
|
||||||
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:?}");
|
||||||
|
|
||||||
|
log::trace!("Getting rank...");
|
||||||
|
let rank = match order {
|
||||||
|
SortingOrder::Ascending => rconn.zrank::<&str, &str, usize>(&scores_key, &player),
|
||||||
|
SortingOrder::Descending => rconn.zrevrank::<&str, &str, usize>(&scores_key, &player),
|
||||||
|
}.await.map_err(outcome::redis_cmd_failed)?;
|
||||||
|
log::trace!("Rank is: {rank:?}");
|
||||||
|
|
||||||
|
let result = RouteScoreResponse {score, rank};
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
match changed.gt(&0) {
|
match changed.gt(&0) {
|
||||||
true => StatusCode::CREATED,
|
true => StatusCode::CREATED,
|
||||||
false => StatusCode::OK,
|
false => StatusCode::OK,
|
||||||
},
|
},
|
||||||
outcome::req_success!(nscore)
|
outcome::req_success!(result)
|
||||||
))
|
))
|
||||||
}
|
}
|
Loading…
Reference in a new issue