1
Fork 0
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:
Steffo 2022-11-11 11:49:10 +00:00 committed by GitHub
parent 1e99d6673e
commit edc97e9122
Signed by: github
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 167 additions and 99 deletions

1
Cargo.lock generated
View file

@ -150,6 +150,7 @@ dependencies = [
"log", "log",
"pretty_env_logger", "pretty_env_logger",
"r2d2", "r2d2",
"rand",
"redis", "redis",
"serde", "serde",
"serde_json", "serde_json",

View file

@ -14,4 +14,5 @@ lazy_static = { version = "1.4.0" }
serde = { version = "1.0.147", features=["derive"] } 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" }

View file

@ -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");
} }

View file

@ -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
View 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)
)
}

View file

@ -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!("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..."); log::trace!("Starting Redis transaction...");
rconn.set(&order_key, match input.order { redis::cmd("MULTI").query_async(&mut rconn).await
Order::Ascending => "LT", .map_err(outcome::redis_cmd_failed)?;
Order::Descending => "GT",
}).await
.map_err(|_| RedisSetOrderError)?;
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"))
))
} }

View file

@ -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)
} }