Skip to main content

ferro_rs/routing/
group.rs

1//! Route grouping with shared prefix and middleware
2
3use super::{BoxedHandler, RouteBuilder, Router};
4use crate::http::{Request, Response};
5use crate::middleware::{into_boxed, BoxedMiddleware, Middleware};
6use std::future::Future;
7use std::sync::Arc;
8
9/// Builder for route groups with shared prefix and middleware
10///
11/// # Example
12///
13/// ```rust,ignore
14/// Router::new()
15///     .group("/api", |r| {
16///         r.get("/users", list_users)
17///          .post("/users", create_user)
18///     }).middleware(ApiMiddleware)
19/// ```
20pub struct GroupBuilder {
21    /// The outer router we're building into
22    outer_router: Router,
23    /// Routes registered within this group (stored as full paths)
24    group_routes: Vec<GroupRoute>,
25    /// The prefix for this group
26    prefix: String,
27    /// Middleware to apply to all routes in this group
28    middleware: Vec<BoxedMiddleware>,
29}
30
31/// A route registered within a group
32struct GroupRoute {
33    method: GroupMethod,
34    path: String,
35    handler: Arc<BoxedHandler>,
36}
37
38#[derive(Clone, Copy)]
39enum GroupMethod {
40    Get,
41    Post,
42    Put,
43    Delete,
44}
45
46impl GroupBuilder {
47    /// Apply middleware to all routes in this group
48    ///
49    /// # Example
50    ///
51    /// ```rust,ignore
52    /// Router::new()
53    ///     .group("/api", |r| r.get("/users", handler))
54    ///     .middleware(ApiMiddleware)
55    /// ```
56    pub fn middleware<M: Middleware + 'static>(mut self, middleware: M) -> Self {
57        self.middleware.push(into_boxed(middleware));
58        self
59    }
60
61    /// Finalize the group and merge routes into the outer router
62    fn finalize(mut self) -> Router {
63        // Insert all group routes into the outer router with the prefix
64        for route in self.group_routes {
65            let full_path = format!("{}{}", self.prefix, route.path);
66
67            // Insert into the appropriate method router using public(crate) methods
68            match route.method {
69                GroupMethod::Get => {
70                    self.outer_router.insert_get(&full_path, route.handler);
71                }
72                GroupMethod::Post => {
73                    self.outer_router.insert_post(&full_path, route.handler);
74                }
75                GroupMethod::Put => {
76                    self.outer_router.insert_put(&full_path, route.handler);
77                }
78                GroupMethod::Delete => {
79                    self.outer_router.insert_delete(&full_path, route.handler);
80                }
81            }
82
83            // Apply group middleware to each route
84            for mw in &self.middleware {
85                self.outer_router.add_middleware(&full_path, mw.clone());
86            }
87        }
88
89        self.outer_router
90    }
91}
92
93/// Inner router used within a group closure
94///
95/// This captures routes without a prefix, which are later merged with the group's prefix.
96pub struct GroupRouter {
97    routes: Vec<GroupRoute>,
98}
99
100impl GroupRouter {
101    fn new() -> Self {
102        Self { routes: Vec::new() }
103    }
104
105    /// Register a GET route within the group
106    pub fn get<H, Fut>(mut self, path: &str, handler: H) -> Self
107    where
108        H: Fn(Request) -> Fut + Send + Sync + 'static,
109        Fut: Future<Output = Response> + Send + 'static,
110    {
111        let boxed: BoxedHandler = Box::new(move |req| Box::pin(handler(req)));
112        self.routes.push(GroupRoute {
113            method: GroupMethod::Get,
114            path: path.to_string(),
115            handler: Arc::new(boxed),
116        });
117        self
118    }
119
120    /// Register a POST route within the group
121    pub fn post<H, Fut>(mut self, path: &str, handler: H) -> Self
122    where
123        H: Fn(Request) -> Fut + Send + Sync + 'static,
124        Fut: Future<Output = Response> + Send + 'static,
125    {
126        let boxed: BoxedHandler = Box::new(move |req| Box::pin(handler(req)));
127        self.routes.push(GroupRoute {
128            method: GroupMethod::Post,
129            path: path.to_string(),
130            handler: Arc::new(boxed),
131        });
132        self
133    }
134
135    /// Register a PUT route within the group
136    pub fn put<H, Fut>(mut self, path: &str, handler: H) -> Self
137    where
138        H: Fn(Request) -> Fut + Send + Sync + 'static,
139        Fut: Future<Output = Response> + Send + 'static,
140    {
141        let boxed: BoxedHandler = Box::new(move |req| Box::pin(handler(req)));
142        self.routes.push(GroupRoute {
143            method: GroupMethod::Put,
144            path: path.to_string(),
145            handler: Arc::new(boxed),
146        });
147        self
148    }
149
150    /// Register a DELETE route within the group
151    pub fn delete<H, Fut>(mut self, path: &str, handler: H) -> Self
152    where
153        H: Fn(Request) -> Fut + Send + Sync + 'static,
154        Fut: Future<Output = Response> + Send + 'static,
155    {
156        let boxed: BoxedHandler = Box::new(move |req| Box::pin(handler(req)));
157        self.routes.push(GroupRoute {
158            method: GroupMethod::Delete,
159            path: path.to_string(),
160            handler: Arc::new(boxed),
161        });
162        self
163    }
164}
165
166impl Router {
167    /// Create a route group with a shared prefix
168    ///
169    /// Routes defined within the group will have the prefix prepended to their paths.
170    /// Middleware applied to the group will be applied to all routes within it.
171    ///
172    /// # Example
173    ///
174    /// ```rust,ignore
175    /// Router::new()
176    ///     .group("/api", |r| {
177    ///         r.get("/users", list_users)      // -> GET /api/users
178    ///          .post("/users", create_user)    // -> POST /api/users
179    ///          .get("/users/{id}", show_user)  // -> GET /api/users/{id}
180    ///     })
181    ///     .middleware(ApiMiddleware)
182    /// ```
183    pub fn group<F>(self, prefix: &str, builder_fn: F) -> GroupBuilder
184    where
185        F: FnOnce(GroupRouter) -> GroupRouter,
186    {
187        let inner = GroupRouter::new();
188        let built = builder_fn(inner);
189
190        GroupBuilder {
191            outer_router: self,
192            group_routes: built.routes,
193            prefix: prefix.to_string(),
194            middleware: Vec::new(),
195        }
196    }
197}
198
199impl From<GroupBuilder> for Router {
200    fn from(builder: GroupBuilder) -> Self {
201        builder.finalize()
202    }
203}
204
205// Allow RouteBuilder to chain into groups
206impl RouteBuilder {
207    /// Create a route group with a shared prefix
208    pub fn group<F>(self, prefix: &str, builder_fn: F) -> GroupBuilder
209    where
210        F: FnOnce(GroupRouter) -> GroupRouter,
211    {
212        self.router.group(prefix, builder_fn)
213    }
214}