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}