summaryrefslogtreecommitdiff
path: root/src/setup
diff options
context:
space:
mode:
authorOwen Jacobson <owen@grimoire.ca>2025-06-17 00:46:24 -0400
committerOwen Jacobson <owen@grimoire.ca>2025-06-17 01:08:19 -0400
commit424fb08ecd315c67dd3862c29e87eea7bf32f65c (patch)
treebf85cba9c93766b8651d16de9984bbeb48d58fdf /src/setup
parent4a7fb2c5cf7265c5ef6a78051c1bb73e7d3ef086 (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::or_unavailable(app.clone())) Or like: .route_layer(setup::Required::with_fallback(app.clone(), RESPONSE)) One thing I would have liked to have avoided is the additional `app.clone()` argument, but there isn't a way to extract the _state_ from a request inside of an Axum middleware. It has to be passed in externally - that's what `from_fn_with_state` is doing under the hood, as well. Using `State` as an extractor doesn't work; the `State` extractor is special in a _bunch_ of ways, and this is one of them. Other extractors would work. Realistically, I'd probably want to explore interfaces like .route_layer(setup::Required(app).or_unavailable()) or .route_layer(app.setup().required().or_unavailable())
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.rs88
3 files changed, 90 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..62972b3 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::Layer as Required, routes::router};
diff --git a/src/setup/required.rs b/src/setup/required.rs
new file mode 100644
index 0000000..5b7fe5b
--- /dev/null
+++ b/src/setup/required.rs
@@ -0,0 +1,88 @@
+use axum::{
+ extract::Request,
+ http::StatusCode,
+ response::{IntoResponse, Response},
+};
+use std::pin::Pin;
+use std::task::{Context, Poll};
+use tower::Service;
+
+use crate::{app::App, error::Internal};
+
+const UNAVAILABLE: (StatusCode, &str) = (
+ StatusCode::SERVICE_UNAVAILABLE,
+ "initial setup not completed",
+);
+
+#[derive(Clone)]
+pub struct Layer<F> {
+ app: App,
+ fallback: F,
+}
+
+impl Layer<(StatusCode, &'static str)> {
+ pub fn or_unavailable(app: App) -> Self {
+ Self::with_fallback(app, UNAVAILABLE)
+ }
+}
+
+impl<F> Layer<F> {
+ pub fn with_fallback(app: App, fallback: F) -> Self {
+ Layer { app, fallback }
+ }
+}
+
+impl<S, F> tower::Layer<S> for Layer<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()),
+ }
+ })
+ }
+}