mod strip_prefix;
use std::{borrow::Cow, pin::Pin, sync::Arc};
pub use strip_prefix::*;
use topcoat_core::{context::Cx, error::Result};
use crate::{
Body, Endpoint, IntoPath, Path, Route,
error::{method_not_allowed, not_found},
response::Response,
};
pub type LayerFuture<'a> = Pin<Box<dyn Future<Output = Result<Response>> + Send + 'a>>;
pub trait Layer: Send + Sync + 'static {
fn path(&self) -> Option<&Path>;
fn handle<'a>(&'a self, cx: &'a Cx, body: Body, next: Next<'a>) -> LayerFuture<'a>;
}
impl<L: Layer + ?Sized> Layer for &'static L {
fn path(&self) -> Option<&Path> {
(**self).path()
}
fn handle<'a>(&'a self, cx: &'a Cx, body: Body, next: Next<'a>) -> LayerFuture<'a> {
(**self).handle(cx, body, next)
}
}
#[cfg(feature = "discover")]
inventory::collect!(&'static dyn Layer);
pub type LayerHandlerFn = for<'a> fn(cx: &'a Cx, body: Body, next: Next<'a>) -> LayerFuture<'a>;
#[derive(Debug, Clone)]
pub struct LayerFn {
path: Option<Cow<'static, Path>>,
handle: LayerHandlerFn,
}
impl LayerFn {
#[track_caller]
pub fn new(path: Option<impl IntoPath>, handle: LayerHandlerFn) -> Self {
Self {
path: path.map(IntoPath::into_path),
handle,
}
}
}
impl Layer for LayerFn {
fn path(&self) -> Option<&Path> {
self.path.as_deref()
}
fn handle<'a>(&'a self, cx: &'a Cx, body: Body, next: Next<'a>) -> LayerFuture<'a> {
(self.handle)(cx, body, next)
}
}
pub(crate) fn layers_for_path(layers: &[Arc<dyn Layer>], path: &Path) -> Box<[Arc<dyn Layer>]> {
let mut matching: Vec<&Arc<dyn Layer>> = layers
.iter()
.filter(|layer| layer.path().is_none_or(|prefix| path.starts_with(prefix)))
.rev()
.collect();
matching.sort_by_key(|layer| layer.path().map_or(0, |path| path.len() + 1));
matching.into_iter().cloned().collect()
}
#[derive(Clone, Copy)]
pub(crate) enum Terminal<'a> {
Route(&'a dyn Route),
MethodNotAllowed(&'a Endpoint),
NotFound,
}
pub struct Next<'a> {
layers: &'a [Arc<dyn Layer>],
terminal: Terminal<'a>,
}
impl<'a> Next<'a> {
pub(crate) fn new(layers: &'a [Arc<dyn Layer>], terminal: Terminal<'a>) -> Self {
Self { layers, terminal }
}
#[must_use]
pub fn run(self, cx: &'a Cx, body: Body) -> LayerFuture<'a> {
match self.layers.split_first() {
Some((layer, rest)) => layer.handle(
cx,
body,
Next {
layers: rest,
..self
},
),
None => match self.terminal {
Terminal::Route(route) => route.handle(cx, body),
Terminal::MethodNotAllowed(endpoint) => {
let error = method_not_allowed(endpoint.methods().cloned());
Box::pin(async move { Err(error.into()) })
}
Terminal::NotFound => Box::pin(async { Err(not_found().into()) }),
},
}
}
}
#[cfg(test)]
mod tests {
use std::{
future::Future,
sync::{Arc, Mutex},
};
use http::StatusCode;
use topcoat_core::context::{AppContext, Cx, app_context};
use super::*;
use crate::{
Method, RouteFn, RouteFuture, RouteIndex, error::respond, request::Bytes,
response::IntoResponse, to_bytes,
};
fn block_on<F: Future>(future: F) -> F::Output {
tokio::runtime::Builder::new_current_thread()
.build()
.unwrap()
.block_on(future)
}
fn path(s: &'static str) -> Cow<'static, Path> {
Cow::Borrowed(Path::new(s))
}
fn layer_at(p: &'static str) -> Arc<dyn Layer> {
Arc::new(LayerFn::new(Some(path(p)), noop_layer))
}
fn layer_always() -> Arc<dyn Layer> {
Arc::new(LayerFn::new(None::<&Path>, noop_layer))
}
fn assert_selects(
layers: &[Arc<dyn Layer>],
p: &'static str,
expected: &[Option<&'static str>],
) {
let selected = layers_for_path(layers, Path::new(p));
let paths: Vec<Option<&Path>> = selected.iter().map(|layer| layer.path()).collect();
let expected: Vec<Option<&Path>> = expected.iter().map(|e| e.map(Path::new)).collect();
assert_eq!(paths, expected);
}
fn noop_layer<'a>(cx: &'a Cx, body: Body, next: Next<'a>) -> LayerFuture<'a> {
next.run(cx, body)
}
fn body_bytes(response: Response) -> Bytes {
let (_, body) = response.into_parts();
block_on(to_bytes(body, usize::MAX)).unwrap()
}
type Trace = Mutex<Vec<&'static str>>;
fn cx_with_trace(trace: Arc<Trace>) -> Cx {
let mut app = AppContext::new();
app.insert(trace);
Cx::new(Arc::new(app))
}
fn record_a<'a>(cx: &'a Cx, body: Body, next: Next<'a>) -> LayerFuture<'a> {
Box::pin(async move {
app_context::<Arc<Trace>>(cx).lock().unwrap().push("a");
next.run(cx, body).await
})
}
fn record_b<'a>(cx: &'a Cx, body: Body, next: Next<'a>) -> LayerFuture<'a> {
Box::pin(async move {
app_context::<Arc<Trace>>(cx).lock().unwrap().push("b");
next.run(cx, body).await
})
}
fn short_circuit<'a>(cx: &'a Cx, _body: Body, _next: Next<'a>) -> LayerFuture<'a> {
Box::pin(async move { "short".into_response(cx) })
}
fn say_route(cx: &Cx, _body: Body) -> RouteFuture<'_> {
Box::pin(async move { "route".into_response(cx) })
}
fn record_route(cx: &Cx, _body: Body) -> RouteFuture<'_> {
Box::pin(async move {
app_context::<Arc<Trace>>(cx).lock().unwrap().push("route");
"route".into_response(cx)
})
}
#[test]
fn layer_fn_exposes_its_path() {
let layer = LayerFn::new(Some(path("/admin")), noop_layer);
assert_eq!(layer.path(), Some(Path::new("/admin")));
let layer = LayerFn::new(None::<&Path>, noop_layer);
assert_eq!(layer.path(), None);
}
#[test]
fn for_path_orders_prefix_layers_least_to_most_specific() {
let layers = [layer_at("/"), layer_at("/users"), layer_at("/posts")];
assert_selects(&layers, "/users/{id}", &[Some("/"), Some("/users")]);
}
#[test]
fn for_path_puts_pathless_layers_outermost() {
let layers = [layer_at("/"), layer_always()];
assert_selects(&layers, "/users", &[None, Some("/")]);
let layers = [layer_always(), layer_at("/")];
assert_selects(&layers, "/users", &[None, Some("/")]);
}
#[test]
fn for_path_runs_the_later_of_a_shared_path_first() {
let first = layer_at("/admin");
let second = layer_at("/admin");
let layers = [Arc::clone(&first), Arc::clone(&second)];
let selected = layers_for_path(&layers, Path::new("/admin/users"));
assert_eq!(selected.len(), 2);
assert!(Arc::ptr_eq(&selected[0], &second));
assert!(Arc::ptr_eq(&selected[1], &first));
}
#[test]
fn for_path_rejects_partial_segments() {
let layers = [layer_at("/admin")];
assert!(layers_for_path(&layers, Path::new("/administrator")).is_empty());
}
#[test]
fn for_path_includes_group_segments() {
let layers = [layer_at("/(auth)"), layer_at("/dashboard")];
assert_selects(&layers, "/(auth)/dashboard", &[Some("/(auth)")]);
}
#[test]
fn for_path_distinguishes_param_names() {
let layers = [layer_at("/users/{id}"), layer_at("/users/{user_id}")];
assert_selects(&layers, "/users/{id}/posts", &[Some("/users/{id}")]);
}
#[test]
fn run_invokes_the_route_terminal_when_no_layers_remain() {
let route = RouteFn::new(Method::GET, path("/x"), say_route);
let cx = Cx::default();
let next = Next::new(&[], Terminal::Route(&route));
let result = block_on(next.run(&cx, Body::empty()));
let response = respond(&cx, result);
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(&body_bytes(response)[..], b"route");
}
#[test]
fn run_resolves_the_method_not_allowed_terminal() {
let mut endpoint = Endpoint::new(&path("/x"));
endpoint.insert(Method::GET, RouteIndex::new(0));
endpoint.insert(Method::POST, RouteIndex::new(1));
let cx = Cx::default();
let next = Next::new(&[], Terminal::MethodNotAllowed(&endpoint));
let result = block_on(next.run(&cx, Body::empty()));
let response = respond(&cx, result);
assert_eq!(response.status(), StatusCode::METHOD_NOT_ALLOWED);
let allow = response
.headers()
.get(http::header::ALLOW)
.unwrap()
.to_str()
.unwrap();
assert!(allow.contains("GET"), "{allow:?}");
assert!(allow.contains("POST"), "{allow:?}");
}
#[test]
fn run_walks_layers_in_order_before_the_terminal() {
let layers: [Arc<dyn Layer>; 2] = [
Arc::new(LayerFn::new(Some(path("/")), record_a)),
Arc::new(LayerFn::new(Some(path("/")), record_b)),
];
let route = RouteFn::new(Method::GET, path("/x"), record_route);
let trace: Arc<Trace> = Arc::new(Mutex::new(Vec::new()));
let cx = cx_with_trace(trace.clone());
let next = Next::new(&layers, Terminal::Route(&route));
block_on(next.run(&cx, Body::empty())).unwrap();
assert_eq!(*trace.lock().unwrap(), vec!["a", "b", "route"]);
}
#[test]
fn run_lets_a_layer_short_circuit_without_calling_next() {
let layers: [Arc<dyn Layer>; 1] = [Arc::new(LayerFn::new(Some(path("/")), short_circuit))];
let route = RouteFn::new(Method::GET, path("/x"), say_route);
let cx = Cx::default();
let next = Next::new(&layers, Terminal::Route(&route));
let result = block_on(next.run(&cx, Body::empty()));
let response = respond(&cx, result);
assert_eq!(&body_bytes(response)[..], b"short");
}
}