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