mirror of
https://github.com/Steffo99/distributed-arcade.git
synced 2024-11-21 23:54:25 +00:00
Refactor board creation code
This commit is contained in:
parent
1e99d6673e
commit
edc97e9122
7 changed files with 167 additions and 99 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -150,6 +150,7 @@ dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"pretty_env_logger",
|
"pretty_env_logger",
|
||||||
"r2d2",
|
"r2d2",
|
||||||
|
"rand",
|
||||||
"redis",
|
"redis",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
|
|
@ -15,3 +15,4 @@ serde = { version = "1.0.147", features=["derive"] }
|
||||||
serde_json = { version = "1.0.87" }
|
serde_json = { version = "1.0.87" }
|
||||||
log = { version = "0.4.17" }
|
log = { version = "0.4.17" }
|
||||||
pretty_env_logger = { version = "0.4.0" }
|
pretty_env_logger = { version = "0.4.0" }
|
||||||
|
rand = { version = "0.8.5" }
|
||||||
|
|
|
@ -11,4 +11,7 @@ lazy_static! {
|
||||||
.expect("AXUM_HOST_STRING to be set")
|
.expect("AXUM_HOST_STRING to be set")
|
||||||
.parse()
|
.parse()
|
||||||
.expect("AXUM_HOST_STRING to be a valid SocketAddr");
|
.expect("AXUM_HOST_STRING to be a valid SocketAddr");
|
||||||
|
|
||||||
|
pub static ref CREATE_TOKEN: String = env::var("CREATE_TOKEN")
|
||||||
|
.expect("CREATE_TOKEN to be set");
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
mod config;
|
mod config;
|
||||||
mod routes;
|
mod routes;
|
||||||
|
mod outcome;
|
||||||
|
|
||||||
use axum::routing::{get, post};
|
use axum::routing::{get, post};
|
||||||
|
|
||||||
|
|
65
src/outcome.rs
Normal file
65
src/outcome.rs
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
use axum::extract::Json;
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
/// The `([StatusCode], Body)` tuple returned by API handlers.
|
||||||
|
pub(crate) type RequestTuple = (StatusCode, Json<Value>);
|
||||||
|
|
||||||
|
/// A [`Result`] made of two [`RequestTuple`]s to make handling errors easier.
|
||||||
|
pub(crate) type RequestResult = Result<RequestTuple, RequestTuple>;
|
||||||
|
|
||||||
|
macro_rules! req_error {
|
||||||
|
( $val:tt ) => {
|
||||||
|
Json(serde_json::json!({
|
||||||
|
"ok": false,
|
||||||
|
"error": $val
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Macro used to build a API error.
|
||||||
|
pub(crate) use req_error;
|
||||||
|
|
||||||
|
macro_rules! req_success {
|
||||||
|
( $val:tt ) => {
|
||||||
|
Json(serde_json::json!({
|
||||||
|
"ok": true,
|
||||||
|
"data": $val
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Macro used to build a API success.
|
||||||
|
pub(crate) use req_success;
|
||||||
|
|
||||||
|
/// The server could not connect to Redis.
|
||||||
|
pub(crate) fn redis_conn_failed(_err: redis::RedisError) -> RequestTuple {
|
||||||
|
(
|
||||||
|
StatusCode::GATEWAY_TIMEOUT,
|
||||||
|
req_error!("Could not connect to Redis")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The execution of a command in Redis failed.
|
||||||
|
pub(crate) fn redis_cmd_failed(_err: redis::RedisError) -> RequestTuple {
|
||||||
|
(
|
||||||
|
StatusCode::BAD_GATEWAY,
|
||||||
|
req_error!("Could not execute Redis command")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The result of a command in Redis is unexpected.
|
||||||
|
pub(crate) fn redis_unexpected_behaviour() -> RequestTuple {
|
||||||
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,62 +1,72 @@
|
||||||
//! Module defining routes for `/board/`.
|
//! Module defining routes for `/board/`.
|
||||||
|
|
||||||
use axum::{Extension, Json};
|
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
use axum::response::{IntoResponse, Response};
|
use axum::extract::{Extension, Json};
|
||||||
use redis::AsyncCommands;
|
use redis::AsyncCommands;
|
||||||
use serde_json::json;
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use crate::outcome;
|
||||||
|
|
||||||
/// Possible results for `POST /board/`.
|
|
||||||
pub enum OutcomeBoardPost {
|
|
||||||
/// Could not connect to Redis.
|
|
||||||
RedisConnectionError,
|
|
||||||
/// Could not check the existence of the board on Redis.
|
|
||||||
RedisCheckExistenceError,
|
|
||||||
/// Could not set the board ordering on Redis.
|
|
||||||
RedisSetOrderError,
|
|
||||||
/// Board already exists.
|
|
||||||
AlreadyExists,
|
|
||||||
/// Board created successfully.
|
|
||||||
Success,
|
|
||||||
}
|
|
||||||
|
|
||||||
use OutcomeBoardPost::*;
|
|
||||||
|
|
||||||
impl IntoResponse for OutcomeBoardPost {
|
|
||||||
fn into_response(self) -> Response {
|
|
||||||
let (status, response) = match self {
|
|
||||||
RedisConnectionError => (StatusCode::GATEWAY_TIMEOUT, json!("Could not connect to Redis")),
|
|
||||||
RedisCheckExistenceError => (StatusCode::INTERNAL_SERVER_ERROR, json!("Could not check if the board already exists")),
|
|
||||||
RedisSetOrderError => (StatusCode::INTERNAL_SERVER_ERROR, json!("Could not set the board's ordering")),
|
|
||||||
AlreadyExists => (StatusCode::CONFLICT, json!("Board already exists")),
|
|
||||||
Success => (StatusCode::OK, json!([]))
|
|
||||||
};
|
|
||||||
|
|
||||||
IntoResponse::into_response((status, Json(response)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||||
pub enum Order {
|
pub enum Order {
|
||||||
/// The greater the score, the worse it is.
|
/// The greater the score, the worse it is.
|
||||||
|
#[serde(rename = "ASC")]
|
||||||
Ascending,
|
Ascending,
|
||||||
/// The greater the score, the better it is.
|
/// The greater the score, the better it is.
|
||||||
|
#[serde(rename = "DSC")]
|
||||||
Descending,
|
Descending,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<Order> for String {
|
||||||
|
fn from(ord: Order) -> Self {
|
||||||
|
match ord {
|
||||||
|
Order::Ascending => "ASC".to_string(),
|
||||||
|
Order::Descending => "DSC".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Expected input data for `POST /board/`.
|
/// Expected input data for `POST /board/`.
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct RouteBoardPostInput {
|
pub struct RouteBoardPostInput {
|
||||||
|
/// The name of the board to create.
|
||||||
name: String,
|
name: String,
|
||||||
|
/// The [`Order`] of the scores in the board to create.
|
||||||
order: Order,
|
order: Order,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Expected output data for `POST /board/`.
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct RouteBoardPostOutput {
|
||||||
|
/// The name of the created board.
|
||||||
|
name: String,
|
||||||
|
/// The [`Order`] of the scores in the created board.
|
||||||
|
order: Order,
|
||||||
|
/// The token to use to submit scores to the board.
|
||||||
|
token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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)?
|
||||||
|
.eq("none")
|
||||||
|
.then_some(())
|
||||||
|
.ok_or((StatusCode::CONFLICT, outcome::req_error!("Board already exists")))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const TOKEN_CHARS: &[char; 62] = &[
|
||||||
|
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
/// 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.
|
||||||
|
@ -64,38 +74,55 @@ pub struct RouteBoardPostInput {
|
||||||
/// Will refuse to overwrite an already existing board.
|
/// Will refuse to overwrite an already existing board.
|
||||||
pub async fn route_board_post(
|
pub async fn route_board_post(
|
||||||
Extension(rclient): Extension<redis::Client>,
|
Extension(rclient): Extension<redis::Client>,
|
||||||
Json(input): Json<RouteBoardPostInput>,
|
Json(RouteBoardPostInput {name, order}): Json<RouteBoardPostInput>,
|
||||||
) -> Result<OutcomeBoardPost, OutcomeBoardPost> {
|
) -> outcome::RequestResult {
|
||||||
|
|
||||||
log::trace!("Connecting to Redis...");
|
log::trace!("Connecting to Redis...");
|
||||||
let mut rconn = rclient.get_async_connection().await
|
let mut rconn = rclient.get_async_connection().await
|
||||||
.map_err(|_| RedisConnectionError)?;
|
.map_err(outcome::redis_conn_failed)?;
|
||||||
|
|
||||||
let name = &input.name;
|
log::trace!("Determining the Redis key names...");
|
||||||
let order_key = format!("board:{name}:order");
|
let order_key = format!("board:{name}:order");
|
||||||
|
let token_key = format!("board:{name}:token");
|
||||||
let scores_key = format!("board:{name}:scores");
|
let scores_key = format!("board:{name}:scores");
|
||||||
|
|
||||||
log::trace!("Checking that the board does not already exist via the order key...");
|
log::trace!("Ensuring a board does not already exist...");
|
||||||
redis::cmd("TYPE").arg(&order_key).query_async::<redis::aio::Connection, String>(&mut rconn).await
|
ensure_key_is_empty(&mut rconn, &order_key).await?;
|
||||||
.map_err(|_| RedisCheckExistenceError)?
|
ensure_key_is_empty(&mut rconn, &token_key).await?;
|
||||||
.eq("none").then_some(())
|
ensure_key_is_empty(&mut rconn, &scores_key).await?;
|
||||||
.ok_or(AlreadyExists)?;
|
|
||||||
|
|
||||||
// Possibly superfluous, but better be safe than sorry
|
log::info!("Creating board: {name:?}");
|
||||||
log::trace!("Checking that the board does not already exist via the scores key...");
|
|
||||||
redis::cmd("TYPE").arg(&scores_key).query_async::<redis::aio::Connection, String>(&mut rconn).await
|
|
||||||
.map_err(|_| RedisCheckExistenceError)?
|
|
||||||
.eq("none").then_some(())
|
|
||||||
.ok_or(AlreadyExists)?;
|
|
||||||
|
|
||||||
log::info!("Creating board: {}", &name);
|
log::trace!("Board order is: {order:?}");
|
||||||
|
|
||||||
log::trace!("Setting the board order...");
|
log::trace!("Generating a board token...");
|
||||||
rconn.set(&order_key, match input.order {
|
let mut rng = rand::rngs::OsRng::default();
|
||||||
Order::Ascending => "LT",
|
let mut token: [u32; 16] = [0; 16];
|
||||||
Order::Descending => "GT",
|
rand::Fill::try_fill(&mut token, &mut rng)
|
||||||
}).await
|
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, outcome::req_error!("Failed to generate a secure board token.")))?;
|
||||||
.map_err(|_| RedisSetOrderError)?;
|
// FIXME: only works on platforms where usize >= 32-bit?
|
||||||
|
let token: String = token.iter().map(|e| TOKEN_CHARS.get(*e as usize % 62).expect("randomly generated value to be a valid index"))
|
||||||
|
.collect::<String>();
|
||||||
|
log::trace!("Board token is: {token:?}");
|
||||||
|
|
||||||
Ok(Success)
|
log::trace!("Starting Redis transaction...");
|
||||||
|
redis::cmd("MULTI").query_async(&mut rconn).await
|
||||||
|
.map_err(outcome::redis_cmd_failed)?;
|
||||||
|
|
||||||
|
log::trace!("Setting board order...");
|
||||||
|
rconn.set(&order_key, String::from(order)).await
|
||||||
|
.map_err(outcome::redis_cmd_failed)?;
|
||||||
|
|
||||||
|
log::trace!("Setting board token...");
|
||||||
|
rconn.set(&token_key, &token).await
|
||||||
|
.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)?;
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
StatusCode::OK,
|
||||||
|
Json(serde_json::to_value(RouteBoardPostOutput {name, order, token}).expect("to be able to serialize RouteBoardPostOutput"))
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,37 +1,7 @@
|
||||||
//! Module defining routes for `/`.
|
//! Module defining routes for `/`.
|
||||||
|
|
||||||
use axum::{Extension, Json};
|
use axum::Extension;
|
||||||
use axum::http::StatusCode;
|
use crate::outcome;
|
||||||
use axum::response::{IntoResponse, Response};
|
|
||||||
use serde_json::json;
|
|
||||||
|
|
||||||
|
|
||||||
/// Possible results for `GET /`.
|
|
||||||
pub enum OutcomeHomeGet {
|
|
||||||
/// Could not connect to Redis.
|
|
||||||
RedisConnectionError,
|
|
||||||
/// Could not PING Redis.
|
|
||||||
RedisPingError,
|
|
||||||
/// Did not get a PONG back from Redis.
|
|
||||||
RedisPongError,
|
|
||||||
/// Ping successful.
|
|
||||||
Success,
|
|
||||||
}
|
|
||||||
|
|
||||||
use OutcomeHomeGet::*;
|
|
||||||
|
|
||||||
impl IntoResponse for OutcomeHomeGet {
|
|
||||||
fn into_response(self) -> Response {
|
|
||||||
let (status, response) = match self {
|
|
||||||
RedisConnectionError => (StatusCode::GATEWAY_TIMEOUT, json!("Could not connect to Redis")),
|
|
||||||
RedisPingError => (StatusCode::BAD_GATEWAY, json!("Could not ping Redis")),
|
|
||||||
RedisPongError => (StatusCode::INTERNAL_SERVER_ERROR, json!("Redis did not pong back")),
|
|
||||||
Success => (StatusCode::OK, json!("Welcome to distributed_arcade! Redis seems to be working correctly."))
|
|
||||||
};
|
|
||||||
|
|
||||||
IntoResponse::into_response((status, Json(response)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// Handler for `GET /`.
|
/// Handler for `GET /`.
|
||||||
|
@ -39,17 +9,17 @@ impl IntoResponse for OutcomeHomeGet {
|
||||||
/// Pings Redis to verify that everything is working correctly.
|
/// Pings Redis to verify that everything is working correctly.
|
||||||
pub async fn route_home_get(
|
pub async fn route_home_get(
|
||||||
Extension(rclient): Extension<redis::Client>
|
Extension(rclient): Extension<redis::Client>
|
||||||
) -> Result<OutcomeHomeGet, OutcomeHomeGet> {
|
) -> outcome::RequestResult {
|
||||||
|
|
||||||
log::trace!("Connecting to Redis...");
|
log::trace!("Connecting to Redis...");
|
||||||
let mut rconn = rclient.get_async_connection().await
|
let mut rconn = rclient.get_async_connection().await
|
||||||
.map_err(|_| RedisConnectionError)?;
|
.map_err(outcome::redis_conn_failed)?;
|
||||||
|
|
||||||
log::trace!("Sending PING...");
|
log::trace!("Sending PING and expecting PONG...");
|
||||||
let pong = redis::cmd("PING").query_async::<redis::aio::Connection, String>(&mut rconn).await
|
redis::cmd("PING")
|
||||||
.map_err(|_| RedisPingError)?;
|
.query_async::<redis::aio::Connection, String>(&mut rconn).await
|
||||||
|
.map_err(outcome::redis_cmd_failed)?
|
||||||
log::trace!("Expecting PONG: {pong:?}");
|
.eq("PONG")
|
||||||
pong.eq("PONG")
|
.then(outcome::success_null)
|
||||||
.then_some(Success).ok_or(RedisPongError)
|
.ok_or_else(outcome::redis_unexpected_behaviour)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue