Skip to main content

ferro_rs/routing/
macros.rs

1//! Route definition macros and helpers for Laravel-like routing syntax
2//!
3//! This module provides a clean, declarative way to define routes:
4//!
5//! ```rust,ignore
6//! use ferro_rs::{routes, get, post, put, delete, group};
7//!
8//! routes! {
9//!     get!("/", controllers::home::index).name("home"),
10//!     get!("/users", controllers::user::index).name("users.index"),
11//!     post!("/users", controllers::user::store).name("users.store"),
12//!     get!("/protected", controllers::home::index).middleware(AuthMiddleware),
13//!
14//!     // Route groups with prefix and middleware
15//!     group!("/api", {
16//!         get!("/users", controllers::api::user::index).name("api.users.index"),
17//!         post!("/users", controllers::api::user::store).name("api.users.store"),
18//!     }).middleware(AuthMiddleware),
19//! }
20//! ```
21
22use crate::http::{Request, Response};
23
24/// Const function to validate route paths start with '/'
25///
26/// This provides compile-time validation that all route paths begin with '/'.
27/// If a path doesn't start with '/', compilation will fail with a clear error.
28///
29/// # Panics
30///
31/// Panics at compile time if the path is empty or doesn't start with '/'.
32pub const fn validate_route_path(path: &'static str) -> &'static str {
33    let bytes = path.as_bytes();
34    if bytes.is_empty() || bytes[0] != b'/' {
35        panic!("Route path must start with '/'")
36    }
37    path
38}
39use super::router::update_route_mcp;
40use crate::middleware::{into_boxed, BoxedMiddleware, Middleware};
41use crate::routing::router::{register_route_name, BoxedHandler, Router};
42use std::future::Future;
43use std::sync::Arc;
44
45/// Convert Express-style `:param` route parameters to matchit-style `{param}`
46///
47/// This allows developers to use either syntax:
48/// - `/:id` (Express/Rails style)
49/// - `/{id}` (matchit native style)
50///
51/// # Examples
52///
53/// - `/users/:id` → `/users/{id}`
54/// - `/posts/:post_id/comments/:id` → `/posts/{post_id}/comments/{id}`
55/// - `/users/{id}` → `/users/{id}` (already correct syntax, unchanged)
56fn convert_route_params(path: &str) -> String {
57    let mut result = String::with_capacity(path.len() + 4); // Extra space for braces
58    let mut chars = path.chars().peekable();
59
60    while let Some(ch) = chars.next() {
61        if ch == ':' {
62            // Start of parameter - collect until '/' or end
63            result.push('{');
64            while let Some(&next) = chars.peek() {
65                if next == '/' {
66                    break;
67                }
68                result.push(chars.next().unwrap());
69            }
70            result.push('}');
71        } else {
72            result.push(ch);
73        }
74    }
75    result
76}
77
78/// HTTP method for route definitions
79#[derive(Clone, Copy)]
80pub enum HttpMethod {
81    /// HTTP GET method.
82    Get,
83    /// HTTP POST method.
84    Post,
85    /// HTTP PUT method.
86    Put,
87    /// HTTP PATCH method.
88    Patch,
89    /// HTTP DELETE method.
90    Delete,
91}
92
93/// Builder for route definitions that supports `.name()` and `.middleware()` chaining
94pub struct RouteDefBuilder<H> {
95    method: HttpMethod,
96    path: &'static str,
97    handler: H,
98    name: Option<&'static str>,
99    middlewares: Vec<BoxedMiddleware>,
100    mcp_tool_name: Option<String>,
101    mcp_description: Option<String>,
102    mcp_hint: Option<String>,
103    mcp_hidden: bool,
104}
105
106impl<H, Fut> RouteDefBuilder<H>
107where
108    H: Fn(Request) -> Fut + Send + Sync + 'static,
109    Fut: Future<Output = Response> + Send + 'static,
110{
111    /// Create a new route definition builder
112    pub fn new(method: HttpMethod, path: &'static str, handler: H) -> Self {
113        Self {
114            method,
115            path,
116            handler,
117            name: None,
118            middlewares: Vec::new(),
119            mcp_tool_name: None,
120            mcp_description: None,
121            mcp_hint: None,
122            mcp_hidden: false,
123        }
124    }
125
126    /// Name this route for URL generation
127    pub fn name(mut self, name: &'static str) -> Self {
128        self.name = Some(name);
129        self
130    }
131
132    /// Add middleware to this route
133    pub fn middleware<M: Middleware + 'static>(mut self, middleware: M) -> Self {
134        self.middlewares.push(into_boxed(middleware));
135        self
136    }
137
138    /// Override the auto-generated MCP tool name for this route
139    pub fn mcp_tool_name(mut self, name: &str) -> Self {
140        self.mcp_tool_name = Some(name.to_string());
141        self
142    }
143
144    /// Override the auto-generated MCP description for this route
145    pub fn mcp_description(mut self, desc: &str) -> Self {
146        self.mcp_description = Some(desc.to_string());
147        self
148    }
149
150    /// Add a hint that is appended to the MCP description for AI guidance
151    pub fn mcp_hint(mut self, hint: &str) -> Self {
152        self.mcp_hint = Some(hint.to_string());
153        self
154    }
155
156    /// Hide this route from MCP tool discovery
157    pub fn mcp_hidden(mut self) -> Self {
158        self.mcp_hidden = true;
159        self
160    }
161
162    /// Register this route definition with a router
163    pub fn register(self, router: Router) -> Router {
164        // Convert :param to {param} for matchit compatibility
165        let converted_path = convert_route_params(self.path);
166
167        // Capture MCP fields before moving self
168        let has_mcp = self.mcp_tool_name.is_some()
169            || self.mcp_description.is_some()
170            || self.mcp_hint.is_some()
171            || self.mcp_hidden;
172        let mcp_tool_name = self.mcp_tool_name;
173        let mcp_description = self.mcp_description;
174        let mcp_hint = self.mcp_hint;
175        let mcp_hidden = self.mcp_hidden;
176
177        // First, register the route based on method
178        let builder = match self.method {
179            HttpMethod::Get => router.get(&converted_path, self.handler),
180            HttpMethod::Post => router.post(&converted_path, self.handler),
181            HttpMethod::Put => router.put(&converted_path, self.handler),
182            HttpMethod::Patch => router.patch(&converted_path, self.handler),
183            HttpMethod::Delete => router.delete(&converted_path, self.handler),
184        };
185
186        // Apply any middleware
187        let builder = self
188            .middlewares
189            .into_iter()
190            .fold(builder, |b, m| b.middleware_boxed(m));
191
192        // Apply MCP metadata if any fields are set
193        if has_mcp {
194            update_route_mcp(
195                &converted_path,
196                mcp_tool_name,
197                mcp_description,
198                mcp_hint,
199                mcp_hidden,
200            );
201        }
202
203        // Apply name if present, otherwise convert to Router
204        if let Some(name) = self.name {
205            builder.name(name)
206        } else {
207            builder.into()
208        }
209    }
210}
211
212/// Create a GET route definition with compile-time path validation
213///
214/// # Example
215/// ```rust,ignore
216/// get!("/users", controllers::user::index).name("users.index")
217/// ```
218///
219/// # Compile Error
220///
221/// Fails to compile if path doesn't start with '/'.
222#[macro_export]
223macro_rules! get {
224    ($path:expr, $handler:expr) => {{
225        const _: &str = $crate::validate_route_path($path);
226        $crate::__get_impl($path, $handler)
227    }};
228}
229
230/// Internal implementation for GET routes (used by the get! macro)
231#[doc(hidden)]
232pub fn __get_impl<H, Fut>(path: &'static str, handler: H) -> RouteDefBuilder<H>
233where
234    H: Fn(Request) -> Fut + Send + Sync + 'static,
235    Fut: Future<Output = Response> + Send + 'static,
236{
237    RouteDefBuilder::new(HttpMethod::Get, path, handler)
238}
239
240/// Create a POST route definition with compile-time path validation
241///
242/// # Example
243/// ```rust,ignore
244/// post!("/users", controllers::user::store).name("users.store")
245/// ```
246///
247/// # Compile Error
248///
249/// Fails to compile if path doesn't start with '/'.
250#[macro_export]
251macro_rules! post {
252    ($path:expr, $handler:expr) => {{
253        const _: &str = $crate::validate_route_path($path);
254        $crate::__post_impl($path, $handler)
255    }};
256}
257
258/// Internal implementation for POST routes (used by the post! macro)
259#[doc(hidden)]
260pub fn __post_impl<H, Fut>(path: &'static str, handler: H) -> RouteDefBuilder<H>
261where
262    H: Fn(Request) -> Fut + Send + Sync + 'static,
263    Fut: Future<Output = Response> + Send + 'static,
264{
265    RouteDefBuilder::new(HttpMethod::Post, path, handler)
266}
267
268/// Create a PUT route definition with compile-time path validation
269///
270/// # Example
271/// ```rust,ignore
272/// put!("/users/{id}", controllers::user::update).name("users.update")
273/// ```
274///
275/// # Compile Error
276///
277/// Fails to compile if path doesn't start with '/'.
278#[macro_export]
279macro_rules! put {
280    ($path:expr, $handler:expr) => {{
281        const _: &str = $crate::validate_route_path($path);
282        $crate::__put_impl($path, $handler)
283    }};
284}
285
286/// Internal implementation for PUT routes (used by the put! macro)
287#[doc(hidden)]
288pub fn __put_impl<H, Fut>(path: &'static str, handler: H) -> RouteDefBuilder<H>
289where
290    H: Fn(Request) -> Fut + Send + Sync + 'static,
291    Fut: Future<Output = Response> + Send + 'static,
292{
293    RouteDefBuilder::new(HttpMethod::Put, path, handler)
294}
295
296/// Create a PATCH route definition with compile-time path validation
297///
298/// # Example
299/// ```rust,ignore
300/// patch!("/users/{id}", controllers::user::patch).name("users.patch")
301/// ```
302///
303/// # Compile Error
304///
305/// Fails to compile if path doesn't start with '/'.
306#[macro_export]
307macro_rules! patch {
308    ($path:expr, $handler:expr) => {{
309        const _: &str = $crate::validate_route_path($path);
310        $crate::__patch_impl($path, $handler)
311    }};
312}
313
314/// Internal implementation for PATCH routes (used by the patch! macro)
315#[doc(hidden)]
316pub fn __patch_impl<H, Fut>(path: &'static str, handler: H) -> RouteDefBuilder<H>
317where
318    H: Fn(Request) -> Fut + Send + Sync + 'static,
319    Fut: Future<Output = Response> + Send + 'static,
320{
321    RouteDefBuilder::new(HttpMethod::Patch, path, handler)
322}
323
324/// Create a DELETE route definition with compile-time path validation
325///
326/// # Example
327/// ```rust,ignore
328/// delete!("/users/{id}", controllers::user::destroy).name("users.destroy")
329/// ```
330///
331/// # Compile Error
332///
333/// Fails to compile if path doesn't start with '/'.
334#[macro_export]
335macro_rules! delete {
336    ($path:expr, $handler:expr) => {{
337        const _: &str = $crate::validate_route_path($path);
338        $crate::__delete_impl($path, $handler)
339    }};
340}
341
342/// Internal implementation for DELETE routes (used by the delete! macro)
343#[doc(hidden)]
344pub fn __delete_impl<H, Fut>(path: &'static str, handler: H) -> RouteDefBuilder<H>
345where
346    H: Fn(Request) -> Fut + Send + Sync + 'static,
347    Fut: Future<Output = Response> + Send + 'static,
348{
349    RouteDefBuilder::new(HttpMethod::Delete, path, handler)
350}
351
352// ============================================================================
353// Fallback Route Support
354// ============================================================================
355
356/// Builder for fallback route definitions that supports `.middleware()` chaining
357///
358/// The fallback route is invoked when no other routes match, allowing custom
359/// handling of 404 scenarios.
360pub struct FallbackDefBuilder<H> {
361    handler: H,
362    middlewares: Vec<BoxedMiddleware>,
363}
364
365impl<H, Fut> FallbackDefBuilder<H>
366where
367    H: Fn(Request) -> Fut + Send + Sync + 'static,
368    Fut: Future<Output = Response> + Send + 'static,
369{
370    /// Create a new fallback definition builder
371    pub fn new(handler: H) -> Self {
372        Self {
373            handler,
374            middlewares: Vec::new(),
375        }
376    }
377
378    /// Add middleware to this fallback route
379    pub fn middleware<M: Middleware + 'static>(mut self, middleware: M) -> Self {
380        self.middlewares.push(into_boxed(middleware));
381        self
382    }
383
384    /// Register this fallback definition with a router
385    pub fn register(self, mut router: Router) -> Router {
386        let handler = self.handler;
387        let boxed: BoxedHandler = Box::new(move |req| Box::pin(handler(req)));
388        router.set_fallback(Arc::new(boxed));
389
390        // Apply middleware
391        for mw in self.middlewares {
392            router.add_fallback_middleware(mw);
393        }
394
395        router
396    }
397}
398
399/// Create a fallback route definition
400///
401/// The fallback handler is called when no other routes match the request,
402/// allowing you to override the default 404 behavior.
403///
404/// # Example
405/// ```rust,ignore
406/// routes! {
407///     get!("/", controllers::home::index),
408///     get!("/users", controllers::user::index),
409///
410///     // Custom 404 handler
411///     fallback!(controllers::fallback::invoke),
412/// }
413/// ```
414///
415/// With middleware:
416/// ```rust,ignore
417/// routes! {
418///     get!("/", controllers::home::index),
419///     fallback!(controllers::fallback::invoke).middleware(LoggingMiddleware),
420/// }
421/// ```
422#[macro_export]
423macro_rules! fallback {
424    ($handler:expr) => {{
425        $crate::__fallback_impl($handler)
426    }};
427}
428
429/// Internal implementation for fallback routes (used by the fallback! macro)
430#[doc(hidden)]
431pub fn __fallback_impl<H, Fut>(handler: H) -> FallbackDefBuilder<H>
432where
433    H: Fn(Request) -> Fut + Send + Sync + 'static,
434    Fut: Future<Output = Response> + Send + 'static,
435{
436    FallbackDefBuilder::new(handler)
437}
438
439// ============================================================================
440// Route Grouping Support
441// ============================================================================
442
443/// A route stored within a group (type-erased handler)
444pub struct GroupRoute {
445    method: HttpMethod,
446    path: &'static str,
447    handler: Arc<BoxedHandler>,
448    name: Option<&'static str>,
449    middlewares: Vec<BoxedMiddleware>,
450    mcp_tool_name: Option<String>,
451    mcp_description: Option<String>,
452    mcp_hint: Option<String>,
453    mcp_hidden: bool,
454}
455
456/// An item that can be added to a route group - either a route or a nested group
457pub enum GroupItem {
458    /// A single route
459    Route(GroupRoute),
460    /// A nested group with its own prefix and middleware
461    NestedGroup(Box<GroupDef>),
462}
463
464/// Trait for types that can be converted into a GroupItem
465pub trait IntoGroupItem {
466    /// Convert this value into a group item for the route builder.
467    fn into_group_item(self) -> GroupItem;
468}
469
470/// MCP defaults inherited from parent groups to child routes
471#[derive(Default, Clone)]
472struct McpDefaults {
473    tool_name: Option<String>,
474    description: Option<String>,
475    hint: Option<String>,
476    hidden: bool,
477}
478
479/// Group definition that collects routes and applies prefix/middleware
480///
481/// Supports nested groups for arbitrary route organization:
482///
483/// ```rust,ignore
484/// routes! {
485///     group!("/api", {
486///         get!("/users", controllers::user::index).name("api.users"),
487///         post!("/users", controllers::user::store),
488///         // Nested groups are supported
489///         group!("/admin", {
490///             get!("/dashboard", controllers::admin::dashboard),
491///         }),
492///     }).middleware(AuthMiddleware),
493/// }
494/// ```
495pub struct GroupDef {
496    prefix: &'static str,
497    items: Vec<GroupItem>,
498    group_middlewares: Vec<BoxedMiddleware>,
499    mcp_tool_name: Option<String>,
500    mcp_description: Option<String>,
501    mcp_hint: Option<String>,
502    mcp_hidden: bool,
503}
504
505impl GroupDef {
506    /// Create a new route group with the given prefix (internal use)
507    ///
508    /// Use the `group!` macro instead for compile-time validation.
509    #[doc(hidden)]
510    pub fn __new_unchecked(prefix: &'static str) -> Self {
511        Self {
512            prefix,
513            items: Vec::new(),
514            group_middlewares: Vec::new(),
515            mcp_tool_name: None,
516            mcp_description: None,
517            mcp_hint: None,
518            mcp_hidden: false,
519        }
520    }
521
522    /// Add an item (route or nested group) to this group
523    ///
524    /// This is the primary method for adding items to a group. It accepts
525    /// anything that implements `IntoGroupItem`, including routes created
526    /// with `get!`, `post!`, etc., and nested groups created with `group!`.
527    #[allow(clippy::should_implement_trait)]
528    pub fn add<I: IntoGroupItem>(mut self, item: I) -> Self {
529        self.items.push(item.into_group_item());
530        self
531    }
532
533    /// Add a route to this group (backward compatibility)
534    ///
535    /// Prefer using `.add()` which accepts both routes and nested groups.
536    pub fn route<H, Fut>(self, route: RouteDefBuilder<H>) -> Self
537    where
538        H: Fn(Request) -> Fut + Send + Sync + 'static,
539        Fut: Future<Output = Response> + Send + 'static,
540    {
541        self.add(route)
542    }
543
544    /// Add middleware to all routes in this group
545    ///
546    /// Middleware is applied in the order it's added.
547    ///
548    /// # Example
549    ///
550    /// ```rust,ignore
551    /// group!("/api", {
552    ///     get!("/users", handler),
553    /// }).middleware(AuthMiddleware).middleware(RateLimitMiddleware)
554    /// ```
555    pub fn middleware<M: Middleware + 'static>(mut self, middleware: M) -> Self {
556        self.group_middlewares.push(into_boxed(middleware));
557        self
558    }
559
560    /// Set a default MCP tool name for all routes in this group
561    ///
562    /// Route-level overrides take precedence.
563    pub fn mcp_tool_name(mut self, name: &str) -> Self {
564        self.mcp_tool_name = Some(name.to_string());
565        self
566    }
567
568    /// Set a default MCP description for all routes in this group
569    ///
570    /// Route-level overrides take precedence.
571    pub fn mcp_description(mut self, desc: &str) -> Self {
572        self.mcp_description = Some(desc.to_string());
573        self
574    }
575
576    /// Set a default MCP hint for all routes in this group
577    ///
578    /// Route-level overrides take precedence.
579    pub fn mcp_hint(mut self, hint: &str) -> Self {
580        self.mcp_hint = Some(hint.to_string());
581        self
582    }
583
584    /// Hide all routes in this group from MCP tool discovery
585    ///
586    /// Route-level overrides take precedence.
587    pub fn mcp_hidden(mut self) -> Self {
588        self.mcp_hidden = true;
589        self
590    }
591
592    /// Register all routes in this group with the router
593    ///
594    /// This prepends the group prefix to each route path and applies
595    /// group middleware to all routes. Nested groups are flattened recursively,
596    /// with prefixes concatenated and middleware inherited from parent groups.
597    ///
598    /// # Path Combination
599    ///
600    /// - If route path is "/" (root), the full path is just the group prefix
601    /// - Otherwise, prefix and route path are concatenated
602    ///
603    /// # Middleware Inheritance
604    ///
605    /// Parent group middleware is applied before child group middleware,
606    /// which is applied before route-specific middleware.
607    pub fn register(self, mut router: Router) -> Router {
608        let mcp_defaults = McpDefaults::default();
609        self.register_with_inherited(&mut router, "", &[], &mcp_defaults);
610        router
611    }
612
613    /// Internal recursive registration with inherited prefix and middleware
614    fn register_with_inherited(
615        self,
616        router: &mut Router,
617        parent_prefix: &str,
618        inherited_middleware: &[BoxedMiddleware],
619        inherited_mcp: &McpDefaults,
620    ) {
621        // Build the full prefix for this group
622        let full_prefix = if parent_prefix.is_empty() {
623            self.prefix.to_string()
624        } else {
625            format!("{}{}", parent_prefix, self.prefix)
626        };
627
628        // Combine inherited middleware with this group's middleware
629        // Parent middleware runs first (outer), then this group's middleware
630        let combined_middleware: Vec<BoxedMiddleware> = inherited_middleware
631            .iter()
632            .cloned()
633            .chain(self.group_middlewares.iter().cloned())
634            .collect();
635
636        // Merge MCP defaults: this group's settings override inherited
637        let combined_mcp = McpDefaults {
638            tool_name: self.mcp_tool_name.or(inherited_mcp.tool_name.clone()),
639            description: self.mcp_description.or(inherited_mcp.description.clone()),
640            hint: self.mcp_hint.or(inherited_mcp.hint.clone()),
641            hidden: self.mcp_hidden || inherited_mcp.hidden,
642        };
643
644        for item in self.items {
645            match item {
646                GroupItem::Route(route) => {
647                    // Convert :param to {param} for matchit compatibility
648                    let converted_route_path = convert_route_params(route.path);
649
650                    // Build full path with prefix
651                    // If route path is "/" (root), just use the prefix without trailing slash
652                    let full_path = if converted_route_path == "/" {
653                        if full_prefix.is_empty() {
654                            "/".to_string()
655                        } else {
656                            full_prefix.clone()
657                        }
658                    } else if full_prefix == "/" {
659                        // Prefix is just "/", use route path directly
660                        converted_route_path.to_string()
661                    } else {
662                        format!("{full_prefix}{converted_route_path}")
663                    };
664                    // We need to leak the string to get a 'static str for the router
665                    let full_path: &'static str = Box::leak(full_path.into_boxed_str());
666
667                    // Register the route with the router
668                    match route.method {
669                        HttpMethod::Get => {
670                            router.insert_get(full_path, route.handler);
671                        }
672                        HttpMethod::Post => {
673                            router.insert_post(full_path, route.handler);
674                        }
675                        HttpMethod::Put => {
676                            router.insert_put(full_path, route.handler);
677                        }
678                        HttpMethod::Patch => {
679                            router.insert_patch(full_path, route.handler);
680                        }
681                        HttpMethod::Delete => {
682                            router.insert_delete(full_path, route.handler);
683                        }
684                    }
685
686                    // Register route name if present
687                    if let Some(name) = route.name {
688                        register_route_name(name, full_path);
689                    }
690
691                    // Apply MCP metadata: route-level overrides group defaults
692                    let mcp_tool_name = route.mcp_tool_name.or(combined_mcp.tool_name.clone());
693                    let mcp_description =
694                        route.mcp_description.or(combined_mcp.description.clone());
695                    let mcp_hint = route.mcp_hint.or(combined_mcp.hint.clone());
696                    let mcp_hidden = route.mcp_hidden || combined_mcp.hidden;
697
698                    if mcp_tool_name.is_some()
699                        || mcp_description.is_some()
700                        || mcp_hint.is_some()
701                        || mcp_hidden
702                    {
703                        update_route_mcp(
704                            full_path,
705                            mcp_tool_name,
706                            mcp_description,
707                            mcp_hint,
708                            mcp_hidden,
709                        );
710                    }
711
712                    // Apply combined middleware (inherited + group), then route-specific
713                    for mw in &combined_middleware {
714                        router.add_middleware(full_path, mw.clone());
715                    }
716                    for mw in route.middlewares {
717                        router.add_middleware(full_path, mw);
718                    }
719                }
720                GroupItem::NestedGroup(nested) => {
721                    // Recursively register the nested group with accumulated prefix and middleware
722                    nested.register_with_inherited(
723                        router,
724                        &full_prefix,
725                        &combined_middleware,
726                        &combined_mcp,
727                    );
728                }
729            }
730        }
731    }
732}
733
734impl<H, Fut> RouteDefBuilder<H>
735where
736    H: Fn(Request) -> Fut + Send + Sync + 'static,
737    Fut: Future<Output = Response> + Send + 'static,
738{
739    /// Convert this route definition to a type-erased GroupRoute
740    ///
741    /// This is used internally when adding routes to a group.
742    pub fn into_group_route(self) -> GroupRoute {
743        let handler = self.handler;
744        let boxed: BoxedHandler = Box::new(move |req| Box::pin(handler(req)));
745        GroupRoute {
746            method: self.method,
747            path: self.path,
748            handler: Arc::new(boxed),
749            name: self.name,
750            middlewares: self.middlewares,
751            mcp_tool_name: self.mcp_tool_name,
752            mcp_description: self.mcp_description,
753            mcp_hint: self.mcp_hint,
754            mcp_hidden: self.mcp_hidden,
755        }
756    }
757}
758
759// ============================================================================
760// IntoGroupItem implementations
761// ============================================================================
762
763impl<H, Fut> IntoGroupItem for RouteDefBuilder<H>
764where
765    H: Fn(Request) -> Fut + Send + Sync + 'static,
766    Fut: Future<Output = Response> + Send + 'static,
767{
768    fn into_group_item(self) -> GroupItem {
769        GroupItem::Route(self.into_group_route())
770    }
771}
772
773impl IntoGroupItem for GroupDef {
774    fn into_group_item(self) -> GroupItem {
775        GroupItem::NestedGroup(Box::new(self))
776    }
777}
778
779/// Define a route group with a shared prefix
780///
781/// Routes within a group will have the prefix prepended to their paths.
782/// Middleware can be applied to the entire group using `.middleware()`.
783/// Groups can be nested arbitrarily deep.
784///
785/// # Example
786///
787/// ```rust,ignore
788/// use ferro_rs::{routes, get, post, group};
789///
790/// routes! {
791///     get!("/", controllers::home::index),
792///
793///     // All routes in this group start with /api
794///     group!("/api", {
795///         get!("/users", controllers::user::index),      // -> GET /api/users
796///         post!("/users", controllers::user::store),     // -> POST /api/users
797///
798///         // Nested groups are supported
799///         group!("/admin", {
800///             get!("/dashboard", controllers::admin::dashboard), // -> GET /api/admin/dashboard
801///         }),
802///     }).middleware(AuthMiddleware),  // Applies to ALL routes including nested
803/// }
804/// ```
805///
806/// # Middleware Inheritance
807///
808/// Middleware applied to a parent group is automatically inherited by all nested groups.
809/// The execution order is: parent middleware -> child middleware -> route middleware.
810///
811/// # Compile Error
812///
813/// Fails to compile if prefix doesn't start with '/'.
814#[macro_export]
815macro_rules! group {
816    ($prefix:expr, { $( $item:expr ),* $(,)? }) => {{
817        const _: &str = $crate::validate_route_path($prefix);
818        let mut group = $crate::GroupDef::__new_unchecked($prefix);
819        $(
820            group = group.add($item);
821        )*
822        group
823    }};
824}
825
826/// Define routes with a clean, Laravel-like syntax
827///
828/// This macro generates a `pub fn register() -> Router` function automatically.
829/// Place it at the top level of your `routes.rs` file.
830///
831/// # Example
832/// ```rust,ignore
833/// use ferro_rs::{routes, get, post, put, delete};
834/// use ferro_rs::controllers;
835/// use ferro_rs::middleware::AuthMiddleware;
836///
837/// routes! {
838///     get!("/", controllers::home::index).name("home"),
839///     get!("/users", controllers::user::index).name("users.index"),
840///     get!("/users/{id}", controllers::user::show).name("users.show"),
841///     post!("/users", controllers::user::store).name("users.store"),
842///     put!("/users/{id}", controllers::user::update).name("users.update"),
843///     delete!("/users/{id}", controllers::user::destroy).name("users.destroy"),
844///     get!("/protected", controllers::home::index).middleware(AuthMiddleware),
845/// }
846/// ```
847#[macro_export]
848macro_rules! routes {
849    ( $( $route:expr ),* $(,)? ) => {
850        pub fn register() -> $crate::Router {
851            let mut router = $crate::Router::new();
852            $(
853                router = $route.register(router);
854            )*
855            router
856        }
857    };
858}
859
860// ============================================================================
861// RESTful Resource Routing Support
862// ============================================================================
863
864/// Actions available for resource routing
865#[derive(Clone, Copy, PartialEq, Eq, Debug)]
866pub enum ResourceAction {
867    /// List all resources (GET /resources).
868    Index,
869    /// Show creation form (GET /resources/create).
870    Create,
871    /// Persist a new resource (POST /resources).
872    Store,
873    /// Display a single resource (GET /resources/{id}).
874    Show,
875    /// Show edit form (GET /resources/{id}/edit).
876    Edit,
877    /// Update an existing resource (PUT /resources/{id}).
878    Update,
879    /// Delete a resource (DELETE /resources/{id}).
880    Destroy,
881}
882
883impl ResourceAction {
884    /// Get all available resource actions
885    pub const fn all() -> &'static [ResourceAction] {
886        &[
887            ResourceAction::Index,
888            ResourceAction::Create,
889            ResourceAction::Store,
890            ResourceAction::Show,
891            ResourceAction::Edit,
892            ResourceAction::Update,
893            ResourceAction::Destroy,
894        ]
895    }
896
897    /// Get the HTTP method for this action
898    pub const fn method(&self) -> HttpMethod {
899        match self {
900            ResourceAction::Index => HttpMethod::Get,
901            ResourceAction::Create => HttpMethod::Get,
902            ResourceAction::Store => HttpMethod::Post,
903            ResourceAction::Show => HttpMethod::Get,
904            ResourceAction::Edit => HttpMethod::Get,
905            ResourceAction::Update => HttpMethod::Put,
906            ResourceAction::Destroy => HttpMethod::Delete,
907        }
908    }
909
910    /// Get the path suffix for this action (relative to resource path)
911    pub const fn path_suffix(&self) -> &'static str {
912        match self {
913            ResourceAction::Index => "/",
914            ResourceAction::Create => "/create",
915            ResourceAction::Store => "/",
916            ResourceAction::Show => "/{id}",
917            ResourceAction::Edit => "/{id}/edit",
918            ResourceAction::Update => "/{id}",
919            ResourceAction::Destroy => "/{id}",
920        }
921    }
922
923    /// Get the route name suffix for this action
924    pub const fn name_suffix(&self) -> &'static str {
925        match self {
926            ResourceAction::Index => "index",
927            ResourceAction::Create => "create",
928            ResourceAction::Store => "store",
929            ResourceAction::Show => "show",
930            ResourceAction::Edit => "edit",
931            ResourceAction::Update => "update",
932            ResourceAction::Destroy => "destroy",
933        }
934    }
935}
936
937/// A resource route stored within a ResourceDef (type-erased handler)
938pub struct ResourceRoute {
939    action: ResourceAction,
940    handler: Arc<BoxedHandler>,
941}
942
943/// Resource definition that generates RESTful routes from a controller module
944///
945/// Generates 7 standard routes following Rails/Laravel conventions:
946///
947/// - GET    /users          -> index   (list all)
948/// - GET    /users/create   -> create  (show create form)
949/// - POST   /users          -> store   (create new)
950/// - GET    /users/{id}     -> show    (show one)
951/// - GET    /users/{id}/edit -> edit   (show edit form)
952/// - PUT    /users/{id}     -> update  (update one)
953/// - DELETE /users/{id}     -> destroy (delete one)
954///
955/// Route names are auto-generated: users.index, users.create, etc.
956///
957/// # Example
958///
959/// ```rust,ignore
960/// routes! {
961///     resource!("/users", controllers::user),
962///     resource!("/posts", controllers::post).middleware(AuthMiddleware),
963///     resource!("/comments", controllers::comment, only: [index, show]),
964/// }
965/// ```
966pub struct ResourceDef {
967    prefix: &'static str,
968    routes: Vec<ResourceRoute>,
969    middlewares: Vec<BoxedMiddleware>,
970}
971
972impl ResourceDef {
973    /// Create a new resource definition with no routes (internal use)
974    #[doc(hidden)]
975    pub fn __new_unchecked(prefix: &'static str) -> Self {
976        Self {
977            prefix,
978            routes: Vec::new(),
979            middlewares: Vec::new(),
980        }
981    }
982
983    /// Add a route for a specific action
984    #[doc(hidden)]
985    pub fn __add_route(mut self, action: ResourceAction, handler: Arc<BoxedHandler>) -> Self {
986        self.routes.push(ResourceRoute { action, handler });
987        self
988    }
989
990    /// Add middleware to all routes in this resource
991    ///
992    /// # Example
993    ///
994    /// ```rust,ignore
995    /// resource!("/admin", controllers::admin, only: [index, show])
996    ///     .middleware(AuthMiddleware)
997    ///     .middleware(AdminMiddleware)
998    /// ```
999    pub fn middleware<M: Middleware + 'static>(mut self, middleware: M) -> Self {
1000        self.middlewares.push(into_boxed(middleware));
1001        self
1002    }
1003
1004    /// Register all resource routes with the router
1005    pub fn register(self, mut router: Router) -> Router {
1006        // Derive route name prefix from path: "/users" -> "users", "/api/users" -> "api.users"
1007        let name_prefix = self.prefix.trim_start_matches('/').replace('/', ".");
1008
1009        for route in self.routes {
1010            let action = route.action;
1011            let path_suffix = action.path_suffix();
1012
1013            // Build full path
1014            let full_path = if path_suffix == "/" {
1015                self.prefix.to_string()
1016            } else {
1017                format!("{}{}", self.prefix, path_suffix)
1018            };
1019            let full_path: &'static str = Box::leak(full_path.into_boxed_str());
1020
1021            // Build route name
1022            let route_name = format!("{}.{}", name_prefix, action.name_suffix());
1023            let route_name: &'static str = Box::leak(route_name.into_boxed_str());
1024
1025            // Register the route
1026            match action.method() {
1027                HttpMethod::Get => {
1028                    router.insert_get(full_path, route.handler);
1029                }
1030                HttpMethod::Post => {
1031                    router.insert_post(full_path, route.handler);
1032                }
1033                HttpMethod::Put => {
1034                    router.insert_put(full_path, route.handler);
1035                }
1036                HttpMethod::Patch => {
1037                    router.insert_patch(full_path, route.handler);
1038                }
1039                HttpMethod::Delete => {
1040                    router.insert_delete(full_path, route.handler);
1041                }
1042            }
1043
1044            // Register route name
1045            register_route_name(route_name, full_path);
1046
1047            // Apply middleware
1048            for mw in &self.middlewares {
1049                router.add_middleware(full_path, mw.clone());
1050            }
1051        }
1052
1053        router
1054    }
1055}
1056
1057/// Helper function to create a boxed handler from a handler function
1058#[doc(hidden)]
1059pub fn __box_handler<H, Fut>(handler: H) -> Arc<BoxedHandler>
1060where
1061    H: Fn(Request) -> Fut + Send + Sync + 'static,
1062    Fut: Future<Output = Response> + Send + 'static,
1063{
1064    let boxed: BoxedHandler = Box::new(move |req| Box::pin(handler(req)));
1065    Arc::new(boxed)
1066}
1067
1068/// Define RESTful resource routes with convention-over-configuration
1069///
1070/// Generates 7 standard routes following Rails/Laravel conventions from a
1071/// controller module reference.
1072///
1073/// # Convention Mapping
1074///
1075/// | Method | Path            | Action  | Route Name    |
1076/// |--------|-----------------|---------|---------------|
1077/// | GET    | /users          | index   | users.index   |
1078/// | GET    | /users/create   | create  | users.create  |
1079/// | POST   | /users          | store   | users.store   |
1080/// | GET    | /users/{id}     | show    | users.show    |
1081/// | GET    | /users/{id}/edit| edit    | users.edit    |
1082/// | PUT    | /users/{id}     | update  | users.update  |
1083/// | DELETE | /users/{id}     | destroy | users.destroy |
1084///
1085/// # Basic Usage
1086///
1087/// ```rust,ignore
1088/// routes! {
1089///     resource!("/users", controllers::user),
1090/// }
1091/// ```
1092///
1093/// # With Middleware
1094///
1095/// ```rust,ignore
1096/// routes! {
1097///     resource!("/admin", controllers::admin).middleware(AuthMiddleware),
1098/// }
1099/// ```
1100///
1101/// # Subset of Actions
1102///
1103/// Use `only:` to generate only specific routes:
1104///
1105/// ```rust,ignore
1106/// routes! {
1107///     // Only index, show, and store - no create/edit forms, update, or destroy
1108///     resource!("/posts", controllers::post, only: [index, show, store]),
1109/// }
1110/// ```
1111///
1112/// # Path Naming
1113///
1114/// Route names are derived from the path:
1115/// - `/users` → `users.index`, `users.show`, etc.
1116/// - `/api/users` → `api.users.index`, `api.users.show`, etc.
1117///
1118/// # Compile Error
1119///
1120/// Fails to compile if path doesn't start with '/'.
1121#[macro_export]
1122macro_rules! resource {
1123    // Full resource (all 7 routes)
1124    // Note: The module path is followed by path segments to each handler
1125    ($path:expr, $($controller:ident)::+) => {{
1126        const _: &str = $crate::validate_route_path($path);
1127        $crate::ResourceDef::__new_unchecked($path)
1128            .__add_route($crate::ResourceAction::Index, $crate::__box_handler($($controller)::+::index))
1129            .__add_route($crate::ResourceAction::Create, $crate::__box_handler($($controller)::+::create))
1130            .__add_route($crate::ResourceAction::Store, $crate::__box_handler($($controller)::+::store))
1131            .__add_route($crate::ResourceAction::Show, $crate::__box_handler($($controller)::+::show))
1132            .__add_route($crate::ResourceAction::Edit, $crate::__box_handler($($controller)::+::edit))
1133            .__add_route($crate::ResourceAction::Update, $crate::__box_handler($($controller)::+::update))
1134            .__add_route($crate::ResourceAction::Destroy, $crate::__box_handler($($controller)::+::destroy))
1135    }};
1136
1137    // Subset of routes with `only:` parameter
1138    ($path:expr, $($controller:ident)::+, only: [$($action:ident),* $(,)?]) => {{
1139        const _: &str = $crate::validate_route_path($path);
1140        let mut resource = $crate::ResourceDef::__new_unchecked($path);
1141        $(
1142            resource = resource.__add_route(
1143                $crate::__resource_action!($action),
1144                $crate::__box_handler($($controller)::+::$action)
1145            );
1146        )*
1147        resource
1148    }};
1149}
1150
1151/// Internal macro to convert action identifier to ResourceAction enum
1152#[doc(hidden)]
1153#[macro_export]
1154macro_rules! __resource_action {
1155    (index) => {
1156        $crate::ResourceAction::Index
1157    };
1158    (create) => {
1159        $crate::ResourceAction::Create
1160    };
1161    (store) => {
1162        $crate::ResourceAction::Store
1163    };
1164    (show) => {
1165        $crate::ResourceAction::Show
1166    };
1167    (edit) => {
1168        $crate::ResourceAction::Edit
1169    };
1170    (update) => {
1171        $crate::ResourceAction::Update
1172    };
1173    (destroy) => {
1174        $crate::ResourceAction::Destroy
1175    };
1176}
1177
1178#[cfg(test)]
1179mod tests {
1180    use super::*;
1181
1182    #[test]
1183    fn test_convert_route_params() {
1184        // Basic parameter conversion
1185        assert_eq!(convert_route_params("/users/:id"), "/users/{id}");
1186
1187        // Multiple parameters
1188        assert_eq!(
1189            convert_route_params("/posts/:post_id/comments/:id"),
1190            "/posts/{post_id}/comments/{id}"
1191        );
1192
1193        // Already uses matchit syntax - should be unchanged
1194        assert_eq!(convert_route_params("/users/{id}"), "/users/{id}");
1195
1196        // No parameters - should be unchanged
1197        assert_eq!(convert_route_params("/users"), "/users");
1198        assert_eq!(convert_route_params("/"), "/");
1199
1200        // Mixed syntax (edge case)
1201        assert_eq!(
1202            convert_route_params("/users/:user_id/posts/{post_id}"),
1203            "/users/{user_id}/posts/{post_id}"
1204        );
1205
1206        // Parameter at the end
1207        assert_eq!(
1208            convert_route_params("/api/v1/:version"),
1209            "/api/v1/{version}"
1210        );
1211    }
1212
1213    // Helper for creating test handlers
1214    async fn test_handler(_req: Request) -> Response {
1215        crate::http::text("ok")
1216    }
1217
1218    #[test]
1219    fn test_group_item_route() {
1220        // Test that RouteDefBuilder can be converted to GroupItem
1221        let route_builder = RouteDefBuilder::new(HttpMethod::Get, "/test", test_handler);
1222        let item = route_builder.into_group_item();
1223        matches!(item, GroupItem::Route(_));
1224    }
1225
1226    #[test]
1227    fn test_group_item_nested_group() {
1228        // Test that GroupDef can be converted to GroupItem
1229        let group_def = GroupDef::__new_unchecked("/nested");
1230        let item = group_def.into_group_item();
1231        matches!(item, GroupItem::NestedGroup(_));
1232    }
1233
1234    #[test]
1235    fn test_group_add_route() {
1236        // Test adding a route to a group
1237        let group = GroupDef::__new_unchecked("/api").add(RouteDefBuilder::new(
1238            HttpMethod::Get,
1239            "/users",
1240            test_handler,
1241        ));
1242
1243        assert_eq!(group.items.len(), 1);
1244        matches!(&group.items[0], GroupItem::Route(_));
1245    }
1246
1247    #[test]
1248    fn test_group_add_nested_group() {
1249        // Test adding a nested group to a group
1250        let nested = GroupDef::__new_unchecked("/users");
1251        let group = GroupDef::__new_unchecked("/api").add(nested);
1252
1253        assert_eq!(group.items.len(), 1);
1254        matches!(&group.items[0], GroupItem::NestedGroup(_));
1255    }
1256
1257    #[test]
1258    fn test_group_mixed_items() {
1259        // Test adding both routes and nested groups
1260        let nested = GroupDef::__new_unchecked("/admin");
1261        let group = GroupDef::__new_unchecked("/api")
1262            .add(RouteDefBuilder::new(
1263                HttpMethod::Get,
1264                "/users",
1265                test_handler,
1266            ))
1267            .add(nested)
1268            .add(RouteDefBuilder::new(
1269                HttpMethod::Post,
1270                "/users",
1271                test_handler,
1272            ));
1273
1274        assert_eq!(group.items.len(), 3);
1275        matches!(&group.items[0], GroupItem::Route(_));
1276        matches!(&group.items[1], GroupItem::NestedGroup(_));
1277        matches!(&group.items[2], GroupItem::Route(_));
1278    }
1279
1280    #[test]
1281    fn test_deep_nesting() {
1282        // Test deeply nested groups (3 levels)
1283        let level3 = GroupDef::__new_unchecked("/level3").add(RouteDefBuilder::new(
1284            HttpMethod::Get,
1285            "/",
1286            test_handler,
1287        ));
1288
1289        let level2 = GroupDef::__new_unchecked("/level2").add(level3);
1290
1291        let level1 = GroupDef::__new_unchecked("/level1").add(level2);
1292
1293        assert_eq!(level1.items.len(), 1);
1294        if let GroupItem::NestedGroup(l2) = &level1.items[0] {
1295            assert_eq!(l2.items.len(), 1);
1296            if let GroupItem::NestedGroup(l3) = &l2.items[0] {
1297                assert_eq!(l3.items.len(), 1);
1298            } else {
1299                panic!("Expected nested group at level 2");
1300            }
1301        } else {
1302            panic!("Expected nested group at level 1");
1303        }
1304    }
1305
1306    #[test]
1307    fn test_backward_compatibility_route_method() {
1308        // Test that the old .route() method still works
1309        let group = GroupDef::__new_unchecked("/api").route(RouteDefBuilder::new(
1310            HttpMethod::Get,
1311            "/users",
1312            test_handler,
1313        ));
1314
1315        assert_eq!(group.items.len(), 1);
1316        matches!(&group.items[0], GroupItem::Route(_));
1317    }
1318}