summaryrefslogtreecommitdiff
path: root/src/invite
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 /src/invite
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.
Diffstat (limited to 'src/invite')
-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
7 files changed, 28 insertions, 28 deletions
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 {}),