summaryrefslogtreecommitdiff
path: root/src/setup
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/setup
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/setup')
-rw-r--r--src/setup/middleware.rs20
-rw-r--r--src/setup/mod.rs4
-rw-r--r--src/setup/required.rs107
3 files changed, 109 insertions, 22 deletions
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()
+ }
+}