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