use crate::path::to_axum_path;
use crate::session::{session_layer, SessionBackend};
use axum::extract::Request;
use axum::handler::Handler;
use axum::routing::{delete, get, patch, post, put, MethodRouter};
use larust_core::AppError;
use sqlx::AnyPool;
use std::collections::HashMap;
use std::sync::OnceLock;
use tower_sessions::SessionManagerLayer;
type BoxedMiddleware = Box<dyn Fn(MethodRouter) -> MethodRouter>;
#[derive(Debug, Clone)]
pub struct RouteInfo {
pub method: &'static str,
pub path: String,
pub name: Option<String>,
}
pub trait Plugin {
fn routes(&self) -> Router {
Router::new()
}
}
struct Entry {
info: RouteInfo,
method_router: MethodRouter,
immune_to_parent_middleware: bool,
}
pub struct Route;
impl Route {
pub fn get<H, T>(path: &str, handler: H) -> Router
where
H: Handler<T, ()>,
T: 'static,
{
Router::new().get(path, handler)
}
pub fn post<H, T>(path: &str, handler: H) -> Router
where
H: Handler<T, ()>,
T: 'static,
{
Router::new().post(path, handler)
}
pub fn put<H, T>(path: &str, handler: H) -> Router
where
H: Handler<T, ()>,
T: 'static,
{
Router::new().put(path, handler)
}
pub fn patch<H, T>(path: &str, handler: H) -> Router
where
H: Handler<T, ()>,
T: 'static,
{
Router::new().patch(path, handler)
}
pub fn delete<H, T>(path: &str, handler: H) -> Router
where
H: Handler<T, ()>,
T: 'static,
{
Router::new().delete(path, handler)
}
pub fn group<F>(prefix: &str, build: F) -> Router
where
F: FnOnce(Router) -> Router,
{
Router::new().group(prefix, build)
}
#[allow(clippy::too_many_arguments)] pub fn resource<
HIndex,
TIndex,
HCreate,
TCreate,
HStore,
TStore,
HShow,
TShow,
HEdit,
TEdit,
HUpdate,
TUpdate,
HDestroy,
TDestroy,
>(
prefix: &str,
param: &str,
index: HIndex,
create: HCreate,
store: HStore,
show: HShow,
edit: HEdit,
update: HUpdate,
destroy: HDestroy,
) -> Router
where
HIndex: Handler<TIndex, ()>,
TIndex: 'static,
HCreate: Handler<TCreate, ()>,
TCreate: 'static,
HStore: Handler<TStore, ()>,
TStore: 'static,
HShow: Handler<TShow, ()>,
TShow: 'static,
HEdit: Handler<TEdit, ()>,
TEdit: 'static,
HUpdate: Handler<TUpdate, ()>,
TUpdate: 'static,
HDestroy: Handler<TDestroy, ()>,
TDestroy: 'static,
{
Router::new().resource(
prefix, param, index, create, store, show, edit, update, destroy,
)
}
}
#[derive(Default)]
#[must_use]
pub struct Router {
entries: Vec<Entry>,
middlewares: Vec<BoxedMiddleware>,
session_layer: Option<SessionManagerLayer<SessionBackend>>,
}
impl Router {
pub fn new() -> Self {
Self::default()
}
pub fn get<H, T>(self, path: &str, handler: H) -> Self
where
H: Handler<T, ()>,
T: 'static,
{
self.push("GET", path, get(handler))
}
pub fn post<H, T>(self, path: &str, handler: H) -> Self
where
H: Handler<T, ()>,
T: 'static,
{
self.push("POST", path, post(handler))
}
pub fn put<H, T>(self, path: &str, handler: H) -> Self
where
H: Handler<T, ()>,
T: 'static,
{
self.push("PUT", path, put(handler))
}
pub fn patch<H, T>(self, path: &str, handler: H) -> Self
where
H: Handler<T, ()>,
T: 'static,
{
self.push("PATCH", path, patch(handler))
}
pub fn delete<H, T>(self, path: &str, handler: H) -> Self
where
H: Handler<T, ()>,
T: 'static,
{
self.push("DELETE", path, delete(handler))
}
pub fn name(mut self, name: &str) -> Self {
if let Some(last) = self.entries.last_mut() {
last.info.name = Some(name.to_string());
}
self
}
pub fn group<F>(mut self, prefix: &str, build: F) -> Self
where
F: FnOnce(Router) -> Router,
{
let sub = build(Router::new());
for entry in sub.entries {
let info = RouteInfo {
method: entry.info.method,
path: format!("{prefix}{}", entry.info.path),
name: entry.info.name,
};
let method_router = sub
.middlewares
.iter()
.rev()
.fold(entry.method_router, |router, middleware| middleware(router));
self.entries.push(Entry {
info,
method_router,
immune_to_parent_middleware: false,
});
}
self
}
pub fn merge(mut self, prefix: &str, other: Router) -> Self {
for entry in other.entries {
let info = RouteInfo {
method: entry.info.method,
path: format!("{prefix}{}", entry.info.path),
name: entry.info.name,
};
let method_router = other
.middlewares
.iter()
.rev()
.fold(entry.method_router, |router, middleware| middleware(router));
self.entries.push(Entry {
info,
method_router,
immune_to_parent_middleware: true,
});
}
self
}
pub fn plugin<P: Plugin>(mut self, plugin: P) -> Self {
let other = plugin.routes();
for entry in other.entries {
let method_router = other
.middlewares
.iter()
.rev()
.fold(entry.method_router, |router, middleware| middleware(router));
self.entries.push(Entry {
info: entry.info,
method_router,
immune_to_parent_middleware: false,
});
}
self
}
#[allow(clippy::too_many_arguments)] pub fn resource<
HIndex,
TIndex,
HCreate,
TCreate,
HStore,
TStore,
HShow,
TShow,
HEdit,
TEdit,
HUpdate,
TUpdate,
HDestroy,
TDestroy,
>(
self,
prefix: &str,
param: &str,
index: HIndex,
create: HCreate,
store: HStore,
show: HShow,
edit: HEdit,
update: HUpdate,
destroy: HDestroy,
) -> Self
where
HIndex: Handler<TIndex, ()>,
TIndex: 'static,
HCreate: Handler<TCreate, ()>,
TCreate: 'static,
HStore: Handler<TStore, ()>,
TStore: 'static,
HShow: Handler<TShow, ()>,
TShow: 'static,
HEdit: Handler<TEdit, ()>,
TEdit: 'static,
HUpdate: Handler<TUpdate, ()>,
TUpdate: 'static,
HDestroy: Handler<TDestroy, ()>,
TDestroy: 'static,
{
let index_path = format!("/{prefix}");
let create_path = format!("/{prefix}/create");
let item_path = format!("/{prefix}/{{{param}}}");
let edit_path = format!("{item_path}/edit");
self.get(&index_path, index)
.name(&format!("{prefix}.index"))
.get(&create_path, create)
.name(&format!("{prefix}.create"))
.post(&index_path, store)
.name(&format!("{prefix}.store"))
.get(&item_path, show)
.name(&format!("{prefix}.show"))
.get(&edit_path, edit)
.name(&format!("{prefix}.edit"))
.put(&item_path, update)
.name(&format!("{prefix}.update"))
.delete(&item_path, destroy)
.name(&format!("{prefix}.destroy"))
}
pub fn routes(&self) -> Vec<RouteInfo> {
self.entries
.iter()
.map(|entry| entry.info.clone())
.collect()
}
pub fn middleware<L>(mut self, layer: L) -> Self
where
L: tower::Layer<axum::routing::Route> + Clone + Send + 'static,
L::Service: tower::Service<Request> + Clone + Send + 'static,
<L::Service as tower::Service<Request>>::Response: axum::response::IntoResponse + 'static,
<L::Service as tower::Service<Request>>::Error: Into<std::convert::Infallible> + 'static,
<L::Service as tower::Service<Request>>::Future: Send + 'static,
{
self.middlewares.push(Box::new(move |router: MethodRouter| {
router.layer(layer.clone())
}));
self
}
pub async fn with_sessions(mut self, pool: &AnyPool, secure: bool) -> Result<Self, AppError> {
self.session_layer = Some(session_layer(pool, secure).await?);
Ok(self)
}
pub fn into_axum_router(self) -> axum::Router {
let mut names = HashMap::new();
let mut router = axum::Router::new();
for entry in self.entries {
let method_router = if entry.immune_to_parent_middleware {
entry.method_router
} else {
self.middlewares
.iter()
.rev()
.fold(entry.method_router, |router, middleware| middleware(router))
};
let axum_path = to_axum_path(&entry.info.path);
router = router.route(&axum_path, method_router);
if let Some(name) = entry.info.name {
names.insert(name, entry.info.path);
}
}
if let Some(layer) = self.session_layer {
router = router.layer(layer);
}
router = router.layer(axum::middleware::from_fn(crate::push_registry_scope::scope));
publish_route_names(names);
router
}
fn push(mut self, method: &'static str, path: &str, method_router: MethodRouter) -> Self {
self.entries.push(Entry {
info: RouteInfo {
method,
path: path.to_string(),
name: None,
},
method_router,
immune_to_parent_middleware: false,
});
self
}
}
static ROUTE_NAMES: OnceLock<HashMap<String, String>> = OnceLock::new();
fn publish_route_names(names: HashMap<String, String>) {
if ROUTE_NAMES.set(names).is_err() {
tracing::warn!(
"into_axum_router() called more than once in this process; \
route name resolution still uses the first router's names"
);
}
}
pub fn resolve_route_name(name: &str) -> Option<String> {
ROUTE_NAMES.get().and_then(|names| names.get(name).cloned())
}
#[cfg(test)]
mod tests {
use super::*;
async fn index() -> &'static str {
"index"
}
async fn show() -> &'static str {
"show"
}
#[test]
fn name_applies_to_most_recently_added_route() {
let router = Router::new()
.get("/", index)
.get("/posts", index)
.name("posts.index")
.get("/posts/{post}", show)
.name("posts.show");
let routes = router.routes();
assert_eq!(routes[0].name, None);
assert_eq!(routes[1].name.as_deref(), Some("posts.index"));
assert_eq!(routes[2].name.as_deref(), Some("posts.show"));
}
#[test]
fn group_prefixes_nested_route_paths() {
let router = Route::group("/admin", |r| {
r.get("/dashboard", index).name("admin.dashboard")
});
let routes = router.routes();
assert_eq!(routes.len(), 1);
assert_eq!(routes[0].path, "/admin/dashboard");
assert_eq!(routes[0].name.as_deref(), Some("admin.dashboard"));
}
#[test]
fn route_static_entry_points_match_router_instance_methods() {
let via_route = Route::get("/posts", index).routes();
let via_router = Router::new().get("/posts", index).routes();
assert_eq!(via_route[0].method, via_router[0].method);
assert_eq!(via_route[0].path, via_router[0].path);
}
struct TestPlugin;
impl Plugin for TestPlugin {
fn routes(&self) -> Router {
Router::new()
.get("/__test_plugin/runtime.js", index)
.post("/__test_plugin/{id}", show)
}
}
struct EmptyPlugin;
impl Plugin for EmptyPlugin {}
#[test]
fn plugin_merges_routes_unprefixed() {
let routes = Router::new().get("/", index).plugin(TestPlugin).routes();
assert_eq!(routes.len(), 3);
assert_eq!(routes[1].method, "GET");
assert_eq!(routes[1].path, "/__test_plugin/runtime.js");
assert_eq!(routes[2].method, "POST");
assert_eq!(routes[2].path, "/__test_plugin/{id}");
}
#[test]
fn plugin_default_routes_method_is_a_no_op() {
let routes = Router::new().get("/", index).plugin(EmptyPlugin).routes();
assert_eq!(routes.len(), 1);
}
#[test]
fn group_double_prefixes_a_nested_plugin_call() {
let routes = Route::group("/admin", |r| r.plugin(TestPlugin)).routes();
assert_eq!(routes[0].path, "/admin/__test_plugin/runtime.js");
assert_eq!(routes[1].path, "/admin/__test_plugin/{id}");
}
}