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