use crate::http::{Request, Response};
pub const fn validate_route_path(path: &'static str) -> &'static str {
let bytes = path.as_bytes();
if bytes.is_empty() || bytes[0] != b'/' {
panic!("Route path must start with '/'")
}
path
}
use super::router::update_route_mcp;
use crate::middleware::{into_boxed, BoxedMiddleware, Middleware};
use crate::routing::router::{register_route_name, BoxedHandler, Router};
use std::future::Future;
use std::sync::Arc;
fn convert_route_params(path: &str) -> String {
let mut result = String::with_capacity(path.len() + 4); let mut chars = path.chars().peekable();
while let Some(ch) = chars.next() {
if ch == ':' {
result.push('{');
while let Some(&next) = chars.peek() {
if next == '/' {
break;
}
result.push(chars.next().unwrap());
}
result.push('}');
} else {
result.push(ch);
}
}
result
}
#[derive(Clone, Copy)]
pub enum HttpMethod {
Get,
Post,
Put,
Patch,
Delete,
}
pub struct RouteDefBuilder<H> {
method: HttpMethod,
path: &'static str,
handler: H,
name: Option<&'static str>,
middlewares: Vec<BoxedMiddleware>,
mcp_tool_name: Option<String>,
mcp_description: Option<String>,
mcp_hint: Option<String>,
mcp_hidden: bool,
}
impl<H, Fut> RouteDefBuilder<H>
where
H: Fn(Request) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Response> + Send + 'static,
{
pub fn new(method: HttpMethod, path: &'static str, handler: H) -> Self {
Self {
method,
path,
handler,
name: None,
middlewares: Vec::new(),
mcp_tool_name: None,
mcp_description: None,
mcp_hint: None,
mcp_hidden: false,
}
}
pub fn name(mut self, name: &'static str) -> Self {
self.name = Some(name);
self
}
pub fn middleware<M: Middleware + 'static>(mut self, middleware: M) -> Self {
self.middlewares.push(into_boxed(middleware));
self
}
pub fn mcp_tool_name(mut self, name: &str) -> Self {
self.mcp_tool_name = Some(name.to_string());
self
}
pub fn mcp_description(mut self, desc: &str) -> Self {
self.mcp_description = Some(desc.to_string());
self
}
pub fn mcp_hint(mut self, hint: &str) -> Self {
self.mcp_hint = Some(hint.to_string());
self
}
pub fn mcp_hidden(mut self) -> Self {
self.mcp_hidden = true;
self
}
pub fn register(self, router: Router) -> Router {
let converted_path = convert_route_params(self.path);
let has_mcp = self.mcp_tool_name.is_some()
|| self.mcp_description.is_some()
|| self.mcp_hint.is_some()
|| self.mcp_hidden;
let mcp_tool_name = self.mcp_tool_name;
let mcp_description = self.mcp_description;
let mcp_hint = self.mcp_hint;
let mcp_hidden = self.mcp_hidden;
let builder = match self.method {
HttpMethod::Get => router.get(&converted_path, self.handler),
HttpMethod::Post => router.post(&converted_path, self.handler),
HttpMethod::Put => router.put(&converted_path, self.handler),
HttpMethod::Patch => router.patch(&converted_path, self.handler),
HttpMethod::Delete => router.delete(&converted_path, self.handler),
};
let builder = self
.middlewares
.into_iter()
.fold(builder, |b, m| b.middleware_boxed(m));
if has_mcp {
update_route_mcp(
&converted_path,
mcp_tool_name,
mcp_description,
mcp_hint,
mcp_hidden,
);
}
if let Some(name) = self.name {
builder.name(name)
} else {
builder.into()
}
}
}
#[macro_export]
macro_rules! get {
($path:expr, $handler:expr) => {{
const _: &str = $crate::validate_route_path($path);
$crate::__get_impl($path, $handler)
}};
}
#[doc(hidden)]
pub fn __get_impl<H, Fut>(path: &'static str, handler: H) -> RouteDefBuilder<H>
where
H: Fn(Request) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Response> + Send + 'static,
{
RouteDefBuilder::new(HttpMethod::Get, path, handler)
}
#[macro_export]
macro_rules! post {
($path:expr, $handler:expr) => {{
const _: &str = $crate::validate_route_path($path);
$crate::__post_impl($path, $handler)
}};
}
#[doc(hidden)]
pub fn __post_impl<H, Fut>(path: &'static str, handler: H) -> RouteDefBuilder<H>
where
H: Fn(Request) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Response> + Send + 'static,
{
RouteDefBuilder::new(HttpMethod::Post, path, handler)
}
#[macro_export]
macro_rules! put {
($path:expr, $handler:expr) => {{
const _: &str = $crate::validate_route_path($path);
$crate::__put_impl($path, $handler)
}};
}
#[doc(hidden)]
pub fn __put_impl<H, Fut>(path: &'static str, handler: H) -> RouteDefBuilder<H>
where
H: Fn(Request) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Response> + Send + 'static,
{
RouteDefBuilder::new(HttpMethod::Put, path, handler)
}
#[macro_export]
macro_rules! patch {
($path:expr, $handler:expr) => {{
const _: &str = $crate::validate_route_path($path);
$crate::__patch_impl($path, $handler)
}};
}
#[doc(hidden)]
pub fn __patch_impl<H, Fut>(path: &'static str, handler: H) -> RouteDefBuilder<H>
where
H: Fn(Request) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Response> + Send + 'static,
{
RouteDefBuilder::new(HttpMethod::Patch, path, handler)
}
#[macro_export]
macro_rules! delete {
($path:expr, $handler:expr) => {{
const _: &str = $crate::validate_route_path($path);
$crate::__delete_impl($path, $handler)
}};
}
#[doc(hidden)]
pub fn __delete_impl<H, Fut>(path: &'static str, handler: H) -> RouteDefBuilder<H>
where
H: Fn(Request) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Response> + Send + 'static,
{
RouteDefBuilder::new(HttpMethod::Delete, path, handler)
}
pub struct FallbackDefBuilder<H> {
handler: H,
middlewares: Vec<BoxedMiddleware>,
}
impl<H, Fut> FallbackDefBuilder<H>
where
H: Fn(Request) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Response> + Send + 'static,
{
pub fn new(handler: H) -> Self {
Self {
handler,
middlewares: Vec::new(),
}
}
pub fn middleware<M: Middleware + 'static>(mut self, middleware: M) -> Self {
self.middlewares.push(into_boxed(middleware));
self
}
pub fn register(self, mut router: Router) -> Router {
let handler = self.handler;
let boxed: BoxedHandler = Box::new(move |req| Box::pin(handler(req)));
router.set_fallback(Arc::new(boxed));
for mw in self.middlewares {
router.add_fallback_middleware(mw);
}
router
}
}
#[macro_export]
macro_rules! fallback {
($handler:expr) => {{
$crate::__fallback_impl($handler)
}};
}
#[doc(hidden)]
pub fn __fallback_impl<H, Fut>(handler: H) -> FallbackDefBuilder<H>
where
H: Fn(Request) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Response> + Send + 'static,
{
FallbackDefBuilder::new(handler)
}
pub struct GroupRoute {
method: HttpMethod,
path: &'static str,
handler: Arc<BoxedHandler>,
name: Option<&'static str>,
middlewares: Vec<BoxedMiddleware>,
mcp_tool_name: Option<String>,
mcp_description: Option<String>,
mcp_hint: Option<String>,
mcp_hidden: bool,
}
pub enum GroupItem {
Route(GroupRoute),
NestedGroup(Box<GroupDef>),
}
pub trait IntoGroupItem {
fn into_group_item(self) -> GroupItem;
}
#[derive(Default, Clone)]
struct McpDefaults {
tool_name: Option<String>,
description: Option<String>,
hint: Option<String>,
hidden: bool,
}
pub struct GroupDef {
prefix: &'static str,
items: Vec<GroupItem>,
group_middlewares: Vec<BoxedMiddleware>,
mcp_tool_name: Option<String>,
mcp_description: Option<String>,
mcp_hint: Option<String>,
mcp_hidden: bool,
}
impl GroupDef {
#[doc(hidden)]
pub fn __new_unchecked(prefix: &'static str) -> Self {
Self {
prefix,
items: Vec::new(),
group_middlewares: Vec::new(),
mcp_tool_name: None,
mcp_description: None,
mcp_hint: None,
mcp_hidden: false,
}
}
#[allow(clippy::should_implement_trait)]
pub fn add<I: IntoGroupItem>(mut self, item: I) -> Self {
self.items.push(item.into_group_item());
self
}
pub fn route<H, Fut>(self, route: RouteDefBuilder<H>) -> Self
where
H: Fn(Request) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Response> + Send + 'static,
{
self.add(route)
}
pub fn middleware<M: Middleware + 'static>(mut self, middleware: M) -> Self {
self.group_middlewares.push(into_boxed(middleware));
self
}
pub fn mcp_tool_name(mut self, name: &str) -> Self {
self.mcp_tool_name = Some(name.to_string());
self
}
pub fn mcp_description(mut self, desc: &str) -> Self {
self.mcp_description = Some(desc.to_string());
self
}
pub fn mcp_hint(mut self, hint: &str) -> Self {
self.mcp_hint = Some(hint.to_string());
self
}
pub fn mcp_hidden(mut self) -> Self {
self.mcp_hidden = true;
self
}
pub fn register(self, mut router: Router) -> Router {
let mcp_defaults = McpDefaults::default();
self.register_with_inherited(&mut router, "", &[], &mcp_defaults);
router
}
fn register_with_inherited(
self,
router: &mut Router,
parent_prefix: &str,
inherited_middleware: &[BoxedMiddleware],
inherited_mcp: &McpDefaults,
) {
let full_prefix = if parent_prefix.is_empty() {
self.prefix.to_string()
} else {
format!("{}{}", parent_prefix, self.prefix)
};
let combined_middleware: Vec<BoxedMiddleware> = inherited_middleware
.iter()
.cloned()
.chain(self.group_middlewares.iter().cloned())
.collect();
let combined_mcp = McpDefaults {
tool_name: self.mcp_tool_name.or(inherited_mcp.tool_name.clone()),
description: self.mcp_description.or(inherited_mcp.description.clone()),
hint: self.mcp_hint.or(inherited_mcp.hint.clone()),
hidden: self.mcp_hidden || inherited_mcp.hidden,
};
for item in self.items {
match item {
GroupItem::Route(route) => {
let converted_route_path = convert_route_params(route.path);
let full_path = if converted_route_path == "/" {
if full_prefix.is_empty() {
"/".to_string()
} else {
full_prefix.clone()
}
} else if full_prefix == "/" {
converted_route_path.to_string()
} else {
format!("{full_prefix}{converted_route_path}")
};
let full_path: &'static str = Box::leak(full_path.into_boxed_str());
match route.method {
HttpMethod::Get => {
router.insert_get(full_path, route.handler);
}
HttpMethod::Post => {
router.insert_post(full_path, route.handler);
}
HttpMethod::Put => {
router.insert_put(full_path, route.handler);
}
HttpMethod::Patch => {
router.insert_patch(full_path, route.handler);
}
HttpMethod::Delete => {
router.insert_delete(full_path, route.handler);
}
}
if let Some(name) = route.name {
register_route_name(name, full_path);
}
let mcp_tool_name = route.mcp_tool_name.or(combined_mcp.tool_name.clone());
let mcp_description =
route.mcp_description.or(combined_mcp.description.clone());
let mcp_hint = route.mcp_hint.or(combined_mcp.hint.clone());
let mcp_hidden = route.mcp_hidden || combined_mcp.hidden;
if mcp_tool_name.is_some()
|| mcp_description.is_some()
|| mcp_hint.is_some()
|| mcp_hidden
{
update_route_mcp(
full_path,
mcp_tool_name,
mcp_description,
mcp_hint,
mcp_hidden,
);
}
for mw in &combined_middleware {
router.add_middleware(full_path, mw.clone());
}
for mw in route.middlewares {
router.add_middleware(full_path, mw);
}
}
GroupItem::NestedGroup(nested) => {
nested.register_with_inherited(
router,
&full_prefix,
&combined_middleware,
&combined_mcp,
);
}
}
}
}
}
impl<H, Fut> RouteDefBuilder<H>
where
H: Fn(Request) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Response> + Send + 'static,
{
pub fn into_group_route(self) -> GroupRoute {
let handler = self.handler;
let boxed: BoxedHandler = Box::new(move |req| Box::pin(handler(req)));
GroupRoute {
method: self.method,
path: self.path,
handler: Arc::new(boxed),
name: self.name,
middlewares: self.middlewares,
mcp_tool_name: self.mcp_tool_name,
mcp_description: self.mcp_description,
mcp_hint: self.mcp_hint,
mcp_hidden: self.mcp_hidden,
}
}
}
impl<H, Fut> IntoGroupItem for RouteDefBuilder<H>
where
H: Fn(Request) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Response> + Send + 'static,
{
fn into_group_item(self) -> GroupItem {
GroupItem::Route(self.into_group_route())
}
}
impl IntoGroupItem for GroupDef {
fn into_group_item(self) -> GroupItem {
GroupItem::NestedGroup(Box::new(self))
}
}
#[macro_export]
macro_rules! group {
($prefix:expr, { $( $item:expr ),* $(,)? }) => {{
const _: &str = $crate::validate_route_path($prefix);
let mut group = $crate::GroupDef::__new_unchecked($prefix);
$(
group = group.add($item);
)*
group
}};
}
#[macro_export]
macro_rules! routes {
( $( $route:expr ),* $(,)? ) => {
pub fn register() -> $crate::Router {
let mut router = $crate::Router::new();
$(
router = $route.register(router);
)*
router
}
};
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum ResourceAction {
Index,
Create,
Store,
Show,
Edit,
Update,
Destroy,
}
impl ResourceAction {
pub const fn all() -> &'static [ResourceAction] {
&[
ResourceAction::Index,
ResourceAction::Create,
ResourceAction::Store,
ResourceAction::Show,
ResourceAction::Edit,
ResourceAction::Update,
ResourceAction::Destroy,
]
}
pub const fn method(&self) -> HttpMethod {
match self {
ResourceAction::Index => HttpMethod::Get,
ResourceAction::Create => HttpMethod::Get,
ResourceAction::Store => HttpMethod::Post,
ResourceAction::Show => HttpMethod::Get,
ResourceAction::Edit => HttpMethod::Get,
ResourceAction::Update => HttpMethod::Put,
ResourceAction::Destroy => HttpMethod::Delete,
}
}
pub const fn path_suffix(&self) -> &'static str {
match self {
ResourceAction::Index => "/",
ResourceAction::Create => "/create",
ResourceAction::Store => "/",
ResourceAction::Show => "/{id}",
ResourceAction::Edit => "/{id}/edit",
ResourceAction::Update => "/{id}",
ResourceAction::Destroy => "/{id}",
}
}
pub const fn name_suffix(&self) -> &'static str {
match self {
ResourceAction::Index => "index",
ResourceAction::Create => "create",
ResourceAction::Store => "store",
ResourceAction::Show => "show",
ResourceAction::Edit => "edit",
ResourceAction::Update => "update",
ResourceAction::Destroy => "destroy",
}
}
}
pub struct ResourceRoute {
action: ResourceAction,
handler: Arc<BoxedHandler>,
}
pub struct ResourceDef {
prefix: &'static str,
routes: Vec<ResourceRoute>,
middlewares: Vec<BoxedMiddleware>,
}
impl ResourceDef {
#[doc(hidden)]
pub fn __new_unchecked(prefix: &'static str) -> Self {
Self {
prefix,
routes: Vec::new(),
middlewares: Vec::new(),
}
}
#[doc(hidden)]
pub fn __add_route(mut self, action: ResourceAction, handler: Arc<BoxedHandler>) -> Self {
self.routes.push(ResourceRoute { action, handler });
self
}
pub fn middleware<M: Middleware + 'static>(mut self, middleware: M) -> Self {
self.middlewares.push(into_boxed(middleware));
self
}
pub fn register(self, mut router: Router) -> Router {
let name_prefix = self.prefix.trim_start_matches('/').replace('/', ".");
for route in self.routes {
let action = route.action;
let path_suffix = action.path_suffix();
let full_path = if path_suffix == "/" {
self.prefix.to_string()
} else {
format!("{}{}", self.prefix, path_suffix)
};
let full_path: &'static str = Box::leak(full_path.into_boxed_str());
let route_name = format!("{}.{}", name_prefix, action.name_suffix());
let route_name: &'static str = Box::leak(route_name.into_boxed_str());
match action.method() {
HttpMethod::Get => {
router.insert_get(full_path, route.handler);
}
HttpMethod::Post => {
router.insert_post(full_path, route.handler);
}
HttpMethod::Put => {
router.insert_put(full_path, route.handler);
}
HttpMethod::Patch => {
router.insert_patch(full_path, route.handler);
}
HttpMethod::Delete => {
router.insert_delete(full_path, route.handler);
}
}
register_route_name(route_name, full_path);
for mw in &self.middlewares {
router.add_middleware(full_path, mw.clone());
}
}
router
}
}
#[doc(hidden)]
pub fn __box_handler<H, Fut>(handler: H) -> Arc<BoxedHandler>
where
H: Fn(Request) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Response> + Send + 'static,
{
let boxed: BoxedHandler = Box::new(move |req| Box::pin(handler(req)));
Arc::new(boxed)
}
#[macro_export]
macro_rules! resource {
($path:expr, $($controller:ident)::+) => {{
const _: &str = $crate::validate_route_path($path);
$crate::ResourceDef::__new_unchecked($path)
.__add_route($crate::ResourceAction::Index, $crate::__box_handler($($controller)::+::index))
.__add_route($crate::ResourceAction::Create, $crate::__box_handler($($controller)::+::create))
.__add_route($crate::ResourceAction::Store, $crate::__box_handler($($controller)::+::store))
.__add_route($crate::ResourceAction::Show, $crate::__box_handler($($controller)::+::show))
.__add_route($crate::ResourceAction::Edit, $crate::__box_handler($($controller)::+::edit))
.__add_route($crate::ResourceAction::Update, $crate::__box_handler($($controller)::+::update))
.__add_route($crate::ResourceAction::Destroy, $crate::__box_handler($($controller)::+::destroy))
}};
($path:expr, $($controller:ident)::+, only: [$($action:ident),* $(,)?]) => {{
const _: &str = $crate::validate_route_path($path);
let mut resource = $crate::ResourceDef::__new_unchecked($path);
$(
resource = resource.__add_route(
$crate::__resource_action!($action),
$crate::__box_handler($($controller)::+::$action)
);
)*
resource
}};
}
#[doc(hidden)]
#[macro_export]
macro_rules! __resource_action {
(index) => {
$crate::ResourceAction::Index
};
(create) => {
$crate::ResourceAction::Create
};
(store) => {
$crate::ResourceAction::Store
};
(show) => {
$crate::ResourceAction::Show
};
(edit) => {
$crate::ResourceAction::Edit
};
(update) => {
$crate::ResourceAction::Update
};
(destroy) => {
$crate::ResourceAction::Destroy
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_convert_route_params() {
assert_eq!(convert_route_params("/users/:id"), "/users/{id}");
assert_eq!(
convert_route_params("/posts/:post_id/comments/:id"),
"/posts/{post_id}/comments/{id}"
);
assert_eq!(convert_route_params("/users/{id}"), "/users/{id}");
assert_eq!(convert_route_params("/users"), "/users");
assert_eq!(convert_route_params("/"), "/");
assert_eq!(
convert_route_params("/users/:user_id/posts/{post_id}"),
"/users/{user_id}/posts/{post_id}"
);
assert_eq!(
convert_route_params("/api/v1/:version"),
"/api/v1/{version}"
);
}
async fn test_handler(_req: Request) -> Response {
crate::http::text("ok")
}
#[test]
fn test_group_item_route() {
let route_builder = RouteDefBuilder::new(HttpMethod::Get, "/test", test_handler);
let item = route_builder.into_group_item();
matches!(item, GroupItem::Route(_));
}
#[test]
fn test_group_item_nested_group() {
let group_def = GroupDef::__new_unchecked("/nested");
let item = group_def.into_group_item();
matches!(item, GroupItem::NestedGroup(_));
}
#[test]
fn test_group_add_route() {
let group = GroupDef::__new_unchecked("/api").add(RouteDefBuilder::new(
HttpMethod::Get,
"/users",
test_handler,
));
assert_eq!(group.items.len(), 1);
matches!(&group.items[0], GroupItem::Route(_));
}
#[test]
fn test_group_add_nested_group() {
let nested = GroupDef::__new_unchecked("/users");
let group = GroupDef::__new_unchecked("/api").add(nested);
assert_eq!(group.items.len(), 1);
matches!(&group.items[0], GroupItem::NestedGroup(_));
}
#[test]
fn test_group_mixed_items() {
let nested = GroupDef::__new_unchecked("/admin");
let group = GroupDef::__new_unchecked("/api")
.add(RouteDefBuilder::new(
HttpMethod::Get,
"/users",
test_handler,
))
.add(nested)
.add(RouteDefBuilder::new(
HttpMethod::Post,
"/users",
test_handler,
));
assert_eq!(group.items.len(), 3);
matches!(&group.items[0], GroupItem::Route(_));
matches!(&group.items[1], GroupItem::NestedGroup(_));
matches!(&group.items[2], GroupItem::Route(_));
}
#[test]
fn test_deep_nesting() {
let level3 = GroupDef::__new_unchecked("/level3").add(RouteDefBuilder::new(
HttpMethod::Get,
"/",
test_handler,
));
let level2 = GroupDef::__new_unchecked("/level2").add(level3);
let level1 = GroupDef::__new_unchecked("/level1").add(level2);
assert_eq!(level1.items.len(), 1);
if let GroupItem::NestedGroup(l2) = &level1.items[0] {
assert_eq!(l2.items.len(), 1);
if let GroupItem::NestedGroup(l3) = &l2.items[0] {
assert_eq!(l3.items.len(), 1);
} else {
panic!("Expected nested group at level 2");
}
} else {
panic!("Expected nested group at level 1");
}
}
#[test]
fn test_backward_compatibility_route_method() {
let group = GroupDef::__new_unchecked("/api").route(RouteDefBuilder::new(
HttpMethod::Get,
"/users",
test_handler,
));
assert_eq!(group.items.len(), 1);
matches!(&group.items[0], GroupItem::Route(_));
}
}