summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/app.rs37
-rw-r--r--src/main.rs73
-rw-r--r--src/play.rs25
-rw-r--r--src/session.rs2
4 files changed, 74 insertions, 63 deletions
diff --git a/src/app.rs b/src/app.rs
new file mode 100644
index 0000000..43e2ac6
--- /dev/null
+++ b/src/app.rs
@@ -0,0 +1,37 @@
+//! Internal app logic.
+
+use crate::session::Session;
+use std::collections::HashMap;
+use std::sync::{Mutex, MutexGuard, RwLock};
+
+/// Provider of the state that Tabletop Ambulator needs to keep track of.
+///
+/// Provides the app state, including runtime data such as the active game
+/// sessions. It provides access to relevant data through synchronization
+/// primitives, but does not handle shared ownership; for that, you may
+/// want to use a solution like [`std::sync::Arc`].
+#[derive(Debug, Default)]
+pub struct AppState {
+ sessions: RwLock<HashMap<String, Mutex<Session>>>,
+}
+
+impl AppState {
+ /// Creates a new `AppState`.
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ /// Creates and stores a new [`Session`], which can later be accessed with [`with_session`].
+ pub fn create_session(&self, id: String, name: String) {
+ let session = Mutex::new(Session::new(name));
+ self.sessions.write().unwrap().insert(id, session);
+ }
+
+ /// Finds a [`Session`] by the provided `id`. If the session exists, it is provided within a
+ /// [`MutexGuard`] to the supplied function. Returns the result of the function, or `None` if
+ /// there was no session with the supplied id.
+ pub fn with_session<T>(&self, id: &str, f: impl FnOnce(MutexGuard<Session>) -> T) -> Option<T> {
+ let sessions = self.sessions.read().unwrap();
+ sessions.get(id).map(|session| f(session.lock().unwrap()))
+ }
+}
diff --git a/src/main.rs b/src/main.rs
index b423f6b..38bbbf2 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -6,12 +6,14 @@
#![warn(missing_docs, missing_debug_implementations)]
+mod app;
mod play;
mod session;
mod template;
+use crate::app::AppState;
use crate::play::handle_play;
-use crate::session::{HandObject, PlayerColor, Session};
+use crate::session::{HandObject, PlayerColor};
use crate::template::{IndexTemplate, SessionTemplate};
use askama::Template;
use axum::extract::{Path, Query, State, WebSocketUpgrade};
@@ -23,23 +25,12 @@ use rust_embed::Embed;
use std::array;
use std::collections::HashMap;
use std::net::SocketAddr;
-use std::sync::{Arc, Mutex, RwLock};
+use std::sync::Arc;
#[derive(Embed)]
#[folder = "assets/"]
struct EmbedAsset;
-#[derive(Default)]
-struct AppState {
- sessions: RwLock<HashMap<String, Mutex<Session>>>,
-}
-
-impl AppState {
- fn new() -> Self {
- Self::default()
- }
-}
-
#[tokio::main]
async fn main() {
let app = Router::new()
@@ -57,8 +48,7 @@ async fn main() {
}
async fn serve_index() -> axum::response::Result<Html<String>> {
- let template = IndexTemplate;
- Template::render(&template)
+ Template::render(&IndexTemplate)
.map(Html)
.inspect_err(|e| eprintln!("Template render error: {}", e))
.map_err(|_| ErrorResponse::from(StatusCode::INTERNAL_SERVER_ERROR))
@@ -89,19 +79,16 @@ async fn visit_session(
Path(id): Path<String>,
State(state): State<Arc<AppState>>,
) -> axum::response::Result<Html<String>> {
- let sessions = state.sessions.read().unwrap();
- let session = sessions
- .get(&id)
- .ok_or((StatusCode::NOT_FOUND, "Session does not exist"))?
- .lock()
- .unwrap();
-
- let template = SessionTemplate {
- id: id.as_str(),
- session: &session,
- };
- Template::render(&template)
- .map(Html)
+ let rendered = state
+ .with_session(id.as_str(), |session| {
+ Template::render(&SessionTemplate {
+ id: id.as_str(),
+ session: &session,
+ })
+ .map(Html)
+ })
+ .ok_or(StatusCode::NOT_FOUND)?;
+ rendered
.inspect_err(|e| eprintln!("Template render error: {}", e))
.map_err(|_| ErrorResponse::from(StatusCode::INTERNAL_SERVER_ERROR))
}
@@ -111,12 +98,12 @@ async fn create_session(
Query(query): Query<HashMap<String, String>>,
State(state): State<Arc<AppState>>,
) -> StatusCode {
- let name = query.get("name").cloned().unwrap_or("Unknown".to_string());
+ let name = query
+ .get("name")
+ .cloned()
+ .unwrap_or("Unknown User".to_string());
- let mut sessions = state.sessions.write().unwrap();
-
- let session = Session::new(name);
- sessions.insert(id, Mutex::new(session));
+ state.create_session(id, name);
StatusCode::CREATED
}
@@ -124,32 +111,24 @@ async fn create_session(
async fn update_hands(
Path(id): Path<String>,
State(state): State<Arc<AppState>>,
- Json(payload): Json<HashMap<String, Vec<HandObject>>>,
+ Json(mut payload): Json<HashMap<String, Vec<HandObject>>>,
) -> StatusCode {
- let sessions = state.sessions.read().unwrap();
-
let hand = array::from_fn(|i| {
let color = PlayerColor::try_from(i);
let color = color.as_ref().map(AsRef::as_ref);
if let Ok(color) = color {
- // It would not be necessary to clone here if the vector could be moved, which could be
- // done if payload was mutable
- payload.get(color).cloned().unwrap_or_else(Vec::new)
+ payload.remove(color).unwrap_or_else(Vec::new)
} else {
Vec::new()
}
});
- match sessions.get(&id) {
- Some(session) => {
- let mut session = session.lock().unwrap();
-
+ state
+ .with_session(id.as_str(), |mut session| {
session.update_hands(hand);
-
StatusCode::NO_CONTENT
- }
- None => StatusCode::NOT_FOUND,
- }
+ })
+ .unwrap_or(StatusCode::NOT_FOUND)
}
async fn upgrade_play(ws: WebSocketUpgrade, State(state): State<Arc<AppState>>) -> Response {
diff --git a/src/play.rs b/src/play.rs
index 998ac29..0ec9758 100644
--- a/src/play.rs
+++ b/src/play.rs
@@ -144,16 +144,7 @@ async fn handle_play_message(
) -> Result<(), Error> {
match message {
IncomingPlayMessage::Initialize { id } => {
- let (colors, update_rx) = {
- let sessions = app_state.sessions.read().unwrap();
- let session = sessions.get(&id).map(|session| session.lock().unwrap());
- // The Option is unwrapped with let else here instead of ok_or and propagating
- // because the error moves id which would be used later, and the compiler does not
- // reason about propagation returning early.
- let Some(session) = session else {
- return Err(Error::InvalidSession(id));
- };
-
+ let data_opt = app_state.with_session(id.as_str(), |session| {
let colors: Vec<String> = session
.seats
.iter()
@@ -165,6 +156,11 @@ async fn handle_play_message(
let update_rx = session.update_tx.subscribe();
(colors, update_rx)
+ });
+ // let else used instead of propagating Option::ok_or_else because compiler wouldn't
+ // know about early return when moving id
+ let Some((colors, update_rx)) = data_opt else {
+ return Err(Error::InvalidSession(id));
};
let update_cancel_rx = {
@@ -194,16 +190,15 @@ async fn handle_play_message(
let mut state = state.lock().unwrap();
state.color =
PlayerColor::try_from(color.as_str()).map_err(|_| Error::InvalidColor)?;
+
let name = state
.session
.as_ref()
.ok_or_else(|| Error::InvalidSession(String::default()))?;
- let sessions = app_state.sessions.read().unwrap();
- let session = sessions
- .get(name)
- .ok_or(Error::InvalidSession(name.clone()))?;
- session.lock().unwrap().seats[&state.color].clone()
+ app_state
+ .with_session(name.as_str(), |session| session.seats[&state.color].clone())
+ .ok_or_else(|| Error::InvalidSession(name.clone()))?
};
sender_tx
diff --git a/src/session.rs b/src/session.rs
index f4c8fe3..c15bfda 100644
--- a/src/session.rs
+++ b/src/session.rs
@@ -50,7 +50,7 @@ pub struct CustomDeck {
///
/// See [Player Colors](https://api.tabletopsimulator.com/player/colors/) from the Tabletop
/// Simulator API knowledge base.
-#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
+#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum PlayerColor {
White,
Brown,