summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorojacobson <ojacobson@noreply.codeberg.org>2025-06-18 23:52:47 +0200
committerojacobson <ojacobson@noreply.codeberg.org>2025-06-18 23:52:47 +0200
commitd84ba5cd09b713fac2f193d5c05af7415ea6742d (patch)
tree6ea93d417f329baccaa84f1d557638f54f3f2d37 /src
parent4a7fb2c5cf7265c5ef6a78051c1bb73e7d3ef086 (diff)
parent43375bcb875a31ce8c6132ce78552d45f64b261b (diff)
Unify `setup_required` middlewares.
The two middlewares were identical but for the specific `IntoResponse` impl used to generate the response when setup has not been completed. However, unifying them while still using `from_fn_with_state` lead to this horrorshow: .route_layer(middleware::from_fn_with_state( app.clone(), |state, req, next| { setup::middeware::setup_required(UNAVAILABLE, state, req, next) } )) It's a lot to read, and it surfaces the entire signature of a state-driven middleware `fn` into the call site solely to close over one argument (`UNAVAILABLE`). Rather than doing that, I've converted this middleware into a full blown Tower middleware, following <https://docs.rs/axum/latest/axum/middleware/index.html#towerservice-and-pinboxdyn-future>. I considered taking this further and implementing a custom future to remove the allocation for `Box::pin`, but honestly, that allocation isn't hurting anyone and this code already got long enough in the translation. The new API looks like: .route_layer(setup::Required(app.clone())) Or like: .route_layer(setup::Required(app.clone()).with_fallback(RESPONSE)) Part of a broader project to reorganize our routing. This is intended to reduce the number of "why do we have two of these?" questions when the answer isn't very compelling. We had two of these because I needed them to behave slightly differently, and at the time I didn't fully understand how to abstract out that difference. Now I do, thanks to Axum's excellent documentation. Merges unify-setup-required into main.
Diffstat (limited to 'src')
-rw-r--r--src/cli.rs9
-rw-r--r--src/setup/middleware.rs20
-rw-r--r--src/setup/mod.rs4
-rw-r--r--src/setup/required.rs107
-rw-r--r--src/ui/middleware.rs15
-rw-r--r--src/ui/mod.rs1
-rw-r--r--src/ui/routes/mod.rs6
7 files changed, 114 insertions, 48 deletions
diff --git a/src/cli.rs b/src/cli.rs
index 4232c00..7bfdbc0 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -15,12 +15,7 @@ use clap::{CommandFactory, Parser};
use sqlx::sqlite::SqlitePool;
use tokio::net;
-use crate::{
- app::App,
- boot, channel, clock, db, event, expire, invite, message,
- setup::{self, middleware::setup_required},
- ui, user,
-};
+use crate::{app::App, boot, channel, clock, db, event, expire, invite, message, setup, ui, user};
/// Command-line entry point for running the `pilcrow` server.
///
@@ -152,7 +147,7 @@ fn routers(app: &App) -> Router<App> {
app.clone(),
expire::middleware,
))
- .route_layer(middleware::from_fn_with_state(app.clone(), setup_required)),
+ .route_layer(setup::Required(app.clone())),
// API endpoints that handle setup
setup::router(),
// The UI (handles setup state itself)
diff --git a/src/setup/middleware.rs b/src/setup/middleware.rs
deleted file mode 100644
index 5f9996b..0000000
--- a/src/setup/middleware.rs
+++ /dev/null
@@ -1,20 +0,0 @@
-use axum::{
- extract::{Request, State},
- http::StatusCode,
- middleware::Next,
- response::{IntoResponse, Response},
-};
-
-use crate::{app::App, error::Internal};
-
-pub async fn setup_required(State(app): State<App>, request: Request, next: Next) -> Response {
- match app.setup().completed().await {
- Ok(true) => next.run(request).await,
- Ok(false) => (
- StatusCode::SERVICE_UNAVAILABLE,
- "initial setup not completed",
- )
- .into_response(),
- Err(error) => Internal::from(error).into_response(),
- }
-}
diff --git a/src/setup/mod.rs b/src/setup/mod.rs
index 5a8fa37..a4b821c 100644
--- a/src/setup/mod.rs
+++ b/src/setup/mod.rs
@@ -1,6 +1,6 @@
pub mod app;
-pub mod middleware;
pub mod repo;
+mod required;
mod routes;
-pub use self::routes::router;
+pub use self::{required::Required, routes::router};
diff --git a/src/setup/required.rs b/src/setup/required.rs
new file mode 100644
index 0000000..2112e4b
--- /dev/null
+++ b/src/setup/required.rs
@@ -0,0 +1,107 @@
+use axum::{
+ extract::Request,
+ http::StatusCode,
+ response::{IntoResponse, Response},
+};
+use std::pin::Pin;
+use std::task::{Context, Poll};
+use tower::{Layer, Service};
+
+use crate::{app::App, error::Internal};
+
+#[derive(Clone)]
+pub struct Required(pub App);
+
+impl Required {
+ pub fn with_fallback<F>(self, fallback: F) -> WithFallback<F> {
+ let Self(app) = self;
+ WithFallback { app, fallback }
+ }
+}
+
+impl<S> Layer<S> for Required {
+ type Service = Middleware<S, Unavailable>;
+
+ fn layer(&self, inner: S) -> Self::Service {
+ let Self(app) = self.clone();
+ Middleware {
+ inner,
+ app,
+ fallback: Unavailable,
+ }
+ }
+}
+
+#[derive(Clone)]
+pub struct WithFallback<F> {
+ app: App,
+ fallback: F,
+}
+
+impl<S, F> Layer<S> for WithFallback<F>
+where
+ Self: Clone,
+{
+ type Service = Middleware<S, F>;
+
+ fn layer(&self, inner: S) -> Self::Service {
+ let Self { app, fallback } = self.clone();
+ Middleware {
+ inner,
+ app,
+ fallback,
+ }
+ }
+}
+
+#[derive(Clone)]
+pub struct Middleware<S, F> {
+ inner: S,
+ app: App,
+ fallback: F,
+}
+
+impl<S, F> Service<Request> for Middleware<S, F>
+where
+ Self: Clone,
+ S: Service<Request, Response = Response> + Send + 'static,
+ S::Future: Send,
+ F: IntoResponse + Clone + Send + 'static,
+{
+ type Response = S::Response;
+ type Error = S::Error;
+ type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
+
+ fn poll_ready(&mut self, ctx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
+ self.inner.poll_ready(ctx)
+ }
+
+ fn call(&mut self, req: Request) -> Self::Future {
+ let Self {
+ mut inner,
+ app,
+ fallback,
+ } = self.clone();
+
+ Box::pin(async move {
+ match app.setup().completed().await {
+ Ok(true) => inner.call(req).await,
+ Ok(false) => Ok(fallback.into_response()),
+ Err(error) => Ok(Internal::from(error).into_response()),
+ }
+ })
+ }
+}
+
+#[derive(Clone)]
+pub struct Unavailable;
+
+impl IntoResponse for Unavailable {
+ fn into_response(self) -> Response {
+ (
+ StatusCode::SERVICE_UNAVAILABLE,
+ "initial setup not completed",
+ )
+ .into_response()
+ }
+}
diff --git a/src/ui/middleware.rs b/src/ui/middleware.rs
deleted file mode 100644
index f60ee1c..0000000
--- a/src/ui/middleware.rs
+++ /dev/null
@@ -1,15 +0,0 @@
-use axum::{
- extract::{Request, State},
- middleware::Next,
- response::{IntoResponse, Redirect, Response},
-};
-
-use crate::{app::App, error::Internal};
-
-pub async fn setup_required(State(app): State<App>, request: Request, next: Next) -> Response {
- match app.setup().completed().await {
- Ok(true) => next.run(request).await,
- Ok(false) => Redirect::to("/setup").into_response(),
- Err(error) => Internal::from(error).into_response(),
- }
-}
diff --git a/src/ui/mod.rs b/src/ui/mod.rs
index f8caa48..e834bba 100644
--- a/src/ui/mod.rs
+++ b/src/ui/mod.rs
@@ -1,6 +1,5 @@
mod assets;
mod error;
-mod middleware;
mod mime;
mod routes;
diff --git a/src/ui/routes/mod.rs b/src/ui/routes/mod.rs
index 80dc1e5..dc94773 100644
--- a/src/ui/routes/mod.rs
+++ b/src/ui/routes/mod.rs
@@ -1,6 +1,6 @@
-use axum::{Router, middleware, routing::get};
+use axum::{Router, response::Redirect, routing::get};
-use crate::{app::App, ui::middleware::setup_required};
+use crate::app::App;
mod ch;
mod get;
@@ -21,7 +21,7 @@ pub fn router(app: &App) -> Router<App> {
.route("/login", get(login::get::handler))
.route("/ch/{channel}", get(ch::channel::get::handler))
.route("/invite/{invite}", get(invite::invite::get::handler))
- .route_layer(middleware::from_fn_with_state(app.clone(), setup_required)),
+ .route_layer(crate::setup::Required(app.clone()).with_fallback(Redirect::to("/setup"))),
]
.into_iter()
.fold(Router::default(), Router::merge)