Skip to main content

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)] = [..];