From 398bc79a3f1f4316e6cc33b6e6bce133c1db3e4c Mon Sep 17 00:00:00 2001 From: Owen Jacobson Date: Mon, 27 Oct 2025 17:29:36 -0400 Subject: Convert the `Conversations` component into a freestanding struct. Unlike the previous example, this involves cloning an event broadcaster, as well. This is, per the documentation, how the type may be used. From : > The Sender can be cloned to send to the same channel from multiple points in the process or it can be used concurrently from an `Arc`. The language is less firm than the language sqlx uses for its pool, but the intent is clear enough, and it works in practice. --- src/ui/handlers/conversation.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) (limited to 'src/ui') diff --git a/src/ui/handlers/conversation.rs b/src/ui/handlers/conversation.rs index f1bb319..2ff090c 100644 --- a/src/ui/handlers/conversation.rs +++ b/src/ui/handlers/conversation.rs @@ -4,8 +4,7 @@ use axum::{ }; use crate::{ - app::App, - conversation::{self, app}, + conversation::{self, app, app::Conversations}, error::Internal, token::extract::Identity, ui::{ @@ -15,12 +14,12 @@ use crate::{ }; pub async fn handler( - State(app): State, + State(conversations): State, identity: Option, Path(conversation): Path, ) -> Result { let _ = identity.ok_or(Error::NotLoggedIn)?; - app.conversations() + conversations .get(&conversation) .await .map_err(Error::from)?; -- cgit v1.2.3 From f305e487d619f1d993d11d728c8cf7261bf3b371 Mon Sep 17 00:00:00 2001 From: Owen Jacobson Date: Mon, 27 Oct 2025 17:41:22 -0400 Subject: Convert `Invites` into a freestanding component. --- src/app.rs | 10 ++++++++-- src/invite/app.rs | 12 ++++++------ src/invite/handlers/accept/mod.rs | 8 +++----- src/invite/handlers/accept/test.rs | 12 ++++++------ src/invite/handlers/get/mod.rs | 7 +++---- src/invite/handlers/get/test.rs | 6 +++--- src/invite/handlers/issue/mod.rs | 9 ++++++--- src/invite/handlers/issue/test.rs | 2 +- src/test/fixtures/invite.rs | 12 ++++++++---- src/ui/handlers/invite.rs | 6 +++--- 10 files changed, 47 insertions(+), 37 deletions(-) (limited to 'src/ui') diff --git a/src/app.rs b/src/app.rs index 0b560dd..2d6d62b 100644 --- a/src/app.rs +++ b/src/app.rs @@ -46,8 +46,8 @@ impl App { Events::new(self.db.clone(), self.events.clone()) } - pub const fn invites(&self) -> Invites<'_> { - Invites::new(&self.db, &self.events) + pub fn invites(&self) -> Invites { + Invites::new(self.db.clone(), self.events.clone()) } pub const fn logins(&self) -> Logins<'_> { @@ -83,3 +83,9 @@ impl FromRef for Conversations { app.conversations() } } + +impl FromRef for Invites { + fn from_ref(app: &App) -> Self { + app.invites() + } +} diff --git a/src/invite/app.rs b/src/invite/app.rs index 6684d03..6f58d0a 100644 --- a/src/invite/app.rs +++ b/src/invite/app.rs @@ -17,13 +17,13 @@ use crate::{ }, }; -pub struct Invites<'a> { - db: &'a SqlitePool, - events: &'a Broadcaster, +pub struct Invites { + db: SqlitePool, + events: Broadcaster, } -impl<'a> Invites<'a> { - pub const fn new(db: &'a SqlitePool, events: &'a Broadcaster) -> Self { +impl Invites { + pub const fn new(db: SqlitePool, events: Broadcaster) -> Self { Self { db, events } } @@ -88,7 +88,7 @@ impl<'a> Invites<'a> { tx.tokens().create(&token, &secret).await?; tx.commit().await?; - stored.publish(self.events); + stored.publish(&self.events); Ok(secret) } diff --git a/src/invite/handlers/accept/mod.rs b/src/invite/handlers/accept/mod.rs index cdf385f..8bdaa51 100644 --- a/src/invite/handlers/accept/mod.rs +++ b/src/invite/handlers/accept/mod.rs @@ -5,11 +5,10 @@ use axum::{ }; use crate::{ - app::App, clock::RequestedAt, empty::Empty, error::{Internal, NotFound}, - invite::{app, handlers::PathInfo}, + invite::{app, app::Invites, handlers::PathInfo}, name::Name, password::Password, token::extract::IdentityCookie, @@ -19,14 +18,13 @@ use crate::{ mod test; pub async fn handler( - State(app): State, + State(invites): State, RequestedAt(accepted_at): RequestedAt, identity: IdentityCookie, Path(invite): Path, Json(request): Json, ) -> Result<(IdentityCookie, Empty), Error> { - let secret = app - .invites() + let secret = invites .accept(&invite, &request.name, &request.password, &accepted_at) .await .map_err(Error)?; diff --git a/src/invite/handlers/accept/test.rs b/src/invite/handlers/accept/test.rs index 283ec76..446dbf9 100644 --- a/src/invite/handlers/accept/test.rs +++ b/src/invite/handlers/accept/test.rs @@ -24,7 +24,7 @@ async fn valid_invite() { password: password.clone(), }; let (identity, Empty) = super::handler( - State(app.clone()), + State(app.invites()), fixtures::now(), identity, Path(invite.id), @@ -67,7 +67,7 @@ async fn nonexistent_invite() { password: password.clone(), }; let super::Error(error) = super::handler( - State(app.clone()), + State(app.invites()), fixtures::now(), identity, Path(invite.clone()), @@ -103,7 +103,7 @@ async fn expired_invite() { password: password.clone(), }; let super::Error(error) = super::handler( - State(app.clone()), + State(app.invites()), fixtures::now(), identity, Path(invite.id.clone()), @@ -140,7 +140,7 @@ async fn accepted_invite() { password: password.clone(), }; let super::Error(error) = super::handler( - State(app.clone()), + State(app.invites()), fixtures::now(), identity, Path(invite.id.clone()), @@ -183,7 +183,7 @@ async fn conflicting_name() { password: password.clone(), }; let super::Error(error) = super::handler( - State(app.clone()), + State(app.invites()), fixtures::now(), identity, Path(invite.id.clone()), @@ -217,7 +217,7 @@ async fn invalid_name() { password: password.clone(), }; let super::Error(error) = super::handler( - State(app.clone()), + State(app.invites()), fixtures::now(), identity, Path(invite.id), diff --git a/src/invite/handlers/get/mod.rs b/src/invite/handlers/get/mod.rs index bb72586..d5fd9c2 100644 --- a/src/invite/handlers/get/mod.rs +++ b/src/invite/handlers/get/mod.rs @@ -4,19 +4,18 @@ use axum::{ }; use crate::{ - app::App, error::{Internal, NotFound}, - invite::{Id, Summary, handlers::PathInfo}, + invite::{Id, Summary, app::Invites, handlers::PathInfo}, }; #[cfg(test)] mod test; pub async fn handler( - State(app): State, + State(invites): State, Path(invite): Path, ) -> Result, Error> { - app.invites() + invites .get(&invite) .await? .map(Json) diff --git a/src/invite/handlers/get/test.rs b/src/invite/handlers/get/test.rs index 0f2f725..a08c510 100644 --- a/src/invite/handlers/get/test.rs +++ b/src/invite/handlers/get/test.rs @@ -12,7 +12,7 @@ async fn valid_invite() { // Call endpoint - let Json(response) = super::handler(State(app), Path(invite.id)) + let Json(response) = super::handler(State(app.invites()), Path(invite.id)) .await .expect("get for an existing invite succeeds"); @@ -31,7 +31,7 @@ async fn nonexistent_invite() { // Call endpoint let invite = fixtures::invite::fictitious(); - let error = super::handler(State(app), Path(invite.clone())) + let error = super::handler(State(app.invites()), Path(invite.clone())) .await .expect_err("get for a nonexistent invite fails"); @@ -55,7 +55,7 @@ async fn expired_invite() { // Call endpoint - let error = super::handler(State(app), Path(invite.id.clone())) + let error = super::handler(State(app.invites()), Path(invite.id.clone())) .await .expect_err("get for an expired invite fails"); diff --git a/src/invite/handlers/issue/mod.rs b/src/invite/handlers/issue/mod.rs index 4ac74cc..0549c78 100644 --- a/src/invite/handlers/issue/mod.rs +++ b/src/invite/handlers/issue/mod.rs @@ -1,19 +1,22 @@ use axum::extract::{Json, State}; use crate::{ - app::App, clock::RequestedAt, error::Internal, invite::Invite, token::extract::Identity, + clock::RequestedAt, + error::Internal, + invite::{Invite, app::Invites}, + token::extract::Identity, }; #[cfg(test)] mod test; pub async fn handler( - State(app): State, + State(invites): State, RequestedAt(issued_at): RequestedAt, identity: Identity, _: Json, ) -> Result, Internal> { - let invite = app.invites().issue(&identity.login, &issued_at).await?; + let invite = invites.issue(&identity.login, &issued_at).await?; Ok(Json(invite)) } diff --git a/src/invite/handlers/issue/test.rs b/src/invite/handlers/issue/test.rs index 4421705..dc89243 100644 --- a/src/invite/handlers/issue/test.rs +++ b/src/invite/handlers/issue/test.rs @@ -13,7 +13,7 @@ async fn create_invite() { // Call the endpoint let Json(invite) = super::handler( - State(app), + State(app.invites()), issued_at.clone(), issuer.clone(), Json(super::Request {}), diff --git a/src/test/fixtures/invite.rs b/src/test/fixtures/invite.rs index 654d1b4..5a5d4d0 100644 --- a/src/test/fixtures/invite.rs +++ b/src/test/fixtures/invite.rs @@ -1,12 +1,16 @@ +use axum::extract::FromRef; + use crate::{ - app::App, clock::DateTime, - invite::{self, Invite}, + invite::{self, Invite, app::Invites}, login::Login, }; -pub async fn issue(app: &App, issuer: &Login, issued_at: &DateTime) -> Invite { - app.invites() +pub async fn issue(app: &App, issuer: &Login, issued_at: &DateTime) -> Invite +where + Invites: FromRef, +{ + Invites::from_ref(app) .issue(issuer, issued_at) .await .expect("issuing invites never fails") diff --git a/src/ui/handlers/invite.rs b/src/ui/handlers/invite.rs index 0f9580a..edd6dc1 100644 --- a/src/ui/handlers/invite.rs +++ b/src/ui/handlers/invite.rs @@ -4,9 +4,9 @@ use axum::{ }; use crate::{ - app::App, error::Internal, invite, + invite::app::Invites, ui::{ assets::{Asset, Assets}, error::NotFound, @@ -14,10 +14,10 @@ use crate::{ }; pub async fn handler( - State(app): State, + State(invites): State, Path(invite): Path, ) -> Result { - app.invites() + invites .get(&invite) .await .map_err(Error::internal)? -- cgit v1.2.3 From d66728889105f6f1ef5113d9ceb223e362df0008 Mon Sep 17 00:00:00 2001 From: Owen Jacobson Date: Mon, 27 Oct 2025 18:15:25 -0400 Subject: Convert the `Setup` component into a freestanding struct. The changes to the setup-requiring middleware are probably more general than was strictly needed, but they will make it work with anything that can provide a `Setup` component rather than being bolted to `App` specifically, which feels tidier. --- src/app.rs | 10 ++++++++-- src/setup/app.rs | 13 +++++++------ src/setup/handlers/setup/mod.rs | 14 +++++++++----- src/setup/handlers/setup/test.rs | 6 +++--- src/setup/required.rs | 31 ++++++++++++++++++------------- src/ui/handlers/setup.rs | 7 +++---- 6 files changed, 48 insertions(+), 33 deletions(-) (limited to 'src/ui') diff --git a/src/app.rs b/src/app.rs index 74e1070..202d542 100644 --- a/src/app.rs +++ b/src/app.rs @@ -58,8 +58,8 @@ impl App { Messages::new(self.db.clone(), self.events.clone()) } - pub const fn setup(&self) -> Setup<'_> { - Setup::new(&self.db, &self.events) + pub fn setup(&self) -> Setup { + Setup::new(self.db.clone(), self.events.clone()) } pub const fn tokens(&self) -> Tokens<'_> { @@ -101,3 +101,9 @@ impl FromRef for Messages { app.messages() } } + +impl FromRef for Setup { + fn from_ref(app: &App) -> Self { + app.setup() + } +} diff --git a/src/setup/app.rs b/src/setup/app.rs index 2a8ec30..9539406 100644 --- a/src/setup/app.rs +++ b/src/setup/app.rs @@ -10,13 +10,14 @@ use crate::{ user::create::{self, Create}, }; -pub struct Setup<'a> { - db: &'a SqlitePool, - events: &'a Broadcaster, +#[derive(Clone)] +pub struct Setup { + db: SqlitePool, + events: Broadcaster, } -impl<'a> Setup<'a> { - pub const fn new(db: &'a SqlitePool, events: &'a Broadcaster) -> Self { +impl Setup { + pub const fn new(db: SqlitePool, events: Broadcaster) -> Self { Self { db, events } } @@ -41,7 +42,7 @@ impl<'a> Setup<'a> { tx.tokens().create(&token, &secret).await?; tx.commit().await?; - stored.publish(self.events); + stored.publish(&self.events); Ok(secret) } diff --git a/src/setup/handlers/setup/mod.rs b/src/setup/handlers/setup/mod.rs index fe24798..2977da8 100644 --- a/src/setup/handlers/setup/mod.rs +++ b/src/setup/handlers/setup/mod.rs @@ -5,21 +5,25 @@ use axum::{ }; use crate::{ - app::App, clock::RequestedAt, empty::Empty, error::Internal, name::Name, password::Password, - setup::app, token::extract::IdentityCookie, + clock::RequestedAt, + empty::Empty, + error::Internal, + name::Name, + password::Password, + setup::{app, app::Setup}, + token::extract::IdentityCookie, }; #[cfg(test)] mod test; pub async fn handler( - State(app): State, + State(setup): State, RequestedAt(setup_at): RequestedAt, identity: IdentityCookie, Json(request): Json, ) -> Result<(IdentityCookie, Empty), Error> { - let secret = app - .setup() + let secret = setup .initial(&request.name, &request.password, &setup_at) .await .map_err(Error)?; diff --git a/src/setup/handlers/setup/test.rs b/src/setup/handlers/setup/test.rs index 283fe8b..670c111 100644 --- a/src/setup/handlers/setup/test.rs +++ b/src/setup/handlers/setup/test.rs @@ -20,7 +20,7 @@ async fn fresh_instance() { password: password.clone(), }; let (identity, Empty) = - super::handler(State(app.clone()), fixtures::now(), identity, Json(request)) + super::handler(State(app.setup()), fixtures::now(), identity, Json(request)) .await .expect("setup in a fresh app succeeds"); @@ -43,7 +43,7 @@ async fn login_exists() { let (name, password) = fixtures::user::propose(); let request = super::Request { name, password }; let super::Error(error) = - super::handler(State(app.clone()), fixtures::now(), identity, Json(request)) + super::handler(State(app.setup()), fixtures::now(), identity, Json(request)) .await .expect_err("setup in a populated app fails"); @@ -68,7 +68,7 @@ async fn invalid_name() { password: password.clone(), }; let super::Error(error) = - super::handler(State(app.clone()), fixtures::now(), identity, Json(request)) + super::handler(State(app.setup()), fixtures::now(), identity, Json(request)) .await .expect_err("setup with an invalid name fails"); diff --git a/src/setup/required.rs b/src/setup/required.rs index a2aed18..e475381 100644 --- a/src/setup/required.rs +++ b/src/setup/required.rs @@ -4,26 +4,29 @@ use std::{ }; use axum::{ - extract::Request, + extract::{FromRef, Request}, http::StatusCode, response::{IntoResponse, Response}, }; use tower::{Layer, Service}; -use crate::{app::App, error::Internal}; +use crate::{error::Internal, setup::app::Setup}; #[derive(Clone)] -pub struct Required(pub App); +pub struct Required(pub App); -impl Required { - pub fn with_fallback(self, fallback: F) -> WithFallback { +impl Required { + pub fn with_fallback(self, fallback: F) -> WithFallback { let Self(app) = self; WithFallback { app, fallback } } } -impl Layer for Required { - type Service = Middleware; +impl Layer for Required +where + Self: Clone, +{ + type Service = Middleware; fn layer(&self, inner: S) -> Self::Service { let Self(app) = self.clone(); @@ -36,16 +39,16 @@ impl Layer for Required { } #[derive(Clone)] -pub struct WithFallback { +pub struct WithFallback { app: App, fallback: F, } -impl Layer for WithFallback +impl Layer for WithFallback where Self: Clone, { - type Service = Middleware; + type Service = Middleware; fn layer(&self, inner: S) -> Self::Service { let Self { app, fallback } = self.clone(); @@ -58,17 +61,19 @@ where } #[derive(Clone)] -pub struct Middleware { +pub struct Middleware { inner: S, app: App, fallback: F, } -impl Service for Middleware +impl Service for Middleware where + Setup: FromRef, Self: Clone, S: Service + Send + 'static, S::Future: Send, + App: Send + 'static, F: IntoResponse + Clone + Send + 'static, { type Response = S::Response; @@ -87,7 +92,7 @@ where } = self.clone(); Box::pin(async move { - match app.setup().completed().await { + match Setup::from_ref(&app).completed().await { Ok(true) => inner.call(req).await, Ok(false) => Ok(fallback.into_response()), Err(error) => Ok(Internal::from(error).into_response()), diff --git a/src/ui/handlers/setup.rs b/src/ui/handlers/setup.rs index 49821cf..5707765 100644 --- a/src/ui/handlers/setup.rs +++ b/src/ui/handlers/setup.rs @@ -4,14 +4,13 @@ use axum::{ }; use crate::{ - app::App, error::Internal, + setup::app::Setup, ui::assets::{Asset, Assets}, }; -pub async fn handler(State(app): State) -> Result { - if app - .setup() +pub async fn handler(State(setup): State) -> Result { + if setup .completed() .await .map_err(Internal::from) -- cgit v1.2.3