tako_rs_core/router/mounting.rs
1//! Router composition: macro mounting, prefix scoping, nesting, and merging.
2
3use std::sync::Arc;
4use std::sync::atomic::Ordering;
5
6use super::Router;
7
8impl Router {
9 /// Registers every route declared via the `#[tako::route]` / `#[tako::get]`
10 /// (and friends) attribute macros into this router.
11 ///
12 /// Each macro contributes a thunk into the global [`TAKO_ROUTES`] slice at
13 /// link time; this method walks the slice and invokes each thunk against
14 /// `self`, which calls [`Router::route`] under the hood. Routes are
15 /// registered in the order the linker emits them — typically the order they
16 /// appear within a translation unit, but unspecified across crates. If two
17 /// thunks register the same `(method, path)` pair, the second call will
18 /// panic, matching the behavior of [`Router::route`].
19 ///
20 /// # Why `linkme` and not explicit registration
21 ///
22 /// We keep the `linkme` distributed-slice strategy on purpose. The
23 /// alternative — an explicit `register_routes!(my_crate::routes)` invocation
24 /// per crate — was considered and rejected because:
25 ///
26 /// * Adding a handler would require touching three places (the handler
27 /// itself, the per-crate registration list, and the call site that
28 /// imports it) instead of one. The macro authoring story is the main
29 /// reason teams pick attribute routing in the first place.
30 /// * Cross-crate path collisions panic at startup either way; explicit
31 /// registration does not buy any extra safety.
32 /// * Link-order non-determinism only matters when two routes share a
33 /// `(method, path)` pair — that is already a hard failure and a CI test
34 /// catches it deterministically.
35 /// * Prefix grouping is already covered by [`Router::mount_all_into`], so
36 /// "I want all my routes under `/api`" does not require explicit
37 /// registration.
38 ///
39 /// Callers that need stable, deterministic ordering should call
40 /// [`Router::route`] directly.
41 ///
42 /// # Examples
43 ///
44 /// ```ignore
45 /// use tako::{get, router::Router};
46 ///
47 /// #[get("/health")]
48 /// async fn health() -> impl tako::responder::Responder { "ok" }
49 ///
50 /// let mut router = Router::new();
51 /// router.mount_all();
52 /// ```
53 pub fn mount_all(&mut self) -> &mut Self {
54 for register in TAKO_ROUTES {
55 register(self);
56 }
57 self
58 }
59
60 /// Like [`Router::mount_all`] but registers every macro-declared route under
61 /// the given path prefix. The prefix is normalized (trailing `/` stripped),
62 /// then prepended to each registered path. Useful when you want, e.g., all
63 /// `#[get("/users")]` declarations to live under `/api`.
64 ///
65 /// Ordering across crates remains the linker's choice (see
66 /// [`Router::mount_all`] for details).
67 ///
68 /// # Examples
69 ///
70 /// ```ignore
71 /// let mut router = Router::new();
72 /// router.mount_all_into("/api"); // /users → /api/users, /health → /api/health
73 /// ```
74 pub fn mount_all_into(&mut self, prefix: &str) -> &mut Self {
75 let saved = self.pending_prefix.take();
76 self.pending_prefix = Some(prefix.to_string());
77 // Same panic-restore guard as `scope`: a route conflict from any
78 // registered `#[tako_route]` macro now resets `pending_prefix`.
79 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
80 for register in TAKO_ROUTES {
81 register(self);
82 }
83 }));
84 self.pending_prefix = saved;
85 if let Err(payload) = result {
86 std::panic::resume_unwind(payload);
87 }
88 self
89 }
90
91 /// Registers a group of routes under a shared path prefix.
92 ///
93 /// The closure receives `self` with the prefix active, so any `route()` /
94 /// `get()` / `post()` etc. calls inside register the routes with the prefix
95 /// prepended. Prefixes nest: a `scope("/v1", |r| r.scope("/users", …))`
96 /// produces routes under `/v1/users`. Cold path; no dispatch impact.
97 ///
98 /// # Examples
99 ///
100 /// ```rust
101 /// use tako::router::Router;
102 /// use tako::responder::Responder;
103 ///
104 /// async fn list_users() -> impl Responder { "users" }
105 /// async fn create_user() -> impl Responder { "created" }
106 ///
107 /// let mut router = Router::new();
108 /// router.scope("/api/v1", |r| {
109 /// r.get("/users", list_users);
110 /// r.post("/users", create_user);
111 /// });
112 /// ```
113 pub fn scope<F>(&mut self, prefix: &str, build: F) -> &mut Self
114 where
115 F: FnOnce(&mut Router),
116 {
117 let saved = self.pending_prefix.take();
118 let new_prefix = match &saved {
119 Some(parent) => {
120 let parent = parent.trim_end_matches('/');
121 if prefix.starts_with('/') {
122 format!("{parent}{prefix}")
123 } else {
124 format!("{parent}/{prefix}")
125 }
126 }
127 None => prefix.to_string(),
128 };
129 self.pending_prefix = Some(new_prefix);
130 // Panic-safe restore of `pending_prefix`. A route-conflict panic in the
131 // user-supplied `build` closure used to leave the temporary nested
132 // prefix in place, permanently poisoning subsequent route registrations
133 // on the same builder.
134 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| build(self)));
135 self.pending_prefix = saved;
136 if let Err(payload) = result {
137 std::panic::resume_unwind(payload);
138 }
139 self
140 }
141
142 /// Mounts every route from a child router under the given path prefix.
143 ///
144 /// Unlike [`Router::merge`], `nest` builds **new** `Arc<Route>` instances for
145 /// each child route via `Route::cloned_with_path` — so re-nesting the same
146 /// child cannot double-stack its global middleware onto the same shared
147 /// `Arc<Route>`. The child router's global middleware chain is prepended to
148 /// each newly-registered route's middleware chain (so child globals run
149 /// before child-route middleware at dispatch time).
150 ///
151 /// Caveats:
152 /// - Route-level plugins on the child are **not** carried over.
153 /// - The child's fallback / error handlers are **not** inherited.
154 ///
155 /// # Panics
156 ///
157 /// Panics at registration time if mounting the child would conflict with a
158 /// route already present on `self` (same method + same prefixed path).
159 /// Mirrors the behavior of [`Router::route`] — route registration is a
160 /// startup-time operation and conflicts are configuration bugs, not
161 /// runtime conditions.
162 ///
163 /// # Examples
164 ///
165 /// ```rust
166 /// use tako::router::Router;
167 /// use tako::responder::Responder;
168 ///
169 /// async fn list_users() -> impl Responder { "users" }
170 ///
171 /// let mut api = Router::new();
172 /// api.get("/users", list_users);
173 ///
174 /// let mut root = Router::new();
175 /// root.nest("/api/v1", api); // /users → /api/v1/users
176 /// ```
177 pub fn nest(&mut self, prefix: &str, child: Router) -> &mut Self {
178 let upstream_globals = child.middlewares.load_full();
179
180 for (method, weak_vec) in child.routes.iter() {
181 for weak in weak_vec {
182 let Some(child_route) = weak.upgrade() else {
183 continue;
184 };
185
186 let combined = combine_prefix_path(prefix, &child_route.path);
187 let new_path = self.apply_pending_prefix(&combined);
188
189 let new_route = child_route.cloned_with_path(new_path.clone());
190
191 if !upstream_globals.is_empty() {
192 let existing = new_route.middlewares.load_full();
193 let mut merged = Vec::with_capacity(upstream_globals.len() + existing.len());
194 merged.extend(upstream_globals.iter().cloned());
195 merged.extend(existing.iter().cloned());
196 new_route.has_middleware.store(true, Ordering::Release);
197 new_route.middlewares.store(Arc::new(merged));
198 }
199
200 if let Err(err) = self
201 .inner
202 .get_or_default_mut(&method)
203 .insert(new_path, new_route.clone())
204 {
205 panic!("Failed to nest route: {err}");
206 }
207 self
208 .routes
209 .get_or_default_mut(&method)
210 .push(Arc::downgrade(&new_route));
211 }
212 }
213
214 #[cfg(feature = "signals")]
215 self.signals.merge_from(&child.signals);
216
217 self
218 }
219
220 /// Merges another router into this router.
221 ///
222 /// This method combines routes and middleware from another router into the
223 /// current one. Routes are copied over, and the other router's global middleware
224 /// is prepended to each merged route's middleware chain.
225 ///
226 /// # Panics
227 ///
228 /// Panics at registration time if a merged route conflicts with one already
229 /// present on `self` (same method + same path). Mirrors the behavior of
230 /// [`Router::route`] and [`Router::nest`] — merge is a startup-time
231 /// operation and route conflicts are configuration bugs.
232 ///
233 /// # Examples
234 ///
235 /// ```rust
236 /// use tako::{router::Router, Method, responder::Responder, types::Request};
237 ///
238 /// async fn api_handler(_req: Request) -> impl Responder {
239 /// "API response"
240 /// }
241 ///
242 /// async fn web_handler(_req: Request) -> impl Responder {
243 /// "Web response"
244 /// }
245 ///
246 /// // Create API router
247 /// let mut api_router = Router::new();
248 /// api_router.route(Method::GET, "/users", api_handler);
249 /// api_router.middleware(|req, next| async move {
250 /// println!("API middleware");
251 /// next.run(req).await
252 /// });
253 ///
254 /// // Create main router and merge API router
255 /// let mut main_router = Router::new();
256 /// main_router.route(Method::GET, "/", web_handler);
257 /// main_router.merge(api_router);
258 /// ```
259 pub fn merge(&mut self, other: Router) {
260 let upstream_globals = other.middlewares.load_full();
261
262 for (method, weak_vec) in other.routes.iter() {
263 for weak in weak_vec {
264 if let Some(child_route) = weak.upgrade() {
265 // Re-issue the route as a fresh `Arc<Route>` (same path) so we do
266 // not mutate the child's middleware chain in-place — other router
267 // instances may still hold the original `Arc` and would observe
268 // unrelated middleware insertions otherwise.
269 let new_route = child_route.cloned_with_path(child_route.path.clone());
270
271 if !upstream_globals.is_empty() {
272 let existing = new_route.middlewares.load_full();
273 let mut merged = Vec::with_capacity(upstream_globals.len() + existing.len());
274 merged.extend(upstream_globals.iter().cloned());
275 merged.extend(existing.iter().cloned());
276 new_route.has_middleware.store(true, Ordering::Release);
277 new_route.middlewares.store(Arc::new(merged));
278 }
279
280 // Match `nest` semantics: a path conflict is a builder bug, not a
281 // silent overwrite. Returning early via `let _ = … insert` would
282 // throw away the existing route under a stable URL.
283 if let Err(err) = self
284 .inner
285 .get_or_default_mut(&method)
286 .insert(new_route.path.clone(), new_route.clone())
287 {
288 panic!(
289 "Failed to merge route '{}' (method {:?}): {err}",
290 new_route.path, method
291 );
292 }
293
294 self
295 .routes
296 .get_or_default_mut(&method)
297 .push(Arc::downgrade(&new_route));
298 }
299 }
300 }
301
302 #[cfg(feature = "signals")]
303 self.signals.merge_from(&other.signals);
304 }
305}
306
307/// Joins a path prefix and a child path, normalising the boundary slash.
308fn combine_prefix_path(prefix: &str, path: &str) -> String {
309 if prefix.is_empty() || prefix == "/" {
310 return path.to_string();
311 }
312 let prefix = prefix.trim_end_matches('/');
313 if path.is_empty() || path == "/" {
314 return prefix.to_string();
315 }
316 if path.starts_with('/') {
317 let mut out = String::with_capacity(prefix.len() + path.len());
318 out.push_str(prefix);
319 out.push_str(path);
320 out
321 } else {
322 let mut out = String::with_capacity(prefix.len() + 1 + path.len());
323 out.push_str(prefix);
324 out.push('/');
325 out.push_str(path);
326 out
327 }
328}
329
330/// Distributed slice of route registration thunks.
331///
332/// Each `#[tako::route]` / `#[tako::get]` / etc. attribute contributes a
333/// `fn(&mut Router)` closure that calls [`Router::route`] with the
334/// generated `Params::METHOD` / `Params::PATH` and the handler. Iterating
335/// the slice — what [`Router::mount_all`] does — replays every contribution
336/// against the supplied router.
337#[linkme::distributed_slice]
338pub static TAKO_ROUTES: [fn(&mut Router)] = [..];