use std::any::TypeId;
use std::sync::Arc;
use rainier_http::{Request, Response};
use rainier_support::BoxedFuture;
pub trait ConcreteMiddleware: Send + Sync + 'static {
fn concrete_type_id(&self) -> TypeId;
fn concrete_type_name(&self) -> &'static str;
}
impl<T: Middleware> ConcreteMiddleware for T {
fn concrete_type_id(&self) -> TypeId {
TypeId::of::<T>()
}
fn concrete_type_name(&self) -> &'static str {
std::any::type_name::<T>()
}
}
#[async_trait::async_trait]
pub trait Middleware: ConcreteMiddleware {
async fn handle(&self, request: Request, next: Next) -> Response;
fn name(&self) -> &'static str {
short_type_name(self.concrete_type_name())
}
}
pub(crate) fn short_type_name(full: &'static str) -> &'static str {
let without_generics = full.split_once('<').map_or(full, |(head, _)| head);
without_generics.rsplit("::").next().unwrap_or(without_generics)
}
pub trait Destination: Send + Sync + 'static {
fn call(&self, request: Request) -> BoxedFuture<Response>;
}
impl<F, Fut> Destination for F
where
F: Fn(Request) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = Response> + Send + 'static,
{
fn call(&self, request: Request) -> BoxedFuture<Response> {
Box::pin(self(request))
}
}
pub struct Next {
stages: Arc<[Arc<dyn Middleware>]>,
index: usize,
destination: Arc<dyn Destination>,
}
impl Next {
pub fn run(self, request: Request) -> BoxedFuture<Response> {
let Some(stage) = self.stages.get(self.index).cloned() else {
return self.destination.call(request);
};
let next =
Next { stages: self.stages, index: self.index + 1, destination: self.destination };
Box::pin(async move { stage.handle(request, next).await })
}
pub fn remaining(&self) -> usize {
self.stages.len().saturating_sub(self.index)
}
}
impl std::fmt::Debug for Next {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Next").field("remaining", &self.remaining()).finish()
}
}
#[derive(Default)]
pub struct Pipeline {
stages: Vec<Arc<dyn Middleware>>,
}
impl Pipeline {
pub fn new() -> Self {
Self::default()
}
pub fn through(mut self, middleware: impl Middleware) -> Self {
self.stages.push(Arc::new(middleware));
self
}
pub fn through_arc(mut self, middleware: Arc<dyn Middleware>) -> Self {
self.stages.push(middleware);
self
}
pub fn through_all(
mut self,
middleware: impl IntoIterator<Item = Arc<dyn Middleware>>,
) -> Self {
self.stages.extend(middleware);
self
}
pub fn then(self, destination: impl Destination) -> ReadyPipeline {
self.then_arc(Arc::new(destination))
}
pub fn then_arc(self, destination: Arc<dyn Destination>) -> ReadyPipeline {
ReadyPipeline { stages: self.stages.into(), destination }
}
pub fn len(&self) -> usize {
self.stages.len()
}
pub fn is_empty(&self) -> bool {
self.stages.is_empty()
}
}
#[derive(Clone)]
pub struct ReadyPipeline {
stages: Arc<[Arc<dyn Middleware>]>,
destination: Arc<dyn Destination>,
}
impl ReadyPipeline {
pub async fn run(&self, request: Request) -> Response {
Next {
stages: Arc::clone(&self.stages),
index: 0,
destination: Arc::clone(&self.destination),
}
.run(request)
.await
}
pub fn stage_names(&self) -> Vec<&'static str> {
self.stages.iter().map(|s| s.name()).collect()
}
}
impl std::fmt::Debug for ReadyPipeline {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ReadyPipeline").field("stages", &self.stage_names()).finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use rainier_http::StatusCode;
use std::sync::Mutex;
struct Trace {
tag: &'static str,
log: Arc<Mutex<Vec<String>>>,
}
#[async_trait::async_trait]
impl Middleware for Trace {
async fn handle(&self, request: Request, next: Next) -> Response {
self.log.lock().unwrap().push(format!("{}:before", self.tag));
let response = next.run(request).await;
self.log.lock().unwrap().push(format!("{}:after", self.tag));
response
}
fn name(&self) -> &'static str {
"Trace"
}
}
struct ShortCircuit;
#[async_trait::async_trait]
impl Middleware for ShortCircuit {
async fn handle(&self, _request: Request, _next: Next) -> Response {
Response::new(StatusCode::FORBIDDEN)
}
}
fn log() -> Arc<Mutex<Vec<String>>> {
Arc::new(Mutex::new(Vec::new()))
}
#[tokio::test]
async fn an_empty_pipeline_reaches_the_destination() {
let response = Pipeline::new()
.then(|_: Request| async { Response::text("hi") })
.run(Request::builder().build())
.await;
assert_eq!(response.status(), StatusCode::OK);
}
#[tokio::test]
async fn middleware_wraps_the_destination_in_order() {
let log = log();
let recorded = Arc::clone(&log);
let destination_log = Arc::clone(&log);
let response = Pipeline::new()
.through(Trace { tag: "outer", log: Arc::clone(&log) })
.through(Trace { tag: "inner", log: Arc::clone(&log) })
.then(move |_: Request| {
let log = Arc::clone(&destination_log);
async move {
log.lock().unwrap().push("handler".into());
Response::text("ok")
}
})
.run(Request::builder().build())
.await;
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
*recorded.lock().unwrap(),
vec!["outer:before", "inner:before", "handler", "inner:after", "outer:after"]
);
}
#[tokio::test]
async fn short_circuiting_skips_the_rest_and_the_handler() {
let log = log();
let recorded = Arc::clone(&log);
let response = Pipeline::new()
.through(Trace { tag: "outer", log: Arc::clone(&log) })
.through(ShortCircuit)
.through(Trace { tag: "never", log: Arc::clone(&log) })
.then(|_: Request| async { Response::text("unreachable") })
.run(Request::builder().build())
.await;
assert_eq!(response.status(), StatusCode::FORBIDDEN);
assert_eq!(*recorded.lock().unwrap(), vec!["outer:before", "outer:after"]);
}
#[tokio::test]
async fn middleware_can_rewrite_the_request_and_the_response() {
struct Tag;
#[async_trait::async_trait]
impl Middleware for Tag {
async fn handle(&self, mut request: Request, next: Next) -> Response {
request.merge_input(serde_json::json!({ "injected": "yes" }));
next.run(request).await.with_header("x-tagged", "1")
}
}
let response =
Pipeline::new()
.through(Tag)
.then(|request: Request| async move {
Response::text(request.input_or("injected", "no"))
})
.run(Request::builder().build())
.await;
assert_eq!(response.header("x-tagged"), Some("1"));
let body = response.into_http().into_body().collect().await.unwrap();
assert_eq!(body, "yes");
}
#[tokio::test]
async fn a_ready_pipeline_can_be_run_repeatedly() {
let pipeline = Pipeline::new()
.through(Trace { tag: "a", log: log() })
.then(|_: Request| async { Response::text("ok") });
for _ in 0..3 {
assert_eq!(pipeline.run(Request::builder().build()).await.status(), StatusCode::OK);
}
assert_eq!(pipeline.stage_names(), vec!["Trace"]);
}
#[tokio::test]
async fn next_reports_how_much_is_left() {
struct CountRemaining(Arc<Mutex<Vec<usize>>>);
#[async_trait::async_trait]
impl Middleware for CountRemaining {
async fn handle(&self, request: Request, next: Next) -> Response {
self.0.lock().unwrap().push(next.remaining());
next.run(request).await
}
}
let seen = Arc::new(Mutex::new(Vec::new()));
Pipeline::new()
.through(CountRemaining(Arc::clone(&seen)))
.through(CountRemaining(Arc::clone(&seen)))
.then(|_: Request| async { Response::no_content() })
.run(Request::builder().build())
.await;
assert_eq!(*seen.lock().unwrap(), vec![1, 0]);
}
}