1use super::path::combine_group_path;
4use super::{BoxedHandler, RouteBuilder, Router};
5use crate::http::{Request, Response};
6use crate::middleware::{into_boxed, BoxedMiddleware, Middleware};
7use std::future::Future;
8use std::sync::Arc;
9
10pub struct GroupBuilder {
22 outer_router: Router,
24 group_routes: Vec<GroupRoute>,
26 prefix: String,
28 middleware: Vec<BoxedMiddleware>,
30}
31
32struct GroupRoute {
34 method: GroupMethod,
35 path: String,
36 handler: Arc<BoxedHandler>,
37}
38
39#[derive(Clone, Copy)]
40enum GroupMethod {
41 Get,
42 Post,
43 Put,
44 Patch,
45 Delete,
46}
47
48impl GroupBuilder {
49 pub fn middleware<M: Middleware + 'static>(mut self, middleware: M) -> Self {
59 self.middleware.push(into_boxed(middleware));
60 self
61 }
62
63 fn finalize(mut self) -> Router {
73 for route in self.group_routes {
74 let (canonical, alternate) = combine_group_path(&self.prefix, &route.path);
75
76 match route.method {
77 GroupMethod::Get => {
78 self.outer_router
79 .insert_get(&canonical, route.handler.clone());
80 if let Some(alt) = alternate.as_deref() {
81 self.outer_router
82 .insert_get_alias(alt, route.handler, &canonical);
83 }
84 }
85 GroupMethod::Post => {
86 self.outer_router
87 .insert_post(&canonical, route.handler.clone());
88 if let Some(alt) = alternate.as_deref() {
89 self.outer_router
90 .insert_post_alias(alt, route.handler, &canonical);
91 }
92 }
93 GroupMethod::Put => {
94 self.outer_router
95 .insert_put(&canonical, route.handler.clone());
96 if let Some(alt) = alternate.as_deref() {
97 self.outer_router
98 .insert_put_alias(alt, route.handler, &canonical);
99 }
100 }
101 GroupMethod::Patch => {
102 self.outer_router
103 .insert_patch(&canonical, route.handler.clone());
104 if let Some(alt) = alternate.as_deref() {
105 self.outer_router
106 .insert_patch_alias(alt, route.handler, &canonical);
107 }
108 }
109 GroupMethod::Delete => {
110 self.outer_router
111 .insert_delete(&canonical, route.handler.clone());
112 if let Some(alt) = alternate.as_deref() {
113 self.outer_router
114 .insert_delete_alias(alt, route.handler, &canonical);
115 }
116 }
117 }
118
119 for mw in &self.middleware {
124 self.outer_router.add_middleware(&canonical, mw.clone());
125 }
126 }
127
128 self.outer_router
129 }
130}
131
132pub struct GroupRouter {
136 routes: Vec<GroupRoute>,
137}
138
139impl GroupRouter {
140 fn new() -> Self {
141 Self { routes: Vec::new() }
142 }
143
144 pub fn get<H, Fut>(mut self, path: &str, handler: H) -> Self
146 where
147 H: Fn(Request) -> Fut + Send + Sync + 'static,
148 Fut: Future<Output = Response> + Send + 'static,
149 {
150 let boxed: BoxedHandler = Box::new(move |req| Box::pin(handler(req)));
151 self.routes.push(GroupRoute {
152 method: GroupMethod::Get,
153 path: path.to_string(),
154 handler: Arc::new(boxed),
155 });
156 self
157 }
158
159 pub fn post<H, Fut>(mut self, path: &str, handler: H) -> Self
161 where
162 H: Fn(Request) -> Fut + Send + Sync + 'static,
163 Fut: Future<Output = Response> + Send + 'static,
164 {
165 let boxed: BoxedHandler = Box::new(move |req| Box::pin(handler(req)));
166 self.routes.push(GroupRoute {
167 method: GroupMethod::Post,
168 path: path.to_string(),
169 handler: Arc::new(boxed),
170 });
171 self
172 }
173
174 pub fn put<H, Fut>(mut self, path: &str, handler: H) -> Self
176 where
177 H: Fn(Request) -> Fut + Send + Sync + 'static,
178 Fut: Future<Output = Response> + Send + 'static,
179 {
180 let boxed: BoxedHandler = Box::new(move |req| Box::pin(handler(req)));
181 self.routes.push(GroupRoute {
182 method: GroupMethod::Put,
183 path: path.to_string(),
184 handler: Arc::new(boxed),
185 });
186 self
187 }
188
189 pub fn patch<H, Fut>(mut self, path: &str, handler: H) -> Self
191 where
192 H: Fn(Request) -> Fut + Send + Sync + 'static,
193 Fut: Future<Output = Response> + Send + 'static,
194 {
195 let boxed: BoxedHandler = Box::new(move |req| Box::pin(handler(req)));
196 self.routes.push(GroupRoute {
197 method: GroupMethod::Patch,
198 path: path.to_string(),
199 handler: Arc::new(boxed),
200 });
201 self
202 }
203
204 pub fn delete<H, Fut>(mut self, path: &str, handler: H) -> Self
206 where
207 H: Fn(Request) -> Fut + Send + Sync + 'static,
208 Fut: Future<Output = Response> + Send + 'static,
209 {
210 let boxed: BoxedHandler = Box::new(move |req| Box::pin(handler(req)));
211 self.routes.push(GroupRoute {
212 method: GroupMethod::Delete,
213 path: path.to_string(),
214 handler: Arc::new(boxed),
215 });
216 self
217 }
218}
219
220impl Router {
221 pub fn group<F>(self, prefix: &str, builder_fn: F) -> GroupBuilder
238 where
239 F: FnOnce(GroupRouter) -> GroupRouter,
240 {
241 let inner = GroupRouter::new();
242 let built = builder_fn(inner);
243
244 GroupBuilder {
245 outer_router: self,
246 group_routes: built.routes,
247 prefix: prefix.to_string(),
248 middleware: Vec::new(),
249 }
250 }
251}
252
253impl From<GroupBuilder> for Router {
254 fn from(builder: GroupBuilder) -> Self {
255 builder.finalize()
256 }
257}
258
259impl RouteBuilder {
261 pub fn group<F>(self, prefix: &str, builder_fn: F) -> GroupBuilder
263 where
264 F: FnOnce(GroupRouter) -> GroupRouter,
265 {
266 self.router.group(prefix, builder_fn)
267 }
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273 use crate::routing::get_registered_routes;
274 use hyper::Method;
275
276 async fn test_handler(_req: Request) -> Response {
277 crate::http::text("ok")
278 }
279
280 #[test]
281 fn builder_group_root_handler_matches_both_variants() {
282 let router: Router = Router::new()
286 .group("/api-b01", |r| r.get("/", test_handler))
287 .into();
288
289 let routes = get_registered_routes();
291 let count = routes.iter().filter(|r| r.path == "/api-b01").count();
292 assert_eq!(
293 count, 1,
294 "expected exactly 1 RouteInfo entry for /api-b01, got {count}"
295 );
296
297 let hit_canonical = router.match_route(&Method::GET, "/api-b01");
299 assert!(hit_canonical.is_some(), "canonical /api-b01 did not match");
300 assert_eq!(hit_canonical.unwrap().2, "/api-b01");
301
302 let hit_alternate = router.match_route(&Method::GET, "/api-b01/");
304 assert!(hit_alternate.is_some(), "alternate /api-b01/ did not match");
305 assert_eq!(
306 hit_alternate.unwrap().2,
307 "/api-b01",
308 "alternate leaf must carry canonical pattern for middleware lookup"
309 );
310 }
311
312 #[test]
313 fn builder_root_prefix_root_handler_is_single_slash() {
314 let router: Router = Router::new()
316 .group("/", |r| r.get("/", test_handler))
317 .into();
318
319 let hit = router.match_route(&Method::GET, "/");
320 assert!(hit.is_some(), "/ did not match");
321 assert_eq!(hit.unwrap().2, "/");
322
323 let double = router.match_route(&Method::GET, "//");
324 assert!(
325 double.is_none(),
326 "// must not be registered for root-in-root group"
327 );
328 }
329
330 #[test]
331 fn builder_trailing_slash_prefix_is_stripped() {
332 let router: Router = Router::new()
334 .group("/api-b03/", |r| r.get("/x", test_handler))
335 .into();
336
337 assert!(
338 router.match_route(&Method::GET, "/api-b03/x").is_some(),
339 "/api-b03/x did not match after trailing-slash strip"
340 );
341 assert!(
342 router.match_route(&Method::GET, "/api-b03//x").is_none(),
343 "unexpected match for /api-b03//x — trailing slash not stripped"
344 );
345 }
346
347 #[test]
348 fn builder_non_root_prefix_non_root_path_unchanged() {
349 let router: Router = Router::new()
351 .group("/api-b04", |r| r.get("/users", test_handler))
352 .into();
353
354 let routes = get_registered_routes();
355 let count = routes.iter().filter(|r| r.path == "/api-b04/users").count();
356 assert_eq!(
357 count, 1,
358 "expected exactly 1 RouteInfo for /api-b04/users, got {count}"
359 );
360
361 assert!(
362 router.match_route(&Method::GET, "/api-b04/users").is_some(),
363 "/api-b04/users did not match"
364 );
365 assert!(
366 router
367 .match_route(&Method::GET, "/api-b04/users/")
368 .is_none(),
369 "non-root leaf must not emit an alternate"
370 );
371 }
372
373 #[test]
374 fn builder_middleware_registered_under_canonical_only() {
375 let router: Router = Router::new()
380 .group("/api-b05", |r| r.get("/", test_handler))
381 .into();
382
383 assert!(
387 router.get_route_middleware("/api-b05").is_empty(),
388 "canonical key must return empty vec (no middleware registered)"
389 );
390 assert!(
391 router.get_route_middleware("/api-b05/").is_empty(),
392 "alias key must return empty vec (Strategy A: no separate entry)"
393 );
394 }
395
396 #[test]
397 fn builder_post_and_put_and_patch_and_delete_aliases_reach_handler() {
398 let router_post: Router = Router::new()
401 .group("/api-b06p", |r| r.post("/", test_handler))
402 .into();
403 assert!(
404 router_post
405 .match_route(&Method::POST, "/api-b06p")
406 .is_some(),
407 "POST /api-b06p did not match"
408 );
409 assert!(
410 router_post
411 .match_route(&Method::POST, "/api-b06p/")
412 .is_some(),
413 "POST /api-b06p/ did not match"
414 );
415
416 let router_put: Router = Router::new()
417 .group("/api-b06u", |r| r.put("/", test_handler))
418 .into();
419 assert!(
420 router_put.match_route(&Method::PUT, "/api-b06u").is_some(),
421 "PUT /api-b06u did not match"
422 );
423 assert!(
424 router_put.match_route(&Method::PUT, "/api-b06u/").is_some(),
425 "PUT /api-b06u/ did not match"
426 );
427
428 let router_patch: Router = Router::new()
429 .group("/api-b06a", |r| r.patch("/", test_handler))
430 .into();
431 assert!(
432 router_patch
433 .match_route(&Method::PATCH, "/api-b06a")
434 .is_some(),
435 "PATCH /api-b06a did not match"
436 );
437 assert!(
438 router_patch
439 .match_route(&Method::PATCH, "/api-b06a/")
440 .is_some(),
441 "PATCH /api-b06a/ did not match"
442 );
443
444 let router_delete: Router = Router::new()
445 .group("/api-b06d", |r| r.delete("/", test_handler))
446 .into();
447 assert!(
448 router_delete
449 .match_route(&Method::DELETE, "/api-b06d")
450 .is_some(),
451 "DELETE /api-b06d did not match"
452 );
453 assert!(
454 router_delete
455 .match_route(&Method::DELETE, "/api-b06d/")
456 .is_some(),
457 "DELETE /api-b06d/ did not match"
458 );
459 }
460}