summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorojacobson <ojacobson@noreply.codeberg.org>2025-10-28 18:41:41 +0100
committerojacobson <ojacobson@noreply.codeberg.org>2025-10-28 18:41:41 +0100
commit0ef69c7d256380e660edc45ace7f1d6151226340 (patch)
treea6eea3af13f237393057aac1d80f024bc1b40b10
parent58e6496558a01052537c5272169aea3e79ccbc8e (diff)
parentdc7074bbf39aff895ba52abd5e7b7e9bb643cf27 (diff)
Use freestanding structs for `App` components.
A "component" is a struct that provides domain-specific operations on the service. `App` largely acts as a way to obtain components for domain-specific operations: for example, given `let app: App = todo!();`, then `app.tokens()` provides a component (`Tokens`) that supports operations on authentication tokens, and `app.conversations()` provides a component (`Conversations`) that supports operations on conversations. This has been a major piece of the server's internal organization for a long time. Historically, these components have been built as short-lived view structs, which hold onto their functional dependencies by reference. A given component was therefore bound to its source `App`, and had a lifetime bounded by the life of that `App` instance or reference. This change turns components into freestanding structs - that is, they can outlive the `App` that provided them. They hold their dependencies by value, not by reference; `App` provides clones when creating a component, instead of borrowing its own state. For the functional dependencies we have today, cloning is a supported and cheap way to share access; details are documented in the individual commits. I'm making this change because we're working on web push, and we discovered while prototyping that it will be useful to be able to support multiple distinct types of web push client. A running Pilcrow server would use a "real" client (which sends real HTTP requests to deliver push messages), while tests would use a client stub (which doesn't). However, to make that work, we'll need to make `App` generic over the client, and the resulting type parameter would then end up in every handler and in most other things that touch the `App` type. This refactoring dramatically reduces the number of places we mention the `App` type, by making most uses rely on specific components instead of relying on `App` generally. There are still a few places that work on `App` generally, rather than on specific components, because an operation requires the use of two or more components. I don't love all this cloning, even if I know in my head that it's fine. The alternatives that we looked at include: * Provider traits, as we have for `Transaction`, that allow endpoints to specify that they want any type that can provide a `Tokens` or a `Conversation` instead of specifically an `App` (`App<PushClient>`). This is wordy enough that we've opted to punt on that approach for now. * Accept the type parameter as the cost of doing business. This is still an open alternative. * Use `dyn` dispatch instead of a type parameter for the push client. This is still an open alternative, though not one I love as we'd be incurring function call indirection without getting any generality out of it. Merges freestanding-app-components into main.
-rw-r--r--src/app.rs79
-rw-r--r--src/boot/app.rs8
-rw-r--r--src/boot/handlers/boot/mod.rs9
-rw-r--r--src/boot/handlers/boot/test.rs16
-rw-r--r--src/conversation/app.rs10
-rw-r--r--src/conversation/handlers/create/mod.rs8
-rw-r--r--src/conversation/handlers/create/test.rs66
-rw-r--r--src/conversation/handlers/delete/mod.rs9
-rw-r--r--src/conversation/handlers/delete/test.rs12
-rw-r--r--src/conversation/handlers/send/mod.rs11
-rw-r--r--src/conversation/handlers/send/test.rs6
-rw-r--r--src/event/app.rs10
-rw-r--r--src/invite/app.rs12
-rw-r--r--src/invite/handlers/accept/mod.rs8
-rw-r--r--src/invite/handlers/accept/test.rs12
-rw-r--r--src/invite/handlers/get/mod.rs7
-rw-r--r--src/invite/handlers/get/test.rs6
-rw-r--r--src/invite/handlers/issue/mod.rs9
-rw-r--r--src/invite/handlers/issue/test.rs2
-rw-r--r--src/login/app.rs10
-rw-r--r--src/login/handlers/login/mod.rs14
-rw-r--r--src/login/handlers/login/test.rs8
-rw-r--r--src/login/handlers/logout/mod.rs9
-rw-r--r--src/login/handlers/logout/test.rs25
-rw-r--r--src/login/handlers/password/mod.rs8
-rw-r--r--src/login/handlers/password/test.rs2
-rw-r--r--src/message/app.rs10
-rw-r--r--src/message/handlers/delete/mod.rs10
-rw-r--r--src/message/handlers/delete/test.rs12
-rw-r--r--src/setup/app.rs13
-rw-r--r--src/setup/handlers/setup/mod.rs14
-rw-r--r--src/setup/handlers/setup/test.rs6
-rw-r--r--src/setup/required.rs31
-rw-r--r--src/test/fixtures/boot.rs11
-rw-r--r--src/test/fixtures/conversation.rs11
-rw-r--r--src/test/fixtures/cookie.rs14
-rw-r--r--src/test/fixtures/identity.rs21
-rw-r--r--src/test/fixtures/invite.rs12
-rw-r--r--src/test/fixtures/message.rs13
-rw-r--r--src/test/verify/identity.rs26
-rw-r--r--src/test/verify/login.rs23
-rw-r--r--src/test/verify/token.rs29
-rw-r--r--src/token/app.rs10
-rw-r--r--src/token/extract/identity.rs24
-rw-r--r--src/ui/handlers/conversation.rs7
-rw-r--r--src/ui/handlers/invite.rs6
-rw-r--r--src/ui/handlers/setup.rs7
-rw-r--r--src/user/app.rs12
48 files changed, 423 insertions, 275 deletions
diff --git a/src/app.rs b/src/app.rs
index e61672f..ad19bc0 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -1,3 +1,4 @@
+use axum::extract::FromRef;
use sqlx::sqlite::SqlitePool;
#[cfg(test)]
@@ -33,40 +34,82 @@ impl App {
}
impl App {
- pub const fn boot(&self) -> Boot<'_> {
- Boot::new(&self.db)
+ pub fn boot(&self) -> Boot {
+ Boot::new(self.db.clone())
}
- pub const fn conversations(&self) -> Conversations<'_> {
- Conversations::new(&self.db, &self.events)
+ pub fn conversations(&self) -> Conversations {
+ Conversations::new(self.db.clone(), self.events.clone())
}
- pub const fn events(&self) -> Events<'_> {
- Events::new(&self.db, &self.events)
+ pub fn events(&self) -> Events {
+ 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<'_> {
- Logins::new(&self.db, &self.token_events)
+ pub fn logins(&self) -> Logins {
+ Logins::new(self.db.clone(), self.token_events.clone())
}
- pub const fn messages(&self) -> Messages<'_> {
- Messages::new(&self.db, &self.events)
+ pub fn messages(&self) -> Messages {
+ 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<'_> {
- Tokens::new(&self.db, &self.token_events)
+ pub fn tokens(&self) -> Tokens {
+ Tokens::new(self.db.clone(), self.token_events.clone())
}
#[cfg(test)]
- pub const fn users(&self) -> Users<'_> {
- Users::new(&self.db, &self.events)
+ pub fn users(&self) -> Users {
+ Users::new(self.db.clone(), self.events.clone())
+ }
+}
+
+impl FromRef<App> for Boot {
+ fn from_ref(app: &App) -> Self {
+ app.boot()
+ }
+}
+
+impl FromRef<App> for Conversations {
+ fn from_ref(app: &App) -> Self {
+ app.conversations()
+ }
+}
+
+impl FromRef<App> for Invites {
+ fn from_ref(app: &App) -> Self {
+ app.invites()
+ }
+}
+
+impl FromRef<App> for Logins {
+ fn from_ref(app: &App) -> Self {
+ app.logins()
+ }
+}
+
+impl FromRef<App> for Messages {
+ fn from_ref(app: &App) -> Self {
+ app.messages()
+ }
+}
+
+impl FromRef<App> for Setup {
+ fn from_ref(app: &App) -> Self {
+ app.setup()
+ }
+}
+
+impl FromRef<App> for Tokens {
+ fn from_ref(app: &App) -> Self {
+ app.tokens()
}
}
diff --git a/src/boot/app.rs b/src/boot/app.rs
index 0ed5d1b..840243e 100644
--- a/src/boot/app.rs
+++ b/src/boot/app.rs
@@ -10,12 +10,12 @@ use crate::{
user::{self, repo::Provider as _},
};
-pub struct Boot<'a> {
- db: &'a SqlitePool,
+pub struct Boot {
+ db: SqlitePool,
}
-impl<'a> Boot<'a> {
- pub const fn new(db: &'a SqlitePool) -> Self {
+impl Boot {
+ pub const fn new(db: SqlitePool) -> Self {
Self { db }
}
diff --git a/src/boot/handlers/boot/mod.rs b/src/boot/handlers/boot/mod.rs
index 3e022b1..5ff7802 100644
--- a/src/boot/handlers/boot/mod.rs
+++ b/src/boot/handlers/boot/mod.rs
@@ -7,15 +7,18 @@ use axum::{
use serde::Serialize;
use crate::{
- app::App, boot::Snapshot, error::Internal, event::Heartbeat, login::Login,
+ boot::{Snapshot, app::Boot},
+ error::Internal,
+ event::Heartbeat,
+ login::Login,
token::extract::Identity,
};
#[cfg(test)]
mod test;
-pub async fn handler(State(app): State<App>, identity: Identity) -> Result<Response, Internal> {
- let snapshot = app.boot().snapshot().await?;
+pub async fn handler(State(boot): State<Boot>, identity: Identity) -> Result<Response, Internal> {
+ let snapshot = boot.snapshot().await?;
let heartbeat = Heartbeat::TIMEOUT;
Ok(Response {
diff --git a/src/boot/handlers/boot/test.rs b/src/boot/handlers/boot/test.rs
index cb50442..a9891eb 100644
--- a/src/boot/handlers/boot/test.rs
+++ b/src/boot/handlers/boot/test.rs
@@ -8,7 +8,7 @@ async fn returns_identity() {
let app = fixtures::scratch_app().await;
let viewer = fixtures::identity::fictitious();
- let response = super::handler(State(app), viewer.clone())
+ let response = super::handler(State(app.boot()), viewer.clone())
.await
.expect("boot always succeeds");
@@ -21,7 +21,7 @@ async fn includes_users() {
let spectator = fixtures::user::create(&app, &fixtures::now()).await;
let viewer = fixtures::identity::fictitious();
- let response = super::handler(State(app), viewer)
+ let response = super::handler(State(app.boot()), viewer)
.await
.expect("boot always succeeds");
@@ -42,7 +42,7 @@ async fn includes_conversations() {
let conversation = fixtures::conversation::create(&app, &fixtures::now()).await;
let viewer = fixtures::identity::fictitious();
- let response = super::handler(State(app), viewer)
+ let response = super::handler(State(app.boot()), viewer)
.await
.expect("boot always succeeds");
@@ -65,7 +65,7 @@ async fn includes_messages() {
let message = fixtures::message::send(&app, &conversation, &sender, &fixtures::now()).await;
let viewer = fixtures::identity::fictitious();
- let response = super::handler(State(app), viewer)
+ let response = super::handler(State(app.boot()), viewer)
.await
.expect("boot always succeeds");
@@ -94,7 +94,7 @@ async fn includes_expired_messages() {
.expect("expiry never fails");
let viewer = fixtures::identity::fictitious();
- let response = super::handler(State(app), viewer)
+ let response = super::handler(State(app.boot()), viewer)
.await
.expect("boot always succeeds");
@@ -136,7 +136,7 @@ async fn includes_deleted_messages() {
.expect("deleting valid message succeeds");
let viewer = fixtures::identity::fictitious();
- let response = super::handler(State(app), viewer)
+ let response = super::handler(State(app.boot()), viewer)
.await
.expect("boot always succeeds");
@@ -175,7 +175,7 @@ async fn includes_expired_conversations() {
.expect("expiry never fails");
let viewer = fixtures::identity::fictitious();
- let response = super::handler(State(app), viewer)
+ let response = super::handler(State(app.boot()), viewer)
.await
.expect("boot always succeeds");
@@ -214,7 +214,7 @@ async fn includes_deleted_conversations() {
.expect("deleting a valid conversation succeeds");
let viewer = fixtures::identity::fictitious();
- let response = super::handler(State(app), viewer)
+ let response = super::handler(State(app.boot()), viewer)
.await
.expect("boot always succeeds");
diff --git a/src/conversation/app.rs b/src/conversation/app.rs
index 26886af..2b62e77 100644
--- a/src/conversation/app.rs
+++ b/src/conversation/app.rs
@@ -15,13 +15,13 @@ use crate::{
name::{self, Name},
};
-pub struct Conversations<'a> {
- db: &'a SqlitePool,
- events: &'a Broadcaster,
+pub struct Conversations {
+ db: SqlitePool,
+ events: Broadcaster,
}
-impl<'a> Conversations<'a> {
- pub const fn new(db: &'a SqlitePool, events: &'a Broadcaster) -> Self {
+impl Conversations {
+ pub const fn new(db: SqlitePool, events: Broadcaster) -> Self {
Self { db, events }
}
diff --git a/src/conversation/handlers/create/mod.rs b/src/conversation/handlers/create/mod.rs
index 18eca1f..2b7fa39 100644
--- a/src/conversation/handlers/create/mod.rs
+++ b/src/conversation/handlers/create/mod.rs
@@ -5,9 +5,8 @@ use axum::{
};
use crate::{
- app::App,
clock::RequestedAt,
- conversation::{Conversation, app},
+ conversation::{Conversation, app, app::Conversations},
error::Internal,
name::Name,
token::extract::Identity,
@@ -17,13 +16,12 @@ use crate::{
mod test;
pub async fn handler(
- State(app): State<App>,
+ State(conversations): State<Conversations>,
_: Identity, // requires auth, but doesn't actually care who you are
RequestedAt(created_at): RequestedAt,
Json(request): Json<Request>,
) -> Result<Response, Error> {
- let conversation = app
- .conversations()
+ let conversation = conversations
.create(&request.name, &created_at)
.await
.map_err(Error)?;
diff --git a/src/conversation/handlers/create/test.rs b/src/conversation/handlers/create/test.rs
index bc05b00..380bb13 100644
--- a/src/conversation/handlers/create/test.rs
+++ b/src/conversation/handlers/create/test.rs
@@ -22,10 +22,14 @@ async fn new_conversation() {
let name = fixtures::conversation::propose();
let request = super::Request { name: name.clone() };
- let super::Response(response) =
- super::handler(State(app.clone()), creator, fixtures::now(), Json(request))
- .await
- .expect("creating a conversation in an empty app succeeds");
+ let super::Response(response) = super::handler(
+ State(app.conversations()),
+ creator,
+ fixtures::now(),
+ Json(request),
+ )
+ .await
+ .expect("creating a conversation in an empty app succeeds");
// Verify the structure of the response
@@ -77,10 +81,14 @@ async fn duplicate_name() {
let request = super::Request {
name: conversation.name.clone(),
};
- let super::Error(error) =
- super::handler(State(app.clone()), creator, fixtures::now(), Json(request))
- .await
- .expect_err("duplicate conversation name should fail the request");
+ let super::Error(error) = super::handler(
+ State(app.conversations()),
+ creator,
+ fixtures::now(),
+ Json(request),
+ )
+ .await
+ .expect_err("duplicate conversation name should fail the request");
// Verify the structure of the response
@@ -110,10 +118,14 @@ async fn conflicting_canonical_name() {
let request = super::Request {
name: conflicting_name.clone(),
};
- let super::Error(error) =
- super::handler(State(app.clone()), creator, fixtures::now(), Json(request))
- .await
- .expect_err("duplicate conversation name should fail the request");
+ let super::Error(error) = super::handler(
+ State(app.conversations()),
+ creator,
+ fixtures::now(),
+ Json(request),
+ )
+ .await
+ .expect_err("duplicate conversation name should fail the request");
// Verify the structure of the response
@@ -135,7 +147,7 @@ async fn invalid_name() {
let name = fixtures::conversation::propose_invalid_name();
let request = super::Request { name: name.clone() };
let super::Error(error) = crate::conversation::handlers::create::handler(
- State(app.clone()),
+ State(app.conversations()),
creator,
fixtures::now(),
Json(request),
@@ -163,7 +175,7 @@ async fn name_reusable_after_delete() {
let request = super::Request { name: name.clone() };
let super::Response(response) = super::handler(
- State(app.clone()),
+ State(app.conversations()),
creator.clone(),
fixtures::now(),
Json(request),
@@ -181,10 +193,14 @@ async fn name_reusable_after_delete() {
// Call the endpoint (second time)
let request = super::Request { name: name.clone() };
- let super::Response(response) =
- super::handler(State(app.clone()), creator, fixtures::now(), Json(request))
- .await
- .expect("creation succeeds after original conversation deleted");
+ let super::Response(response) = super::handler(
+ State(app.conversations()),
+ creator,
+ fixtures::now(),
+ Json(request),
+ )
+ .await
+ .expect("creation succeeds after original conversation deleted");
// Verify the structure of the response
@@ -212,7 +228,7 @@ async fn name_reusable_after_expiry() {
let request = super::Request { name: name.clone() };
let super::Response(_) = super::handler(
- State(app.clone()),
+ State(app.conversations()),
creator.clone(),
fixtures::ancient(),
Json(request),
@@ -230,10 +246,14 @@ async fn name_reusable_after_expiry() {
// Call the endpoint (second time)
let request = super::Request { name: name.clone() };
- let super::Response(response) =
- super::handler(State(app.clone()), creator, fixtures::now(), Json(request))
- .await
- .expect("creation succeeds after original conversation expired");
+ let super::Response(response) = super::handler(
+ State(app.conversations()),
+ creator,
+ fixtures::now(),
+ Json(request),
+ )
+ .await
+ .expect("creation succeeds after original conversation expired");
// Verify the structure of the response
diff --git a/src/conversation/handlers/delete/mod.rs b/src/conversation/handlers/delete/mod.rs
index 272165a..231e433 100644
--- a/src/conversation/handlers/delete/mod.rs
+++ b/src/conversation/handlers/delete/mod.rs
@@ -5,9 +5,8 @@ use axum::{
};
use crate::{
- app::App,
clock::RequestedAt,
- conversation::{self, app, handlers::PathInfo},
+ conversation::{self, app, app::Conversations, handlers::PathInfo},
error::{Internal, NotFound},
token::extract::Identity,
};
@@ -16,14 +15,12 @@ use crate::{
mod test;
pub async fn handler(
- State(app): State<App>,
+ State(conversations): State<Conversations>,
Path(conversation): Path<PathInfo>,
RequestedAt(deleted_at): RequestedAt,
_: Identity,
) -> Result<Response, Error> {
- app.conversations()
- .delete(&conversation, &deleted_at)
- .await?;
+ conversations.delete(&conversation, &deleted_at).await?;
Ok(Response { id: conversation })
}
diff --git a/src/conversation/handlers/delete/test.rs b/src/conversation/handlers/delete/test.rs
index 2718d3b..e9e882a 100644
--- a/src/conversation/handlers/delete/test.rs
+++ b/src/conversation/handlers/delete/test.rs
@@ -14,7 +14,7 @@ pub async fn valid_conversation() {
let deleter = fixtures::identity::create(&app, &fixtures::now()).await;
let response = super::handler(
- State(app.clone()),
+ State(app.conversations()),
Path(conversation.id.clone()),
fixtures::now(),
deleter,
@@ -52,7 +52,7 @@ pub async fn invalid_conversation_id() {
let deleter = fixtures::identity::create(&app, &fixtures::now()).await;
let conversation = fixtures::conversation::fictitious();
let super::Error(error) = super::handler(
- State(app.clone()),
+ State(app.conversations()),
Path(conversation.clone()),
fixtures::now(),
deleter,
@@ -81,7 +81,7 @@ pub async fn conversation_deleted() {
let deleter = fixtures::identity::create(&app, &fixtures::now()).await;
let super::Error(error) = super::handler(
- State(app.clone()),
+ State(app.conversations()),
Path(conversation.id.clone()),
fixtures::now(),
deleter,
@@ -110,7 +110,7 @@ pub async fn conversation_expired() {
let deleter = fixtures::identity::create(&app, &fixtures::now()).await;
let super::Error(error) = super::handler(
- State(app.clone()),
+ State(app.conversations()),
Path(conversation.id.clone()),
fixtures::now(),
deleter,
@@ -144,7 +144,7 @@ pub async fn conversation_purged() {
let deleter = fixtures::identity::create(&app, &fixtures::now()).await;
let super::Error(error) = super::handler(
- State(app.clone()),
+ State(app.conversations()),
Path(conversation.id.clone()),
fixtures::now(),
deleter,
@@ -170,7 +170,7 @@ pub async fn conversation_not_empty() {
let deleter = fixtures::identity::create(&app, &fixtures::now()).await;
let super::Error(error) = super::handler(
- State(app.clone()),
+ State(app.conversations()),
Path(conversation.id.clone()),
fixtures::now(),
deleter,
diff --git a/src/conversation/handlers/send/mod.rs b/src/conversation/handlers/send/mod.rs
index c8be59c..ff63652 100644
--- a/src/conversation/handlers/send/mod.rs
+++ b/src/conversation/handlers/send/mod.rs
@@ -5,11 +5,13 @@ use axum::{
};
use crate::{
- app::App,
clock::RequestedAt,
conversation::handlers::PathInfo,
error::{Internal, NotFound},
- message::{Body, Message, app::SendError},
+ message::{
+ Body, Message,
+ app::{Messages, SendError},
+ },
token::extract::Identity,
};
@@ -17,14 +19,13 @@ use crate::{
mod test;
pub async fn handler(
- State(app): State<App>,
+ State(messages): State<Messages>,
Path(conversation): Path<PathInfo>,
RequestedAt(sent_at): RequestedAt,
identity: Identity,
Json(request): Json<Request>,
) -> Result<Response, Error> {
- let message = app
- .messages()
+ let message = messages
.send(&conversation, &identity.login, &sent_at, &request.body)
.await?;
diff --git a/src/conversation/handlers/send/test.rs b/src/conversation/handlers/send/test.rs
index 8863090..013aaa4 100644
--- a/src/conversation/handlers/send/test.rs
+++ b/src/conversation/handlers/send/test.rs
@@ -28,7 +28,7 @@ async fn messages_in_order() {
let request = super::Request { body: body.clone() };
let _ = super::handler(
- State(app.clone()),
+ State(app.messages()),
Path(conversation.id.clone()),
sent_at.clone(),
sender.clone(),
@@ -75,7 +75,7 @@ async fn nonexistent_conversation() {
body: fixtures::message::propose(),
};
let super::Error(error) = super::handler(
- State(app),
+ State(app.messages()),
Path(conversation.clone()),
sent_at,
sender,
@@ -112,7 +112,7 @@ async fn deleted_conversation() {
body: fixtures::message::propose(),
};
let super::Error(error) = super::handler(
- State(app),
+ State(app.messages()),
Path(conversation.id.clone()),
sent_at,
sender,
diff --git a/src/event/app.rs b/src/event/app.rs
index 7359bfb..8fa760a 100644
--- a/src/event/app.rs
+++ b/src/event/app.rs
@@ -13,13 +13,13 @@ use crate::{
user::{self, repo::Provider as _},
};
-pub struct Events<'a> {
- db: &'a SqlitePool,
- events: &'a Broadcaster,
+pub struct Events {
+ db: SqlitePool,
+ events: Broadcaster,
}
-impl<'a> Events<'a> {
- pub const fn new(db: &'a SqlitePool, events: &'a Broadcaster) -> Self {
+impl Events {
+ pub const fn new(db: SqlitePool, events: Broadcaster) -> Self {
Self { db, events }
}
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<App>,
+ State(invites): State<Invites>,
RequestedAt(accepted_at): RequestedAt,
identity: IdentityCookie,
Path(invite): Path<PathInfo>,
Json(request): Json<Request>,
) -> 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<App>,
+ State(invites): State<Invites>,
Path(invite): Path<PathInfo>,
) -> Result<Json<Summary>, 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<App>,
+ State(invites): State<Invites>,
RequestedAt(issued_at): RequestedAt,
identity: Identity,
_: Json<Request>,
) -> Result<Json<Invite>, 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/login/app.rs b/src/login/app.rs
index e471000..a2f9636 100644
--- a/src/login/app.rs
+++ b/src/login/app.rs
@@ -9,13 +9,13 @@ use crate::{
token::{Broadcaster, Event as TokenEvent, Secret, Token, repo::Provider as _},
};
-pub struct Logins<'a> {
- db: &'a SqlitePool,
- token_events: &'a Broadcaster,
+pub struct Logins {
+ db: SqlitePool,
+ token_events: Broadcaster,
}
-impl<'a> Logins<'a> {
- pub const fn new(db: &'a SqlitePool, token_events: &'a Broadcaster) -> Self {
+impl Logins {
+ pub const fn new(db: SqlitePool, token_events: Broadcaster) -> Self {
Self { db, token_events }
}
diff --git a/src/login/handlers/login/mod.rs b/src/login/handlers/login/mod.rs
index 6591984..2ce8a67 100644
--- a/src/login/handlers/login/mod.rs
+++ b/src/login/handlers/login/mod.rs
@@ -5,21 +5,25 @@ use axum::{
};
use crate::{
- app::App, clock::RequestedAt, empty::Empty, error::Internal, login::app, name::Name,
- password::Password, token::extract::IdentityCookie,
+ clock::RequestedAt,
+ empty::Empty,
+ error::Internal,
+ login::{app, app::Logins},
+ name::Name,
+ password::Password,
+ token::extract::IdentityCookie,
};
#[cfg(test)]
mod test;
pub async fn handler(
- State(app): State<App>,
+ State(logins): State<Logins>,
RequestedAt(now): RequestedAt,
identity: IdentityCookie,
Json(request): Json<Request>,
) -> Result<(IdentityCookie, Empty), Error> {
- let secret = app
- .logins()
+ let secret = logins
.with_password(&request.name, &request.password, &now)
.await
.map_err(Error)?;
diff --git a/src/login/handlers/login/test.rs b/src/login/handlers/login/test.rs
index f3911d0..7bb56b6 100644
--- a/src/login/handlers/login/test.rs
+++ b/src/login/handlers/login/test.rs
@@ -22,7 +22,7 @@ async fn correct_credentials() {
password,
};
let (identity, Empty) =
- super::handler(State(app.clone()), logged_in_at, identity, Json(request))
+ super::handler(State(app.logins()), logged_in_at, identity, Json(request))
.await
.expect("logged in with valid credentials");
@@ -52,7 +52,7 @@ async fn invalid_name() {
password,
};
let super::Error(error) =
- super::handler(State(app.clone()), logged_in_at, identity, Json(request))
+ super::handler(State(app.logins()), logged_in_at, identity, Json(request))
.await
.expect_err("logged in with an incorrect password fails");
@@ -77,7 +77,7 @@ async fn incorrect_password() {
password: fixtures::user::propose_password(),
};
let super::Error(error) =
- super::handler(State(app.clone()), logged_in_at, identity, Json(request))
+ super::handler(State(app.logins()), logged_in_at, identity, Json(request))
.await
.expect_err("logged in with an incorrect password");
@@ -98,7 +98,7 @@ async fn token_expires() {
let logged_in_at = fixtures::ancient();
let identity = fixtures::cookie::not_logged_in();
let request = super::Request { name, password };
- let (identity, _) = super::handler(State(app.clone()), logged_in_at, identity, Json(request))
+ let (identity, _) = super::handler(State(app.logins()), logged_in_at, identity, Json(request))
.await
.expect("logged in with valid credentials");
let secret = identity.secret().expect("logged in with valid credentials");
diff --git a/src/login/handlers/logout/mod.rs b/src/login/handlers/logout/mod.rs
index 73efe73..ce4cb1a 100644
--- a/src/login/handlers/logout/mod.rs
+++ b/src/login/handlers/logout/mod.rs
@@ -4,25 +4,24 @@ use axum::{
};
use crate::{
- app::App,
clock::RequestedAt,
empty::Empty,
error::{Internal, Unauthorized},
- token::{app, extract::IdentityCookie},
+ token::{app, app::Tokens, extract::IdentityCookie},
};
#[cfg(test)]
mod test;
pub async fn handler(
- State(app): State<App>,
+ State(tokens): State<Tokens>,
RequestedAt(now): RequestedAt,
identity: IdentityCookie,
Json(_): Json<Request>,
) -> Result<(IdentityCookie, Empty), Error> {
if let Some(secret) = identity.secret() {
- let identity = app.tokens().validate(&secret, &now).await?;
- app.tokens().logout(&identity.token).await?;
+ let identity = tokens.validate(&secret, &now).await?;
+ tokens.logout(&identity.token).await?;
}
let identity = identity.clear();
diff --git a/src/login/handlers/logout/test.rs b/src/login/handlers/logout/test.rs
index e7b7dd4..18744ed 100644
--- a/src/login/handlers/logout/test.rs
+++ b/src/login/handlers/logout/test.rs
@@ -18,7 +18,7 @@ async fn successful() {
// Call the endpoint
let (response_identity, Empty) = super::handler(
- State(app.clone()),
+ State(app.tokens()),
fixtures::now(),
identity.clone(),
Json::default(),
@@ -42,9 +42,14 @@ async fn no_identity() {
// Call the endpoint
let identity = fixtures::cookie::not_logged_in();
- let (identity, Empty) = super::handler(State(app), fixtures::now(), identity, Json::default())
- .await
- .expect("logged out with no token succeeds");
+ let (identity, Empty) = super::handler(
+ State(app.tokens()),
+ fixtures::now(),
+ identity,
+ Json::default(),
+ )
+ .await
+ .expect("logged out with no token succeeds");
// Verify the return value's basic structure
@@ -60,10 +65,14 @@ async fn invalid_token() {
// Call the endpoint
let identity = fixtures::cookie::fictitious();
- let super::Error(error) =
- super::handler(State(app), fixtures::now(), identity, Json::default())
- .await
- .expect_err("logged out with an invalid token fails");
+ let super::Error(error) = super::handler(
+ State(app.tokens()),
+ fixtures::now(),
+ identity,
+ Json::default(),
+ )
+ .await
+ .expect_err("logged out with an invalid token fails");
// Verify the return value's basic structure
diff --git a/src/login/handlers/password/mod.rs b/src/login/handlers/password/mod.rs
index 94c7fb4..8b82605 100644
--- a/src/login/handlers/password/mod.rs
+++ b/src/login/handlers/password/mod.rs
@@ -5,11 +5,10 @@ use axum::{
};
use crate::{
- app::App,
clock::RequestedAt,
empty::Empty,
error::Internal,
- login::app,
+ login::{app, app::Logins},
password::Password,
token::extract::{Identity, IdentityCookie},
};
@@ -18,14 +17,13 @@ use crate::{
mod test;
pub async fn handler(
- State(app): State<App>,
+ State(logins): State<Logins>,
RequestedAt(now): RequestedAt,
identity: Identity,
cookie: IdentityCookie,
Json(request): Json<Request>,
) -> Result<(IdentityCookie, Empty), Error> {
- let secret = app
- .logins()
+ let secret = logins
.change_password(&identity.login, &request.password, &request.to, &now)
.await
.map_err(Error)?;
diff --git a/src/login/handlers/password/test.rs b/src/login/handlers/password/test.rs
index ba2f28f..61d5b5a 100644
--- a/src/login/handlers/password/test.rs
+++ b/src/login/handlers/password/test.rs
@@ -21,7 +21,7 @@ async fn password_change() {
to: to.clone(),
};
let (new_cookie, Empty) = super::handler(
- State(app.clone()),
+ State(app.logins()),
fixtures::now(),
identity.clone(),
cookie.clone(),
diff --git a/src/message/app.rs b/src/message/app.rs
index 647152e..cbcbff9 100644
--- a/src/message/app.rs
+++ b/src/message/app.rs
@@ -13,13 +13,13 @@ use crate::{
user::{self, repo::Provider as _},
};
-pub struct Messages<'a> {
- db: &'a SqlitePool,
- events: &'a Broadcaster,
+pub struct Messages {
+ db: SqlitePool,
+ events: Broadcaster,
}
-impl<'a> Messages<'a> {
- pub const fn new(db: &'a SqlitePool, events: &'a Broadcaster) -> Self {
+impl Messages {
+ pub const fn new(db: SqlitePool, events: Broadcaster) -> Self {
Self { db, events }
}
diff --git a/src/message/handlers/delete/mod.rs b/src/message/handlers/delete/mod.rs
index 3e9a212..c09a752 100644
--- a/src/message/handlers/delete/mod.rs
+++ b/src/message/handlers/delete/mod.rs
@@ -5,10 +5,12 @@ use axum::{
};
use crate::{
- app::App,
clock::RequestedAt,
error::{Internal, NotFound},
- message::{self, app::DeleteError},
+ message::{
+ self,
+ app::{DeleteError, Messages},
+ },
token::extract::Identity,
};
@@ -16,12 +18,12 @@ use crate::{
mod test;
pub async fn handler(
- State(app): State<App>,
+ State(messages): State<Messages>,
Path(message): Path<message::Id>,
RequestedAt(deleted_at): RequestedAt,
identity: Identity,
) -> Result<Response, Error> {
- app.messages()
+ messages
.delete(&identity.login, &message, &deleted_at)
.await?;
diff --git a/src/message/handlers/delete/test.rs b/src/message/handlers/delete/test.rs
index 05d9344..198728b 100644
--- a/src/message/handlers/delete/test.rs
+++ b/src/message/handlers/delete/test.rs
@@ -16,7 +16,7 @@ pub async fn delete_message() {
// Send the request
let response = super::handler(
- State(app.clone()),
+ State(app.messages()),
Path(message.id.clone()),
fixtures::now(),
sender,
@@ -52,7 +52,7 @@ pub async fn delete_invalid_message_id() {
let deleter = fixtures::identity::create(&app, &fixtures::now()).await;
let message = fixtures::message::fictitious();
let super::Error(error) = super::handler(
- State(app.clone()),
+ State(app.messages()),
Path(message.clone()),
fixtures::now(),
deleter,
@@ -83,7 +83,7 @@ pub async fn delete_deleted() {
// Send the request
let super::Error(error) = super::handler(
- State(app.clone()),
+ State(app.messages()),
Path(message.id.clone()),
fixtures::now(),
sender,
@@ -114,7 +114,7 @@ pub async fn delete_expired() {
// Send the request
let super::Error(error) = super::handler(
- State(app.clone()),
+ State(app.messages()),
Path(message.id.clone()),
fixtures::now(),
sender,
@@ -150,7 +150,7 @@ pub async fn delete_purged() {
let deleter = fixtures::identity::create(&app, &fixtures::now()).await;
let super::Error(error) = super::handler(
- State(app.clone()),
+ State(app.messages()),
Path(message.id.clone()),
fixtures::now(),
deleter,
@@ -176,7 +176,7 @@ pub async fn delete_not_sender() {
let deleter = fixtures::identity::create(&app, &fixtures::now()).await;
let super::Error(error) = super::handler(
- State(app.clone()),
+ State(app.messages()),
Path(message.id.clone()),
fixtures::now(),
deleter.clone(),
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<App>,
+ State(setup): State<Setup>,
RequestedAt(setup_at): RequestedAt,
identity: IdentityCookie,
Json(request): Json<Request>,
) -> 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<App>(pub App);
-impl Required {
- pub fn with_fallback<F>(self, fallback: F) -> WithFallback<F> {
+impl<App> Required<App> {
+ pub fn with_fallback<F>(self, fallback: F) -> WithFallback<App, F> {
let Self(app) = self;
WithFallback { app, fallback }
}
}
-impl<S> Layer<S> for Required {
- type Service = Middleware<S, Unavailable>;
+impl<S, App> Layer<S> for Required<App>
+where
+ Self: Clone,
+{
+ type Service = Middleware<S, App, Unavailable>;
fn layer(&self, inner: S) -> Self::Service {
let Self(app) = self.clone();
@@ -36,16 +39,16 @@ impl<S> Layer<S> for Required {
}
#[derive(Clone)]
-pub struct WithFallback<F> {
+pub struct WithFallback<App, F> {
app: App,
fallback: F,
}
-impl<S, F> Layer<S> for WithFallback<F>
+impl<S, App, F> Layer<S> for WithFallback<App, F>
where
Self: Clone,
{
- type Service = Middleware<S, F>;
+ type Service = Middleware<S, App, F>;
fn layer(&self, inner: S) -> Self::Service {
let Self { app, fallback } = self.clone();
@@ -58,17 +61,19 @@ where
}
#[derive(Clone)]
-pub struct Middleware<S, F> {
+pub struct Middleware<S, App, F> {
inner: S,
app: App,
fallback: F,
}
-impl<S, F> Service<Request> for Middleware<S, F>
+impl<S, App, F> Service<Request> for Middleware<S, App, F>
where
+ Setup: FromRef<App>,
Self: Clone,
S: Service<Request, Response = Response> + 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/test/fixtures/boot.rs b/src/test/fixtures/boot.rs
index 120726f..7421512 100644
--- a/src/test/fixtures/boot.rs
+++ b/src/test/fixtures/boot.rs
@@ -1,7 +1,12 @@
-use crate::{app::App, event::Sequence};
+use axum::extract::FromRef;
-pub async fn resume_point(app: &App) -> Sequence {
- app.boot()
+use crate::{boot::app::Boot, event::Sequence};
+
+pub async fn resume_point<App>(app: &App) -> Sequence
+where
+ Boot: FromRef<App>,
+{
+ Boot::from_ref(app)
.snapshot()
.await
.expect("boot always succeeds")
diff --git a/src/test/fixtures/conversation.rs b/src/test/fixtures/conversation.rs
index fb2f58d..f0d8c8c 100644
--- a/src/test/fixtures/conversation.rs
+++ b/src/test/fixtures/conversation.rs
@@ -1,3 +1,4 @@
+use axum::extract::FromRef;
use faker_rand::{
en_us::{addresses::CityName, names::FullName},
faker_impl_from_templates,
@@ -6,15 +7,17 @@ use faker_rand::{
use rand;
use crate::{
- app::App,
clock::RequestedAt,
- conversation::{self, Conversation},
+ conversation::{self, Conversation, app::Conversations},
name::Name,
};
-pub async fn create(app: &App, created_at: &RequestedAt) -> Conversation {
+pub async fn create<App>(app: &App, created_at: &RequestedAt) -> Conversation
+where
+ Conversations: FromRef<App>,
+{
let name = propose();
- app.conversations()
+ Conversations::from_ref(app)
.create(&name, created_at)
.await
.expect("should always succeed if the conversation is actually new")
diff --git a/src/test/fixtures/cookie.rs b/src/test/fixtures/cookie.rs
index 7dc5083..0b5ec9b 100644
--- a/src/test/fixtures/cookie.rs
+++ b/src/test/fixtures/cookie.rs
@@ -1,21 +1,25 @@
+use axum::extract::FromRef;
use uuid::Uuid;
use crate::{
- app::App, clock::RequestedAt, name::Name, password::Password, token::extract::IdentityCookie,
+ clock::RequestedAt, login::app::Logins, name::Name, password::Password,
+ token::extract::IdentityCookie,
};
pub fn not_logged_in() -> IdentityCookie {
IdentityCookie::new()
}
-pub async fn logged_in(
+pub async fn logged_in<App>(
app: &App,
credentials: &(Name, Password),
now: &RequestedAt,
-) -> IdentityCookie {
+) -> IdentityCookie
+where
+ Logins: FromRef<App>,
+{
let (name, password) = credentials;
- let secret = app
- .logins()
+ let secret = Logins::from_ref(app)
.with_password(name, password, now)
.await
.expect("should succeed given known-valid credentials");
diff --git a/src/test/fixtures/identity.rs b/src/test/fixtures/identity.rs
index 93e4a38..20929f9 100644
--- a/src/test/fixtures/identity.rs
+++ b/src/test/fixtures/identity.rs
@@ -1,11 +1,15 @@
+use axum::extract::FromRef;
+
use crate::{
app::App,
clock::RequestedAt,
+ login::app::Logins,
name::Name,
password::Password,
test::fixtures,
token::{
Token,
+ app::Tokens,
extract::{Identity, IdentityCookie},
},
};
@@ -15,23 +19,30 @@ pub async fn create(app: &App, created_at: &RequestedAt) -> Identity {
logged_in(app, &credentials, created_at).await
}
-pub async fn from_cookie(
+pub async fn from_cookie<App>(
app: &App,
cookie: &IdentityCookie,
validated_at: &RequestedAt,
-) -> Identity {
+) -> Identity
+where
+ Tokens: FromRef<App>,
+{
let secret = cookie.secret().expect("identity token has a secret");
- app.tokens()
+ Tokens::from_ref(app)
.validate(&secret, validated_at)
.await
.expect("always validates newly-issued secret")
}
-pub async fn logged_in(
+pub async fn logged_in<App>(
app: &App,
credentials: &(Name, Password),
issued_at: &RequestedAt,
-) -> Identity {
+) -> Identity
+where
+ Tokens: FromRef<App>,
+ Logins: FromRef<App>,
+{
let secret = fixtures::cookie::logged_in(app, credentials, issued_at).await;
from_cookie(app, &secret, issued_at).await
}
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: &App, issuer: &Login, issued_at: &DateTime) -> Invite
+where
+ Invites: FromRef<App>,
+{
+ Invites::from_ref(app)
.issue(issuer, issued_at)
.await
.expect("issuing invites never fails")
diff --git a/src/test/fixtures/message.rs b/src/test/fixtures/message.rs
index 92ac1f5..0bd0b7a 100644
--- a/src/test/fixtures/message.rs
+++ b/src/test/fixtures/message.rs
@@ -1,22 +1,25 @@
+use axum::extract::FromRef;
use faker_rand::lorem::Paragraphs;
use crate::{
- app::App,
clock::RequestedAt,
conversation::Conversation,
login::Login,
- message::{self, Body, Message},
+ message::{self, Body, Message, app::Messages},
};
-pub async fn send(
+pub async fn send<App>(
app: &App,
conversation: &Conversation,
sender: &Login,
sent_at: &RequestedAt,
-) -> Message {
+) -> Message
+where
+ Messages: FromRef<App>,
+{
let body = propose();
- app.messages()
+ Messages::from_ref(app)
.send(&conversation.id, sender, sent_at, &body)
.await
.expect("should succeed if the conversation exists")
diff --git a/src/test/verify/identity.rs b/src/test/verify/identity.rs
index 8e2d36e..fba2a4d 100644
--- a/src/test/verify/identity.rs
+++ b/src/test/verify/identity.rs
@@ -1,31 +1,43 @@
+use axum::extract::FromRef;
+
use crate::{
- app::App,
login::Login,
name::Name,
test::{fixtures, verify},
- token::{app::ValidateError, extract::IdentityCookie},
+ token::{
+ app::{Tokens, ValidateError},
+ extract::IdentityCookie,
+ },
};
-pub async fn valid_for_name(app: &App, identity: &IdentityCookie, name: &Name) {
+pub async fn valid_for_name<App>(app: &App, identity: &IdentityCookie, name: &Name)
+where
+ Tokens: FromRef<App>,
+{
let secret = identity
.secret()
.expect("identity cookie must be set to be valid");
verify::token::valid_for_name(app, &secret, name).await;
}
-pub async fn valid_for_login(app: &App, identity: &IdentityCookie, login: &Login) {
+pub async fn valid_for_login<App>(app: &App, identity: &IdentityCookie, login: &Login)
+where
+ Tokens: FromRef<App>,
+{
let secret = identity
.secret()
.expect("identity cookie must be set to be valid");
verify::token::valid_for_login(app, &secret, login).await;
}
-pub async fn invalid(app: &App, identity: &IdentityCookie) {
+pub async fn invalid<App>(app: &App, identity: &IdentityCookie)
+where
+ Tokens: FromRef<App>,
+{
let secret = identity
.secret()
.expect("identity cookie must be set to be invalid");
- let validate_err = app
- .tokens()
+ let validate_err = Tokens::from_ref(app)
.validate(&secret, &fixtures::now())
.await
.expect_err("identity cookie secret must be invalid");
diff --git a/src/test/verify/login.rs b/src/test/verify/login.rs
index ae2e91e..aad01bc 100644
--- a/src/test/verify/login.rs
+++ b/src/test/verify/login.rs
@@ -1,23 +1,30 @@
+use axum::extract::FromRef;
+
use crate::{
- app::App,
- login::app::LoginError,
+ login::app::{LoginError, Logins},
name::Name,
password::Password,
test::{fixtures, verify},
+ token::app::Tokens,
};
-pub async fn valid_login(app: &App, name: &Name, password: &Password) {
- let secret = app
- .logins()
+pub async fn valid_login<App>(app: &App, name: &Name, password: &Password)
+where
+ Logins: FromRef<App>,
+ Tokens: FromRef<App>,
+{
+ let secret = Logins::from_ref(app)
.with_password(name, password, &fixtures::now())
.await
.expect("login credentials expected to be valid");
verify::token::valid_for_name(&app, &secret, &name).await;
}
-pub async fn invalid_login(app: &App, name: &Name, password: &Password) {
- let error = app
- .logins()
+pub async fn invalid_login<App>(app: &App, name: &Name, password: &Password)
+where
+ Logins: FromRef<App>,
+{
+ let error = Logins::from_ref(app)
.with_password(name, password, &fixtures::now())
.await
.expect_err("login credentials expected not to be valid");
diff --git a/src/test/verify/token.rs b/src/test/verify/token.rs
index adc4397..1b61a19 100644
--- a/src/test/verify/token.rs
+++ b/src/test/verify/token.rs
@@ -1,32 +1,39 @@
+use axum::extract::FromRef;
+
use crate::{
- app::App,
login::Login,
name::Name,
test::fixtures,
- token::{Secret, app},
+ token::{Secret, app, app::Tokens},
};
-pub async fn valid_for_name(app: &App, secret: &Secret, name: &Name) {
- let identity = app
- .tokens()
+pub async fn valid_for_name<App>(app: &App, secret: &Secret, name: &Name)
+where
+ Tokens: FromRef<App>,
+{
+ let identity = Tokens::from_ref(app)
.validate(secret, &fixtures::now())
.await
.expect("provided secret is valid");
assert_eq!(name, &identity.login.name);
}
-pub async fn valid_for_login(app: &App, secret: &Secret, login: &Login) {
- let identity = app
- .tokens()
+pub async fn valid_for_login<App>(app: &App, secret: &Secret, login: &Login)
+where
+ Tokens: FromRef<App>,
+{
+ let identity = Tokens::from_ref(app)
.validate(secret, &fixtures::now())
.await
.expect("provided secret is valid");
assert_eq!(login, &identity.login);
}
-pub async fn invalid(app: &App, secret: &Secret) {
- let error = app
- .tokens()
+pub async fn invalid<App>(app: &App, secret: &Secret)
+where
+ Tokens: FromRef<App>,
+{
+ let error = Tokens::from_ref(app)
.validate(secret, &fixtures::now())
.await
.expect_err("provided secret is invalid");
diff --git a/src/token/app.rs b/src/token/app.rs
index 1d68f32..332473d 100644
--- a/src/token/app.rs
+++ b/src/token/app.rs
@@ -12,13 +12,13 @@ use super::{
};
use crate::{clock::DateTime, db::NotFound as _, name};
-pub struct Tokens<'a> {
- db: &'a SqlitePool,
- token_events: &'a Broadcaster,
+pub struct Tokens {
+ db: SqlitePool,
+ token_events: Broadcaster,
}
-impl<'a> Tokens<'a> {
- pub const fn new(db: &'a SqlitePool, token_events: &'a Broadcaster) -> Self {
+impl Tokens {
+ pub const fn new(db: SqlitePool, token_events: Broadcaster) -> Self {
Self { db, token_events }
}
diff --git a/src/token/extract/identity.rs b/src/token/extract/identity.rs
index bee4e31..5c004ef 100644
--- a/src/token/extract/identity.rs
+++ b/src/token/extract/identity.rs
@@ -1,16 +1,18 @@
use axum::{
- extract::{FromRequestParts, OptionalFromRequestParts, State},
+ extract::{FromRef, FromRequestParts, OptionalFromRequestParts, State},
http::request::Parts,
response::{IntoResponse, Response},
};
use super::IdentityCookie;
use crate::{
- app::App,
clock::RequestedAt,
error::{Internal, Unauthorized},
login::Login,
- token::{Token, app::ValidateError},
+ token::{
+ Token,
+ app::{Tokens, ValidateError},
+ },
};
#[derive(Clone, Debug)]
@@ -19,7 +21,11 @@ pub struct Identity {
pub login: Login,
}
-impl FromRequestParts<App> for Identity {
+impl<App> FromRequestParts<App> for Identity
+where
+ Tokens: FromRef<App>,
+ App: Send + Sync,
+{
type Rejection = LoginError<Internal>;
async fn from_request_parts(parts: &mut Parts, state: &App) -> Result<Self, Self::Rejection> {
@@ -28,8 +34,8 @@ impl FromRequestParts<App> for Identity {
let secret = cookie.secret().ok_or(LoginError::Unauthorized)?;
- let app = State::<App>::from_request_parts(parts, state).await?;
- app.tokens()
+ let tokens = State::<Tokens>::from_request_parts(parts, state).await?;
+ tokens
.validate(&secret, &used_at)
.await
.map_err(|err| match err {
@@ -39,7 +45,11 @@ impl FromRequestParts<App> for Identity {
}
}
-impl OptionalFromRequestParts<App> for Identity {
+impl<App> OptionalFromRequestParts<App> for Identity
+where
+ Tokens: FromRef<App>,
+ App: Send + Sync,
+{
type Rejection = LoginError<Internal>;
async fn from_request_parts(
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<App>,
+ State(conversations): State<Conversations>,
identity: Option<Identity>,
Path(conversation): Path<conversation::Id>,
) -> Result<Asset, Error> {
let _ = identity.ok_or(Error::NotLoggedIn)?;
- app.conversations()
+ conversations
.get(&conversation)
.await
.map_err(Error::from)?;
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<App>,
+ State(invites): State<Invites>,
Path(invite): Path<invite::Id>,
) -> Result<Asset, Error> {
- app.invites()
+ invites
.get(&invite)
.await
.map_err(Error::internal)?
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<App>) -> Result<Asset, Error> {
- if app
- .setup()
+pub async fn handler(State(setup): State<Setup>) -> Result<Asset, Error> {
+ if setup
.completed()
.await
.map_err(Internal::from)
diff --git a/src/user/app.rs b/src/user/app.rs
index 0d6046c..891e3b9 100644
--- a/src/user/app.rs
+++ b/src/user/app.rs
@@ -3,13 +3,13 @@ use sqlx::sqlite::SqlitePool;
use super::create::{self, Create};
use crate::{clock::DateTime, event::Broadcaster, login::Login, name::Name, password::Password};
-pub struct Users<'a> {
- db: &'a SqlitePool,
- events: &'a Broadcaster,
+pub struct Users {
+ db: SqlitePool,
+ events: Broadcaster,
}
-impl<'a> Users<'a> {
- pub const fn new(db: &'a SqlitePool, events: &'a Broadcaster) -> Self {
+impl Users {
+ pub const fn new(db: SqlitePool, events: Broadcaster) -> Self {
Self { db, events }
}
@@ -27,7 +27,7 @@ impl<'a> Users<'a> {
tx.commit().await?;
let login = stored.login().to_owned();
- stored.publish(self.events);
+ stored.publish(&self.events);
Ok(login)
}