use axum::extract::Request;
use axum::middleware::{self, Next};
use axum::response::{IntoResponse, Response};
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use tower::util::BoxCloneSyncService;
use tower::{Layer, Service};
#[derive(Clone)]
pub struct PostAuthorizationHook {
apply:
Arc<dyn Fn(Request, Next) -> Pin<Box<dyn Future<Output = Response> + Send>> + Send + Sync>,
}
impl PostAuthorizationHook {
pub fn from_fn<Fut, R>(f: fn(Request, Next) -> Fut) -> Self
where
Fut: Future<Output = R> + Send + 'static,
R: IntoResponse + 'static,
{
Self {
apply: Arc::new(move |req, next| {
Box::pin(async move { f(req, next).await.into_response() })
}),
}
}
}
impl<S> Layer<S> for PostAuthorizationHook
where
S: Service<Request, Response = Response, Error = std::convert::Infallible>
+ Clone
+ Send
+ Sync
+ 'static,
S::Future: Send + 'static,
{
type Service = BoxCloneSyncService<Request, Response, std::convert::Infallible>;
fn layer(&self, inner: S) -> Self::Service {
let apply = Arc::clone(&self.apply);
let from_fn_layer = middleware::from_fn(move |req: Request, next: Next| {
let apply = Arc::clone(&apply);
async move { apply(req, next).await }
});
BoxCloneSyncService::new(from_fn_layer.layer(inner))
}
}
#[cfg(test)]
mod tests {
use super::*;
use axum::Router;
use axum::body::Body;
use axum::http::{HeaderValue, Request as HttpRequest, StatusCode};
use axum::routing::get;
use tower::util::ServiceExt;
async fn tag_response(req: Request, next: Next) -> Response {
let mut res = next.run(req).await;
res.headers_mut()
.insert("x-hook-ran", HeaderValue::from_static("yes"));
res
}
#[tokio::test]
async fn hook_runs_and_can_modify_the_response() {
let app = Router::new()
.route("/hello", get(|| async { "ok" }))
.layer(PostAuthorizationHook::from_fn(tag_response));
let response = app
.oneshot(
HttpRequest::builder()
.uri("/hello")
.body(Body::empty())
.expect("request"),
)
.await
.expect("response");
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(response.headers().get("x-hook-ran").unwrap(), "yes");
}
async fn tag_again(req: Request, next: Next) -> Response {
let mut res = next.run(req).await;
res.headers_mut()
.insert("x-hook-2-ran", HeaderValue::from_static("yes"));
res
}
#[tokio::test]
async fn hook_composes_with_another_layer_stacked_on_top() {
let app = Router::new()
.route("/hello", get(|| async { "ok" }))
.layer(PostAuthorizationHook::from_fn(tag_response))
.layer(PostAuthorizationHook::from_fn(tag_again));
let response = app
.oneshot(
HttpRequest::builder()
.uri("/hello")
.body(Body::empty())
.expect("request"),
)
.await
.expect("response");
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(response.headers().get("x-hook-ran").unwrap(), "yes");
assert_eq!(response.headers().get("x-hook-2-ran").unwrap(), "yes");
}
}