use axum::body::Body;
use axum::handler::Handler;
use axum::response::{IntoResponse, Response};
use std::future::Future;
use std::pin::Pin;
pub type HtmlString = String;
pub type PageFn = Box<
dyn Fn(http::Request<axum::body::Body>) -> Pin<Box<dyn Future<Output = HtmlString> + Send>>
+ Send
+ Sync,
>;
pub type LayoutFn = Box<dyn Fn(&str) -> HtmlString + Send + Sync>;
pub type LoadingFn = Box<dyn Fn() -> HtmlString + Send + Sync>;
pub enum MiddlewareResult {
Continue(http::Request<Body>),
Response(Response),
}
impl MiddlewareResult {
pub fn next(req: http::Request<Body>) -> Self {
Self::Continue(req)
}
pub fn response(response: impl IntoResponse) -> Self {
Self::Response(response.into_response())
}
}
pub type MiddlewareFn = Box<
dyn Fn(http::Request<Body>) -> Pin<Box<dyn Future<Output = MiddlewareResult> + Send>>
+ Send
+ Sync,
>;
pub type RouteFn = Box<
dyn Fn(http::Request<axum::body::Body>) -> Pin<Box<dyn Future<Output = Response> + Send>>
+ Send
+ Sync,
>;
pub struct RouteEntry {
pub path: String,
pub page: Option<PageFn>,
pub layout: Option<LayoutFn>,
pub loading: Option<LoadingFn>,
pub middleware: Option<MiddlewareFn>,
pub methods: Vec<(http::Method, RouteFn)>,
}
pub struct NotFoundEntry {
pub path: String,
pub render: PageFn,
}
pub struct RouteRegistry {
pub entries: Vec<RouteEntry>,
pub not_found: Vec<NotFoundEntry>,
}
impl RouteRegistry {
pub fn new() -> Self {
Self {
entries: Vec::new(),
not_found: Vec::new(),
}
}
pub fn add(&mut self, entry: RouteEntry) {
self.entries.push(entry);
}
pub fn add_not_found(&mut self, path: impl Into<String>, render: PageFn) {
self.not_found.push(NotFoundEntry {
path: path.into(),
render,
});
}
}
pub fn static_page(html: &'static str) -> PageFn {
Box::new(move |_req| Box::pin(async move { html.to_string() }))
}
pub fn static_layout(template: &'static str) -> LayoutFn {
Box::new(move |children| {
template
.replace("{{ children }}", children)
.replace("{{children}}", children)
})
}
pub fn static_loading(html: &'static str) -> LoadingFn {
Box::new(move || html.to_string())
}
pub fn route_method<H, T>(handler: H) -> RouteFn
where
H: Handler<T, ()> + Sync,
T: 'static,
{
Box::new(move |req| {
let handler = handler.clone();
Box::pin(async move { handler.call(req, ()).await })
as Pin<Box<dyn Future<Output = Response> + Send>>
})
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_static_page_returns_html_verbatim() {
let p = static_page("<h1>Hello</h1>");
let req = http::Request::builder()
.body(axum::body::Body::empty())
.unwrap();
let html = p(req).await;
assert_eq!(html, "<h1>Hello</h1>");
}
#[test]
fn test_static_layout_substitutes_children() {
let l = static_layout("<html><body>{{children}}</body></html>");
let out = l("<h1>Hi</h1>");
assert_eq!(out, "<html><body><h1>Hi</h1></body></html>");
}
#[test]
fn test_static_layout_supports_askama_whitespace_form() {
let l = static_layout("<html>{{ children }}</html>");
let out = l("X");
assert_eq!(out, "<html>X</html>");
}
#[test]
fn test_static_layout_with_repeated_children_marker() {
let l = static_layout("<a>{{children}}</a><b>{{children}}</b>");
let out = l("X");
assert_eq!(out, "<a>X</a><b>X</b>");
}
#[test]
fn test_static_loading_returns_html_verbatim() {
let l = static_loading("<div>Loading...</div>");
assert_eq!(l(), "<div>Loading...</div>");
}
#[tokio::test]
async fn test_add_not_found_records_entry_and_render() {
let mut registry = RouteRegistry::new();
assert!(registry.not_found.is_empty());
registry.add_not_found("/admin", static_page("<h1>404</h1>"));
assert_eq!(registry.not_found.len(), 1);
let entry = ®istry.not_found[0];
assert_eq!(entry.path, "/admin");
let req = http::Request::builder()
.body(axum::body::Body::empty())
.unwrap();
assert_eq!((entry.render)(req).await, "<h1>404</h1>");
}
#[tokio::test]
async fn test_middleware_result_helpers() {
let req = http::Request::builder()
.body(axum::body::Body::empty())
.unwrap();
match MiddlewareResult::next(req) {
MiddlewareResult::Continue(_) => {}
MiddlewareResult::Response(_) => panic!("expected continue"),
}
match MiddlewareResult::response(axum::http::StatusCode::UNAUTHORIZED) {
MiddlewareResult::Continue(_) => panic!("expected response"),
MiddlewareResult::Response(resp) => {
assert_eq!(resp.status(), axum::http::StatusCode::UNAUTHORIZED);
}
}
}
#[tokio::test]
async fn test_route_method_wraps_into_response() {
async fn handler(_req: http::Request<axum::body::Body>) -> impl IntoResponse {
axum::http::StatusCode::CREATED
}
let route = route_method(handler);
let req = http::Request::builder()
.body(axum::body::Body::empty())
.unwrap();
let resp = route(req).await;
assert_eq!(resp.status(), axum::http::StatusCode::CREATED);
}
}