mirror of
https://github.com/Steffo99/distributed-arcade.git
synced 2024-11-21 15:44:26 +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",
|
||||
"pretty_env_logger",
|
||||
"r2d2",
|
||||
"rand",
|
||||
"redis",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
|
|
@ -14,4 +14,5 @@ lazy_static = { version = "1.4.0" }
|
|||
serde = { version = "1.0.147", features=["derive"] }
|
||||
serde_json = { version = "1.0.87" }
|
||||
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")
|
||||
.parse()
|
||||
.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 routes;
|
||||
mod outcome;
|
||||
|
||||
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/`.
|
||||
|
||||
use axum::{Extension, Json};
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use axum::extract::{Extension, Json};
|
||||
use redis::AsyncCommands;
|
||||
use serde_json::json;
|
||||
use serde::Serialize;
|
||||
use serde::Deserialize;
|
||||
|
||||
|
||||
/// 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)))
|
||||
}
|
||||
}
|
||||
use crate::outcome;
|
||||
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum Order {
|
||||
/// The greater the score, the worse it is.
|
||||
#[serde(rename = "ASC")]
|
||||
Ascending,
|
||||
/// The greater the score, the better it is.
|
||||
#[serde(rename = "DSC")]
|
||||
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/`.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct RouteBoardPostInput {
|
||||
/// The name of the board to create.
|
||||
name: String,
|
||||
/// The [`Order`] of the scores in the board to create.
|
||||
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/`.
|
||||
///
|
||||
/// Creates a new board, storing the details on Redis.
|
||||
|
@ -64,38 +74,55 @@ pub struct RouteBoardPostInput {
|
|||
/// Will refuse to overwrite an already existing board.
|
||||
pub async fn route_board_post(
|
||||
Extension(rclient): Extension<redis::Client>,
|
||||
Json(input): Json<RouteBoardPostInput>,
|
||||
) -> Result<OutcomeBoardPost, OutcomeBoardPost> {
|
||||
Json(RouteBoardPostInput {name, order}): Json<RouteBoardPostInput>,
|
||||
) -> outcome::RequestResult {
|
||||
|
||||
log::trace!("Connecting to Redis...");
|
||||
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 token_key = format!("board:{name}:token");
|
||||
let scores_key = format!("board:{name}:scores");
|
||||
|
||||
log::trace!("Checking that the board does not already exist via the order key...");
|
||||
redis::cmd("TYPE").arg(&order_key).query_async::<redis::aio::Connection, String>(&mut rconn).await
|
||||
.map_err(|_| RedisCheckExistenceError)?
|
||||
.eq("none").then_some(())
|
||||
.ok_or(AlreadyExists)?;
|
||||
log::trace!("Ensuring a board does not already exist...");
|
||||
ensure_key_is_empty(&mut rconn, &order_key).await?;
|
||||
ensure_key_is_empty(&mut rconn, &token_key).await?;
|
||||
ensure_key_is_empty(&mut rconn, &scores_key).await?;
|
||||
|
||||
// Possibly superfluous, but better be safe than sorry
|
||||
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::info!("Creating board: {}", &name);
|
||||
log::trace!("Board order is: {order:?}");
|
||||
|
||||
log::trace!("Generating a board token...");
|
||||
let mut rng = rand::rngs::OsRng::default();
|
||||
let mut token: [u32; 16] = [0; 16];
|
||||
rand::Fill::try_fill(&mut token, &mut rng)
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, outcome::req_error!("Failed to generate a secure board token.")))?;
|
||||
// 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:?}");
|
||||
|
||||
log::trace!("Setting the board order...");
|
||||
rconn.set(&order_key, match input.order {
|
||||
Order::Ascending => "LT",
|
||||
Order::Descending => "GT",
|
||||
}).await
|
||||
.map_err(|_| RedisSetOrderError)?;
|
||||
log::trace!("Starting Redis transaction...");
|
||||
redis::cmd("MULTI").query_async(&mut rconn).await
|
||||
.map_err(outcome::redis_cmd_failed)?;
|
||||
|
||||
Ok(Success)
|
||||
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 `/`.
|
||||
|
||||
use axum::{Extension, Json};
|
||||
use axum::http::StatusCode;
|
||||
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)))
|
||||
}
|
||||
}
|
||||
use axum::Extension;
|
||||
use crate::outcome;
|
||||
|
||||
|
||||
/// Handler for `GET /`.
|
||||
|
@ -39,17 +9,17 @@ impl IntoResponse for OutcomeHomeGet {
|
|||
/// Pings Redis to verify that everything is working correctly.
|
||||
pub async fn route_home_get(
|
||||
Extension(rclient): Extension<redis::Client>
|
||||
) -> Result<OutcomeHomeGet, OutcomeHomeGet> {
|
||||
) -> outcome::RequestResult {
|
||||
|
||||
log::trace!("Connecting to Redis...");
|
||||
let mut rconn = rclient.get_async_connection().await
|
||||
.map_err(|_| RedisConnectionError)?;
|
||||
.map_err(outcome::redis_conn_failed)?;
|
||||
|
||||
log::trace!("Sending PING...");
|
||||
let pong = redis::cmd("PING").query_async::<redis::aio::Connection, String>(&mut rconn).await
|
||||
.map_err(|_| RedisPingError)?;
|
||||
|
||||
log::trace!("Expecting PONG: {pong:?}");
|
||||
pong.eq("PONG")
|
||||
.then_some(Success).ok_or(RedisPongError)
|
||||
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)?
|
||||
.eq("PONG")
|
||||
.then(outcome::success_null)
|
||||
.ok_or_else(outcome::redis_unexpected_behaviour)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue