kit_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 kit::{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    Delete,
84}
85
86/// Builder for route definitions that supports `.name()` and `.middleware()` chaining
87pub struct RouteDefBuilder<H> {
88    method: HttpMethod,
89    path: &'static str,
90    handler: H,
91    name: Option<&'static str>,
92    middlewares: Vec<BoxedMiddleware>,
93}
94
95impl<H, Fut> RouteDefBuilder<H>
96where
97    H: Fn(Request) -> Fut + Send + Sync + 'static,
98    Fut: Future<Output = Response> + Send + 'static,
99{
100    /// Create a new route definition builder
101    pub fn new(method: HttpMethod, path: &'static str, handler: H) -> Self {
102        Self {
103            method,
104            path,
105            handler,
106            name: None,
107            middlewares: Vec::new(),
108        }
109    }
110
111    /// Name this route for URL generation
112    pub fn name(mut self, name: &'static str) -> Self {
113        self.name = Some(name);
114        self
115    }
116
117    /// Add middleware to this route
118    pub fn middleware<M: Middleware + 'static>(mut self, middleware: M) -> Self {
119        self.middlewares.push(into_boxed(middleware));
120        self
121    }
122
123    /// Register this route definition with a router
124    pub fn register(self, router: Router) -> Router {
125        // Convert :param to {param} for matchit compatibility
126        let converted_path = convert_route_params(self.path);
127
128        // First, register the route based on method
129        let builder = match self.method {
130            HttpMethod::Get => router.get(&converted_path, self.handler),
131            HttpMethod::Post => router.post(&converted_path, self.handler),
132            HttpMethod::Put => router.put(&converted_path, self.handler),
133            HttpMethod::Delete => router.delete(&converted_path, self.handler),
134        };
135
136        // Apply any middleware
137        let builder = self
138            .middlewares
139            .into_iter()
140            .fold(builder, |b, m| b.middleware_boxed(m));
141
142        // Apply name if present, otherwise convert to Router
143        if let Some(name) = self.name {
144            builder.name(name)
145        } else {
146            builder.into()
147        }
148    }
149}
150
151/// Create a GET route definition with compile-time path validation
152///
153/// # Example
154/// ```rust,ignore
155/// get!("/users", controllers::user::index).name("users.index")
156/// ```
157///
158/// # Compile Error
159///
160/// Fails to compile if path doesn't start with '/'.
161#[macro_export]
162macro_rules! get {
163    ($path:expr, $handler:expr) => {{
164        const _: &str = $crate::validate_route_path($path);
165        $crate::__get_impl($path, $handler)
166    }};
167}
168
169/// Internal implementation for GET routes (used by the get! macro)
170#[doc(hidden)]
171pub fn __get_impl<H, Fut>(path: &'static str, handler: H) -> RouteDefBuilder<H>
172where
173    H: Fn(Request) -> Fut + Send + Sync + 'static,
174    Fut: Future<Output = Response> + Send + 'static,
175{
176    RouteDefBuilder::new(HttpMethod::Get, path, handler)
177}
178
179/// Create a POST route definition with compile-time path validation
180///
181/// # Example
182/// ```rust,ignore
183/// post!("/users", controllers::user::store).name("users.store")
184/// ```
185///
186/// # Compile Error
187///
188/// Fails to compile if path doesn't start with '/'.
189#[macro_export]
190macro_rules! post {
191    ($path:expr, $handler:expr) => {{
192        const _: &str = $crate::validate_route_path($path);
193        $crate::__post_impl($path, $handler)
194    }};
195}
196
197/// Internal implementation for POST routes (used by the post! macro)
198#[doc(hidden)]
199pub fn __post_impl<H, Fut>(path: &'static str, handler: H) -> RouteDefBuilder<H>
200where
201    H: Fn(Request) -> Fut + Send + Sync + 'static,
202    Fut: Future<Output = Response> + Send + 'static,
203{
204    RouteDefBuilder::new(HttpMethod::Post, path, handler)
205}
206
207/// Create a PUT route definition with compile-time path validation
208///
209/// # Example
210/// ```rust,ignore
211/// put!("/users/{id}", controllers::user::update).name("users.update")
212/// ```
213///
214/// # Compile Error
215///
216/// Fails to compile if path doesn't start with '/'.
217#[macro_export]
218macro_rules! put {
219    ($path:expr, $handler:expr) => {{
220        const _: &str = $crate::validate_route_path($path);
221        $crate::__put_impl($path, $handler)
222    }};
223}
224
225/// Internal implementation for PUT routes (used by the put! macro)
226#[doc(hidden)]
227pub fn __put_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::Put, path, handler)
233}
234
235/// Create a DELETE route definition with compile-time path validation
236///
237/// # Example
238/// ```rust,ignore
239/// delete!("/users/{id}", controllers::user::destroy).name("users.destroy")
240/// ```
241///
242/// # Compile Error
243///
244/// Fails to compile if path doesn't start with '/'.
245#[macro_export]
246macro_rules! delete {
247    ($path:expr, $handler:expr) => {{
248        const _: &str = $crate::validate_route_path($path);
249        $crate::__delete_impl($path, $handler)
250    }};
251}
252
253/// Internal implementation for DELETE routes (used by the delete! macro)
254#[doc(hidden)]
255pub fn __delete_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::Delete, path, handler)
261}
262
263// ============================================================================
264// Fallback Route Support
265// ============================================================================
266
267/// Builder for fallback route definitions that supports `.middleware()` chaining
268///
269/// The fallback route is invoked when no other routes match, allowing custom
270/// handling of 404 scenarios.
271pub struct FallbackDefBuilder<H> {
272    handler: H,
273    middlewares: Vec<BoxedMiddleware>,
274}
275
276impl<H, Fut> FallbackDefBuilder<H>
277where
278    H: Fn(Request) -> Fut + Send + Sync + 'static,
279    Fut: Future<Output = Response> + Send + 'static,
280{
281    /// Create a new fallback definition builder
282    pub fn new(handler: H) -> Self {
283        Self {
284            handler,
285            middlewares: Vec::new(),
286        }
287    }
288
289    /// Add middleware to this fallback route
290    pub fn middleware<M: Middleware + 'static>(mut self, middleware: M) -> Self {
291        self.middlewares.push(into_boxed(middleware));
292        self
293    }
294
295    /// Register this fallback definition with a router
296    pub fn register(self, mut router: Router) -> Router {
297        let handler = self.handler;
298        let boxed: BoxedHandler = Box::new(move |req| Box::pin(handler(req)));
299        router.set_fallback(Arc::new(boxed));
300
301        // Apply middleware
302        for mw in self.middlewares {
303            router.add_fallback_middleware(mw);
304        }
305
306        router
307    }
308}
309
310/// Create a fallback route definition
311///
312/// The fallback handler is called when no other routes match the request,
313/// allowing you to override the default 404 behavior.
314///
315/// # Example
316/// ```rust,ignore
317/// routes! {
318///     get!("/", controllers::home::index),
319///     get!("/users", controllers::user::index),
320///
321///     // Custom 404 handler
322///     fallback!(controllers::fallback::invoke),
323/// }
324/// ```
325///
326/// With middleware:
327/// ```rust,ignore
328/// routes! {
329///     get!("/", controllers::home::index),
330///     fallback!(controllers::fallback::invoke).middleware(LoggingMiddleware),
331/// }
332/// ```
333#[macro_export]
334macro_rules! fallback {
335    ($handler:expr) => {{
336        $crate::__fallback_impl($handler)
337    }};
338}
339
340/// Internal implementation for fallback routes (used by the fallback! macro)
341#[doc(hidden)]
342pub fn __fallback_impl<H, Fut>(handler: H) -> FallbackDefBuilder<H>
343where
344    H: Fn(Request) -> Fut + Send + Sync + 'static,
345    Fut: Future<Output = Response> + Send + 'static,
346{
347    FallbackDefBuilder::new(handler)
348}
349
350// ============================================================================
351// Route Grouping Support
352// ============================================================================
353
354/// A route stored within a group (type-erased handler)
355pub struct GroupRoute {
356    method: HttpMethod,
357    path: &'static str,
358    handler: Arc<BoxedHandler>,
359    name: Option<&'static str>,
360    middlewares: Vec<BoxedMiddleware>,
361}
362
363/// An item that can be added to a route group - either a route or a nested group
364pub enum GroupItem {
365    /// A single route
366    Route(GroupRoute),
367    /// A nested group with its own prefix and middleware
368    NestedGroup(Box<GroupDef>),
369}
370
371/// Trait for types that can be converted into a GroupItem
372pub trait IntoGroupItem {
373    fn into_group_item(self) -> GroupItem;
374}
375
376/// Group definition that collects routes and applies prefix/middleware
377///
378/// Supports nested groups for arbitrary route organization:
379///
380/// ```rust,ignore
381/// routes! {
382///     group!("/api", {
383///         get!("/users", controllers::user::index).name("api.users"),
384///         post!("/users", controllers::user::store),
385///         // Nested groups are supported
386///         group!("/admin", {
387///             get!("/dashboard", controllers::admin::dashboard),
388///         }),
389///     }).middleware(AuthMiddleware),
390/// }
391/// ```
392pub struct GroupDef {
393    prefix: &'static str,
394    items: Vec<GroupItem>,
395    group_middlewares: Vec<BoxedMiddleware>,
396}
397
398impl GroupDef {
399    /// Create a new route group with the given prefix (internal use)
400    ///
401    /// Use the `group!` macro instead for compile-time validation.
402    #[doc(hidden)]
403    pub fn __new_unchecked(prefix: &'static str) -> Self {
404        Self {
405            prefix,
406            items: Vec::new(),
407            group_middlewares: Vec::new(),
408        }
409    }
410
411    /// Add an item (route or nested group) to this group
412    ///
413    /// This is the primary method for adding items to a group. It accepts
414    /// anything that implements `IntoGroupItem`, including routes created
415    /// with `get!`, `post!`, etc., and nested groups created with `group!`.
416    pub fn add<I: IntoGroupItem>(mut self, item: I) -> Self {
417        self.items.push(item.into_group_item());
418        self
419    }
420
421    /// Add a route to this group (backward compatibility)
422    ///
423    /// Prefer using `.add()` which accepts both routes and nested groups.
424    pub fn route<H, Fut>(self, route: RouteDefBuilder<H>) -> Self
425    where
426        H: Fn(Request) -> Fut + Send + Sync + 'static,
427        Fut: Future<Output = Response> + Send + 'static,
428    {
429        self.add(route)
430    }
431
432    /// Add middleware to all routes in this group
433    ///
434    /// Middleware is applied in the order it's added.
435    ///
436    /// # Example
437    ///
438    /// ```rust,ignore
439    /// group!("/api", {
440    ///     get!("/users", handler),
441    /// }).middleware(AuthMiddleware).middleware(RateLimitMiddleware)
442    /// ```
443    pub fn middleware<M: Middleware + 'static>(mut self, middleware: M) -> Self {
444        self.group_middlewares.push(into_boxed(middleware));
445        self
446    }
447
448    /// Register all routes in this group with the router
449    ///
450    /// This prepends the group prefix to each route path and applies
451    /// group middleware to all routes. Nested groups are flattened recursively,
452    /// with prefixes concatenated and middleware inherited from parent groups.
453    ///
454    /// # Path Combination
455    ///
456    /// - If route path is "/" (root), the full path is just the group prefix
457    /// - Otherwise, prefix and route path are concatenated
458    ///
459    /// # Middleware Inheritance
460    ///
461    /// Parent group middleware is applied before child group middleware,
462    /// which is applied before route-specific middleware.
463    pub fn register(self, mut router: Router) -> Router {
464        self.register_with_inherited(&mut router, "", &[]);
465        router
466    }
467
468    /// Internal recursive registration with inherited prefix and middleware
469    fn register_with_inherited(
470        self,
471        router: &mut Router,
472        parent_prefix: &str,
473        inherited_middleware: &[BoxedMiddleware],
474    ) {
475        // Build the full prefix for this group
476        let full_prefix = if parent_prefix.is_empty() {
477            self.prefix.to_string()
478        } else {
479            format!("{}{}", parent_prefix, self.prefix)
480        };
481
482        // Combine inherited middleware with this group's middleware
483        // Parent middleware runs first (outer), then this group's middleware
484        let combined_middleware: Vec<BoxedMiddleware> = inherited_middleware
485            .iter()
486            .cloned()
487            .chain(self.group_middlewares.iter().cloned())
488            .collect();
489
490        for item in self.items {
491            match item {
492                GroupItem::Route(route) => {
493                    // Convert :param to {param} for matchit compatibility
494                    let converted_route_path = convert_route_params(route.path);
495
496                    // Build full path with prefix
497                    // If route path is "/" (root), just use the prefix without trailing slash
498                    let full_path = if converted_route_path == "/" {
499                        full_prefix.clone()
500                    } else {
501                        format!("{}{}", full_prefix, converted_route_path)
502                    };
503                    // We need to leak the string to get a 'static str for the router
504                    let full_path: &'static str = Box::leak(full_path.into_boxed_str());
505
506                    // Register the route with the router
507                    match route.method {
508                        HttpMethod::Get => {
509                            router.insert_get(full_path, route.handler);
510                        }
511                        HttpMethod::Post => {
512                            router.insert_post(full_path, route.handler);
513                        }
514                        HttpMethod::Put => {
515                            router.insert_put(full_path, route.handler);
516                        }
517                        HttpMethod::Delete => {
518                            router.insert_delete(full_path, route.handler);
519                        }
520                    }
521
522                    // Register route name if present
523                    if let Some(name) = route.name {
524                        register_route_name(name, full_path);
525                    }
526
527                    // Apply combined middleware (inherited + group), then route-specific
528                    for mw in &combined_middleware {
529                        router.add_middleware(full_path, mw.clone());
530                    }
531                    for mw in route.middlewares {
532                        router.add_middleware(full_path, mw);
533                    }
534                }
535                GroupItem::NestedGroup(nested) => {
536                    // Recursively register the nested group with accumulated prefix and middleware
537                    nested.register_with_inherited(router, &full_prefix, &combined_middleware);
538                }
539            }
540        }
541    }
542}
543
544impl<H, Fut> RouteDefBuilder<H>
545where
546    H: Fn(Request) -> Fut + Send + Sync + 'static,
547    Fut: Future<Output = Response> + Send + 'static,
548{
549    /// Convert this route definition to a type-erased GroupRoute
550    ///
551    /// This is used internally when adding routes to a group.
552    pub fn into_group_route(self) -> GroupRoute {
553        let handler = self.handler;
554        let boxed: BoxedHandler = Box::new(move |req| Box::pin(handler(req)));
555        GroupRoute {
556            method: self.method,
557            path: self.path,
558            handler: Arc::new(boxed),
559            name: self.name,
560            middlewares: self.middlewares,
561        }
562    }
563}
564
565// ============================================================================
566// IntoGroupItem implementations
567// ============================================================================
568
569impl<H, Fut> IntoGroupItem for RouteDefBuilder<H>
570where
571    H: Fn(Request) -> Fut + Send + Sync + 'static,
572    Fut: Future<Output = Response> + Send + 'static,
573{
574    fn into_group_item(self) -> GroupItem {
575        GroupItem::Route(self.into_group_route())
576    }
577}
578
579impl IntoGroupItem for GroupDef {
580    fn into_group_item(self) -> GroupItem {
581        GroupItem::NestedGroup(Box::new(self))
582    }
583}
584
585/// Define a route group with a shared prefix
586///
587/// Routes within a group will have the prefix prepended to their paths.
588/// Middleware can be applied to the entire group using `.middleware()`.
589/// Groups can be nested arbitrarily deep.
590///
591/// # Example
592///
593/// ```rust,ignore
594/// use kit::{routes, get, post, group};
595///
596/// routes! {
597///     get!("/", controllers::home::index),
598///
599///     // All routes in this group start with /api
600///     group!("/api", {
601///         get!("/users", controllers::user::index),      // -> GET /api/users
602///         post!("/users", controllers::user::store),     // -> POST /api/users
603///
604///         // Nested groups are supported
605///         group!("/admin", {
606///             get!("/dashboard", controllers::admin::dashboard), // -> GET /api/admin/dashboard
607///         }),
608///     }).middleware(AuthMiddleware),  // Applies to ALL routes including nested
609/// }
610/// ```
611///
612/// # Middleware Inheritance
613///
614/// Middleware applied to a parent group is automatically inherited by all nested groups.
615/// The execution order is: parent middleware -> child middleware -> route middleware.
616///
617/// # Compile Error
618///
619/// Fails to compile if prefix doesn't start with '/'.
620#[macro_export]
621macro_rules! group {
622    ($prefix:expr, { $( $item:expr ),* $(,)? }) => {{
623        const _: &str = $crate::validate_route_path($prefix);
624        let mut group = $crate::GroupDef::__new_unchecked($prefix);
625        $(
626            group = group.add($item);
627        )*
628        group
629    }};
630}
631
632/// Define routes with a clean, Laravel-like syntax
633///
634/// This macro generates a `pub fn register() -> Router` function automatically.
635/// Place it at the top level of your `routes.rs` file.
636///
637/// # Example
638/// ```rust,ignore
639/// use kit::{routes, get, post, put, delete};
640/// use crate::controllers;
641/// use crate::middleware::AuthMiddleware;
642///
643/// routes! {
644///     get!("/", controllers::home::index).name("home"),
645///     get!("/users", controllers::user::index).name("users.index"),
646///     get!("/users/{id}", controllers::user::show).name("users.show"),
647///     post!("/users", controllers::user::store).name("users.store"),
648///     put!("/users/{id}", controllers::user::update).name("users.update"),
649///     delete!("/users/{id}", controllers::user::destroy).name("users.destroy"),
650///     get!("/protected", controllers::home::index).middleware(AuthMiddleware),
651/// }
652/// ```
653#[macro_export]
654macro_rules! routes {
655    ( $( $route:expr ),* $(,)? ) => {
656        pub fn register() -> $crate::Router {
657            let mut router = $crate::Router::new();
658            $(
659                router = $route.register(router);
660            )*
661            router
662        }
663    };
664}
665
666#[cfg(test)]
667mod tests {
668    use super::*;
669
670    #[test]
671    fn test_convert_route_params() {
672        // Basic parameter conversion
673        assert_eq!(convert_route_params("/users/:id"), "/users/{id}");
674
675        // Multiple parameters
676        assert_eq!(
677            convert_route_params("/posts/:post_id/comments/:id"),
678            "/posts/{post_id}/comments/{id}"
679        );
680
681        // Already uses matchit syntax - should be unchanged
682        assert_eq!(convert_route_params("/users/{id}"), "/users/{id}");
683
684        // No parameters - should be unchanged
685        assert_eq!(convert_route_params("/users"), "/users");
686        assert_eq!(convert_route_params("/"), "/");
687
688        // Mixed syntax (edge case)
689        assert_eq!(
690            convert_route_params("/users/:user_id/posts/{post_id}"),
691            "/users/{user_id}/posts/{post_id}"
692        );
693
694        // Parameter at the end
695        assert_eq!(convert_route_params("/api/v1/:version"), "/api/v1/{version}");
696    }
697
698    // Helper for creating test handlers
699    async fn test_handler(_req: Request) -> Response {
700        crate::http::text("ok")
701    }
702
703    #[test]
704    fn test_group_item_route() {
705        // Test that RouteDefBuilder can be converted to GroupItem
706        let route_builder = RouteDefBuilder::new(HttpMethod::Get, "/test", test_handler);
707        let item = route_builder.into_group_item();
708        matches!(item, GroupItem::Route(_));
709    }
710
711    #[test]
712    fn test_group_item_nested_group() {
713        // Test that GroupDef can be converted to GroupItem
714        let group_def = GroupDef::__new_unchecked("/nested");
715        let item = group_def.into_group_item();
716        matches!(item, GroupItem::NestedGroup(_));
717    }
718
719    #[test]
720    fn test_group_add_route() {
721        // Test adding a route to a group
722        let group = GroupDef::__new_unchecked("/api")
723            .add(RouteDefBuilder::new(HttpMethod::Get, "/users", test_handler));
724
725        assert_eq!(group.items.len(), 1);
726        matches!(&group.items[0], GroupItem::Route(_));
727    }
728
729    #[test]
730    fn test_group_add_nested_group() {
731        // Test adding a nested group to a group
732        let nested = GroupDef::__new_unchecked("/users");
733        let group = GroupDef::__new_unchecked("/api").add(nested);
734
735        assert_eq!(group.items.len(), 1);
736        matches!(&group.items[0], GroupItem::NestedGroup(_));
737    }
738
739    #[test]
740    fn test_group_mixed_items() {
741        // Test adding both routes and nested groups
742        let nested = GroupDef::__new_unchecked("/admin");
743        let group = GroupDef::__new_unchecked("/api")
744            .add(RouteDefBuilder::new(HttpMethod::Get, "/users", test_handler))
745            .add(nested)
746            .add(RouteDefBuilder::new(HttpMethod::Post, "/users", test_handler));
747
748        assert_eq!(group.items.len(), 3);
749        matches!(&group.items[0], GroupItem::Route(_));
750        matches!(&group.items[1], GroupItem::NestedGroup(_));
751        matches!(&group.items[2], GroupItem::Route(_));
752    }
753
754    #[test]
755    fn test_deep_nesting() {
756        // Test deeply nested groups (3 levels)
757        let level3 = GroupDef::__new_unchecked("/level3")
758            .add(RouteDefBuilder::new(HttpMethod::Get, "/", test_handler));
759
760        let level2 = GroupDef::__new_unchecked("/level2").add(level3);
761
762        let level1 = GroupDef::__new_unchecked("/level1").add(level2);
763
764        assert_eq!(level1.items.len(), 1);
765        if let GroupItem::NestedGroup(l2) = &level1.items[0] {
766            assert_eq!(l2.items.len(), 1);
767            if let GroupItem::NestedGroup(l3) = &l2.items[0] {
768                assert_eq!(l3.items.len(), 1);
769            } else {
770                panic!("Expected nested group at level 2");
771            }
772        } else {
773            panic!("Expected nested group at level 1");
774        }
775    }
776
777    #[test]
778    fn test_backward_compatibility_route_method() {
779        // Test that the old .route() method still works
780        let group = GroupDef::__new_unchecked("/api")
781            .route(RouteDefBuilder::new(HttpMethod::Get, "/users", test_handler));
782
783        assert_eq!(group.items.len(), 1);
784        matches!(&group.items[0], GroupItem::Route(_));
785    }
786}