tako/router.rs
1//! HTTP request routing and dispatch functionality.
2//!
3//! This module provides the core `Router` struct that manages HTTP routes, middleware chains,
4//! and request dispatching. The router supports dynamic path parameters, middleware composition,
5//! plugin integration, and global state management. It handles matching incoming requests to
6//! registered routes and executing the appropriate handlers through middleware pipelines.
7//!
8//! # Examples
9//!
10//! ```rust
11//! use tako::{router::Router, Method, responder::Responder, types::Request};
12//!
13//! async fn hello(_req: Request) -> impl Responder {
14//! "Hello, World!"
15//! }
16//!
17//! async fn user_handler(_req: Request) -> impl Responder {
18//! "User profile"
19//! }
20//!
21//! let mut router = Router::new();
22//! router.route(Method::GET, "/", hello);
23//! router.route(Method::GET, "/users/{id}", user_handler);
24//!
25//! // Add global middleware
26//! router.middleware(|req, next| async move {
27//! println!("Processing request to: {}", req.uri());
28//! next.run(req).await
29//! });
30//! ```
31
32use std::collections::HashMap;
33use std::sync::Arc;
34use std::sync::Weak;
35#[cfg(feature = "plugins")]
36use std::sync::atomic::AtomicBool;
37
38use http::Method;
39use http::StatusCode;
40use parking_lot::RwLock;
41use scc::HashMap as SccHashMap;
42
43use crate::body::TakoBody;
44use crate::extractors::params::PathParams;
45use crate::handler::BoxHandler;
46use crate::handler::Handler;
47use crate::middleware::Next;
48#[cfg(feature = "plugins")]
49use crate::plugins::TakoPlugin;
50use crate::responder::Responder;
51use crate::route::Route;
52#[cfg(feature = "signals")]
53use crate::signals::Signal;
54#[cfg(feature = "signals")]
55use crate::signals::SignalArbiter;
56#[cfg(feature = "signals")]
57use crate::signals::ids;
58use crate::state::set_state;
59use crate::types::BoxMiddleware;
60use crate::types::BuildHasher;
61use crate::types::Request;
62use crate::types::Response;
63
64/// HTTP router for managing routes, middleware, and request dispatching.
65///
66/// The `Router` is the central component for routing HTTP requests to appropriate
67/// handlers. It supports dynamic path parameters, middleware chains, plugin integration,
68/// and global state management. Routes are matched based on HTTP method and path pattern,
69/// with support for trailing slash redirection and parameter extraction.
70///
71/// # Examples
72///
73/// ```rust
74/// use tako::{router::Router, Method, responder::Responder, types::Request};
75///
76/// async fn index(_req: Request) -> impl Responder {
77/// "Welcome to the home page!"
78/// }
79///
80/// async fn user_profile(_req: Request) -> impl Responder {
81/// "User profile page"
82/// }
83///
84/// let mut router = Router::new();
85/// router.route(Method::GET, "/", index);
86/// router.route(Method::GET, "/users/{id}", user_profile);
87/// router.state("app_name", "MyApp".to_string());
88/// ```
89#[doc(alias = "router")]
90pub struct Router {
91 /// Map of registered routes keyed by method.
92 inner: SccHashMap<Method, matchit::Router<Arc<Route>>>,
93 /// An easy-to-iterate index of the same routes so we can access the `Arc<Route>` values
94 routes: SccHashMap<Method, Vec<Weak<Route>>>,
95 /// Global middleware chain applied to all routes.
96 pub(crate) middlewares: RwLock<Vec<BoxMiddleware>>,
97 /// Optional fallback handler executed when no route matches.
98 fallback: Option<BoxHandler>,
99 /// Registered plugins for extending functionality.
100 #[cfg(feature = "plugins")]
101 plugins: Vec<Box<dyn TakoPlugin>>,
102 /// Flag to ensure plugins are initialized only once.
103 #[cfg(feature = "plugins")]
104 plugins_initialized: AtomicBool,
105 /// Signal arbiter for in-process event emission and handling.
106 #[cfg(feature = "signals")]
107 signals: SignalArbiter,
108}
109
110impl Router {
111 /// Creates a new, empty router.
112 pub fn new() -> Self {
113 let router = Self {
114 inner: SccHashMap::default(),
115 routes: SccHashMap::default(),
116 middlewares: RwLock::new(Vec::new()),
117 fallback: None,
118 #[cfg(feature = "plugins")]
119 plugins: Vec::new(),
120 #[cfg(feature = "plugins")]
121 plugins_initialized: AtomicBool::new(false),
122 #[cfg(feature = "signals")]
123 signals: SignalArbiter::new(),
124 };
125
126 #[cfg(feature = "signals")]
127 {
128 // If not already present, expose router-level SignalArbiter via global state
129 if crate::state::get_state::<SignalArbiter>().is_none() {
130 set_state::<SignalArbiter>(router.signals.clone());
131 }
132 }
133
134 router
135 }
136
137 /// Registers a new route with the router.
138 ///
139 /// Associates an HTTP method and path pattern with a handler function. The path
140 /// can contain dynamic segments using curly braces (e.g., `/users/{id}`), which
141 /// are extracted as parameters during request processing.
142 ///
143 /// # Examples
144 ///
145 /// ```rust
146 /// use tako::{router::Router, Method, responder::Responder, types::Request};
147 ///
148 /// async fn get_user(_req: Request) -> impl Responder {
149 /// "User details"
150 /// }
151 ///
152 /// async fn create_user(_req: Request) -> impl Responder {
153 /// "User created"
154 /// }
155 ///
156 /// let mut router = Router::new();
157 /// router.route(Method::GET, "/users/{id}", get_user);
158 /// router.route(Method::POST, "/users", create_user);
159 /// router.route(Method::GET, "/health", |_req| async { "OK" });
160 /// ```
161 pub fn route<H, T>(&mut self, method: Method, path: &str, handler: H) -> Arc<Route>
162 where
163 H: Handler<T> + Clone + 'static,
164 {
165 let route = Arc::new(Route::new(
166 path.to_string(),
167 method.clone(),
168 BoxHandler::new::<H, T>(handler),
169 None,
170 ));
171
172 let mut method_router = self.inner.entry_sync(method.clone()).or_default();
173
174 if let Err(err) = method_router
175 .get_mut()
176 .insert(path.to_string(), route.clone())
177 {
178 panic!("Failed to register route: {err}");
179 }
180
181 self
182 .routes
183 .entry_sync(method)
184 .or_default()
185 .push(Arc::downgrade(&route));
186
187 route
188 }
189
190 /// Registers a route with trailing slash redirection enabled.
191 ///
192 /// When TSR is enabled, requests to paths with or without trailing slashes
193 /// are automatically redirected to the canonical version. This helps maintain
194 /// consistent URLs and prevents duplicate content issues.
195 ///
196 /// # Panics
197 ///
198 /// Panics if called with the root path ("/") since TSR is not applicable.
199 ///
200 /// # Examples
201 ///
202 /// ```rust
203 /// use tako::{router::Router, Method, responder::Responder, types::Request};
204 ///
205 /// async fn api_handler(_req: Request) -> impl Responder {
206 /// "API endpoint"
207 /// }
208 ///
209 /// let mut router = Router::new();
210 /// // Both "/api" and "/api/" will redirect to the canonical form
211 /// router.route_with_tsr(Method::GET, "/api", api_handler);
212 /// ```
213 pub fn route_with_tsr<H, T>(&mut self, method: Method, path: &str, handler: H) -> Arc<Route>
214 where
215 H: Handler<T> + Clone + 'static,
216 {
217 if path == "/" {
218 panic!("Cannot route with TSR for root path");
219 }
220
221 let route = Arc::new(Route::new(
222 path.to_string(),
223 method.clone(),
224 BoxHandler::new::<H, T>(handler),
225 Some(true),
226 ));
227
228 let mut method_router = self.inner.entry_sync(method.clone()).or_default();
229
230 if let Err(err) = method_router
231 .get_mut()
232 .insert(path.to_string(), route.clone())
233 {
234 panic!("Failed to register route: {err}");
235 }
236
237 self
238 .routes
239 .entry_sync(method)
240 .or_default()
241 .push(Arc::downgrade(&route));
242
243 route
244 }
245
246 /// Executes the given endpoint through the global middleware chain.
247 ///
248 /// This helper is used for cases like TSR redirects and default 404 responses,
249 /// ensuring that router-level middleware (e.g., CORS) always runs.
250 async fn run_with_global_middlewares_for_endpoint(
251 &self,
252 req: Request,
253 endpoint: BoxHandler,
254 ) -> Response {
255 let g_mws = self.middlewares.read().clone();
256 let next = Next {
257 middlewares: Arc::new(g_mws),
258 endpoint: Arc::new(endpoint),
259 };
260
261 next.run(req).await
262 }
263
264 /// Dispatches an incoming request to the appropriate route handler.
265 pub async fn dispatch(&self, mut req: Request) -> Response {
266 let method = req.method().clone();
267 let path = req.uri().path().to_string();
268
269 if let Some(method_router) = self.inner.get_sync(&method)
270 && let Ok(matched) = method_router.at(&path)
271 {
272 let route = matched.value;
273
274 // Protocol guard: early-return if request version does not satisfy route guard
275 if let Some(res) = Self::enforce_protocol_guard(route, &req) {
276 return res;
277 }
278
279 #[cfg(feature = "signals")]
280 let route_signals = route.signal_arbiter();
281
282 // Initialize route-level plugins on first request
283 #[cfg(feature = "plugins")]
284 route.setup_plugins_once();
285
286 if !matched.params.iter().collect::<Vec<_>>().is_empty() {
287 let mut params: HashMap<String, String, BuildHasher> =
288 HashMap::with_hasher(BuildHasher::default());
289 for (k, v) in matched.params.iter() {
290 params.insert(k.to_string(), v.to_string());
291 }
292 req.extensions_mut().insert(PathParams(params));
293 }
294 let g_mws = self.middlewares.read().clone();
295 let r_mws = route.middlewares.read().clone();
296 let mut chain = Vec::new();
297 chain.extend(g_mws.into_iter());
298 chain.extend(r_mws.into_iter());
299
300 let next = Next {
301 middlewares: Arc::new(chain),
302 endpoint: Arc::new(route.handler.clone()),
303 };
304
305 #[cfg(feature = "signals")]
306 {
307 let method_str = method.to_string();
308 let path_str = path.clone();
309
310 let mut start_meta: HashMap<String, String, BuildHasher> =
311 HashMap::with_hasher(BuildHasher::default());
312 start_meta.insert("method".to_string(), method_str.clone());
313 start_meta.insert("path".to_string(), path_str.clone());
314 route_signals
315 .emit(Signal::with_metadata(
316 ids::ROUTE_REQUEST_STARTED,
317 start_meta,
318 ))
319 .await;
320
321 let response = next.run(req).await;
322
323 let mut done_meta: HashMap<String, String, BuildHasher> =
324 HashMap::with_hasher(BuildHasher::default());
325 done_meta.insert("method".to_string(), method_str);
326 done_meta.insert("path".to_string(), path_str);
327 done_meta.insert("status".to_string(), response.status().as_u16().to_string());
328 route_signals
329 .emit(Signal::with_metadata(
330 ids::ROUTE_REQUEST_COMPLETED,
331 done_meta,
332 ))
333 .await;
334
335 return response;
336 }
337
338 #[cfg(not(feature = "signals"))]
339 {
340 return next.run(req).await;
341 }
342 }
343
344 let tsr_path = if path.ends_with('/') {
345 path.trim_end_matches('/').to_string()
346 } else {
347 format!("{path}/")
348 };
349
350 if let Some(method_router) = self.inner.get_sync(&method)
351 && let Ok(matched) = method_router.at(&tsr_path)
352 && matched.value.tsr
353 {
354 let handler = move |_req: Request| async move {
355 http::Response::builder()
356 .status(StatusCode::TEMPORARY_REDIRECT)
357 .header("Location", tsr_path.clone())
358 .body(TakoBody::empty())
359 .unwrap()
360 };
361
362 return self
363 .run_with_global_middlewares_for_endpoint(req, BoxHandler::new::<_, (Request,)>(handler))
364 .await;
365 }
366
367 // No match: use fallback handler if configured
368 if let Some(handler) = &self.fallback {
369 return self
370 .run_with_global_middlewares_for_endpoint(req, handler.clone())
371 .await;
372 }
373
374 // No fallback: run global middlewares (if any) around a default 404 response
375 let handler = |_req: Request| async {
376 http::Response::builder()
377 .status(StatusCode::NOT_FOUND)
378 .body(TakoBody::empty())
379 .unwrap()
380 };
381
382 self
383 .run_with_global_middlewares_for_endpoint(req, BoxHandler::new::<_, (Request,)>(handler))
384 .await
385 }
386
387 /// Adds a value to the global type-based state accessible by all handlers.
388 ///
389 /// Global state allows sharing data across different routes and middleware.
390 /// Values are stored by their concrete type and retrieved via the
391 /// [`State`](crate::extractors::state::State) extractor or with
392 /// [`crate::state::get_state`].
393 ///
394 /// # Examples
395 ///
396 /// ```rust
397 /// use tako::router::Router;
398 ///
399 /// #[derive(Clone)]
400 /// struct AppConfig { database_url: String, api_key: String }
401 ///
402 /// let mut router = Router::new();
403 /// router.state(AppConfig {
404 /// database_url: "postgresql://localhost/mydb".to_string(),
405 /// api_key: "secret-key".to_string(),
406 /// });
407 /// // You can also store simple types by type:
408 /// router.state::<String>("1.0.0".to_string());
409 /// ```
410 pub fn state<T: Clone + Send + Sync + 'static>(&mut self, value: T) {
411 set_state(value);
412 }
413
414 #[cfg(feature = "signals")]
415 /// Returns a reference to the signal arbiter.
416 pub fn signals(&self) -> &SignalArbiter {
417 &self.signals
418 }
419
420 #[cfg(feature = "signals")]
421 /// Returns a clone of the signal arbiter, useful for sharing through state.
422 pub fn signal_arbiter(&self) -> SignalArbiter {
423 self.signals.clone()
424 }
425
426 #[cfg(feature = "signals")]
427 /// Registers a handler for a named signal on this router's arbiter.
428 pub fn on_signal<F, Fut>(&self, id: impl Into<String>, handler: F)
429 where
430 F: Fn(Signal) -> Fut + Send + Sync + 'static,
431 Fut: std::future::Future<Output = ()> + Send + 'static,
432 {
433 self.signals.on(id, handler);
434 }
435
436 #[cfg(feature = "signals")]
437 /// Emits a signal through this router's arbiter.
438 pub async fn emit_signal(&self, signal: Signal) {
439 self.signals.emit(signal).await;
440 }
441
442 /// Adds global middleware to the router.
443 ///
444 /// Global middleware is executed for all routes in the order it was added,
445 /// before any route-specific middleware. Middleware can modify requests,
446 /// generate responses, or perform side effects like logging or authentication.
447 ///
448 /// # Examples
449 ///
450 /// ```rust
451 /// use tako::{router::Router, middleware::Next, types::Request};
452 ///
453 /// let mut router = Router::new();
454 ///
455 /// // Logging middleware
456 /// router.middleware(|req, next| async move {
457 /// println!("Request: {} {}", req.method(), req.uri());
458 /// let response = next.run(req).await;
459 /// println!("Response: {}", response.status());
460 /// response
461 /// });
462 ///
463 /// // Authentication middleware
464 /// router.middleware(|req, next| async move {
465 /// if req.headers().contains_key("authorization") {
466 /// next.run(req).await
467 /// } else {
468 /// "Unauthorized".into_response()
469 /// }
470 /// });
471 /// ```
472 pub fn middleware<F, Fut, R>(&self, f: F) -> &Self
473 where
474 F: Fn(Request, Next) -> Fut + Clone + Send + Sync + 'static,
475 Fut: std::future::Future<Output = R> + Send + 'static,
476 R: Responder + Send + 'static,
477 {
478 let mw: BoxMiddleware = Arc::new(move |req, next| {
479 let fut = f(req, next);
480 Box::pin(async move { fut.await.into_response() })
481 });
482
483 self.middlewares.write().push(mw);
484 self
485 }
486
487 /// Sets a fallback handler that will be executed when no route matches.
488 ///
489 /// The fallback runs after global middlewares and can be used to implement
490 /// custom 404 pages, catch-all logic, or method-independent handlers.
491 ///
492 /// # Examples
493 ///
494 /// ```rust
495 /// use tako::{router::Router, Method, responder::Responder, types::Request};
496 ///
497 /// async fn not_found(_req: Request) -> impl Responder { "Not Found" }
498 ///
499 /// let mut router = Router::new();
500 /// router.route(Method::GET, "/", |_req| async { "Hello" });
501 /// router.fallback(not_found);
502 /// ```
503 pub fn fallback<F, Fut, R>(&mut self, handler: F) -> &mut Self
504 where
505 F: Fn(Request) -> Fut + Clone + Send + Sync + 'static,
506 Fut: std::future::Future<Output = R> + Send + 'static,
507 R: Responder + Send + 'static,
508 {
509 // Use the Request-arg handler impl to box the fallback
510 self.fallback = Some(BoxHandler::new::<F, (Request,)>(handler));
511 self
512 }
513
514 /// Sets a fallback handler that supports extractors (like `Path`, `Query`, etc.).
515 ///
516 /// Use this when your fallback needs to parse request data via extractors. If you
517 /// only need access to the raw `Request`, prefer `fallback` for simpler type inference.
518 ///
519 /// # Examples
520 ///
521 /// ```rust
522 /// use tako::{router::Router, responder::Responder, extractors::{path::Path, query::Query}};
523 ///
524 /// #[derive(serde::Deserialize)]
525 /// struct Q { q: Option<String> }
526 ///
527 /// async fn fallback_with_q(Path(_p): Path<String>, Query(_q): Query<Q>) -> impl Responder {
528 /// "Not Found"
529 /// }
530 ///
531 /// let mut router = Router::new();
532 /// router.fallback_with_extractors(fallback_with_q);
533 /// ```
534 pub fn fallback_with_extractors<H, T>(&mut self, handler: H) -> &mut Self
535 where
536 H: Handler<T> + Clone + 'static,
537 {
538 self.fallback = Some(BoxHandler::new::<H, T>(handler));
539 self
540 }
541
542 /// Registers a plugin with the router.
543 ///
544 /// Plugins extend the router's functionality by providing additional features
545 /// like compression, CORS handling, rate limiting, or custom behavior. Plugins
546 /// are initialized once when the server starts.
547 ///
548 /// # Examples
549 ///
550 /// ```rust
551 /// # #[cfg(feature = "plugins")]
552 /// use tako::{router::Router, plugins::TakoPlugin};
553 /// # #[cfg(feature = "plugins")]
554 /// use anyhow::Result;
555 ///
556 /// # #[cfg(feature = "plugins")]
557 /// struct LoggingPlugin;
558 ///
559 /// # #[cfg(feature = "plugins")]
560 /// impl TakoPlugin for LoggingPlugin {
561 /// fn name(&self) -> &'static str {
562 /// "logging"
563 /// }
564 ///
565 /// fn setup(&self, _router: &Router) -> Result<()> {
566 /// println!("Logging plugin initialized");
567 /// Ok(())
568 /// }
569 /// }
570 ///
571 /// # #[cfg(feature = "plugins")]
572 /// # fn example() {
573 /// let mut router = Router::new();
574 /// router.plugin(LoggingPlugin);
575 /// # }
576 /// ```
577 #[cfg(feature = "plugins")]
578 #[cfg_attr(docsrs, doc(cfg(feature = "plugins")))]
579 pub fn plugin<P>(&mut self, plugin: P) -> &mut Self
580 where
581 P: TakoPlugin + Clone + Send + Sync + 'static,
582 {
583 self.plugins.push(Box::new(plugin));
584 self
585 }
586
587 /// Returns references to all registered plugins.
588 #[cfg(feature = "plugins")]
589 #[cfg_attr(docsrs, doc(cfg(feature = "plugins")))]
590 pub(crate) fn plugins(&self) -> Vec<&dyn TakoPlugin> {
591 self.plugins.iter().map(|plugin| plugin.as_ref()).collect()
592 }
593
594 /// Initializes all registered plugins exactly once.
595 #[cfg(feature = "plugins")]
596 #[cfg_attr(docsrs, doc(cfg(feature = "plugins")))]
597 pub(crate) fn setup_plugins_once(&self) {
598 use std::sync::atomic::Ordering;
599
600 if !self.plugins_initialized.swap(true, Ordering::SeqCst) {
601 for plugin in self.plugins() {
602 let _ = plugin.setup(self);
603 }
604 }
605 }
606
607 /// Merges another router into this router.
608 ///
609 /// This method combines routes and middleware from another router into the
610 /// current one. Routes are copied over, and the other router's global middleware
611 /// is prepended to each merged route's middleware chain.
612 ///
613 /// # Examples
614 ///
615 /// ```rust
616 /// use tako::{router::Router, Method, responder::Responder, types::Request};
617 ///
618 /// async fn api_handler(_req: Request) -> impl Responder {
619 /// "API response"
620 /// }
621 ///
622 /// async fn web_handler(_req: Request) -> impl Responder {
623 /// "Web response"
624 /// }
625 ///
626 /// // Create API router
627 /// let mut api_router = Router::new();
628 /// api_router.route(Method::GET, "/users", api_handler);
629 /// api_router.middleware(|req, next| async move {
630 /// println!("API middleware");
631 /// next.run(req).await
632 /// });
633 ///
634 /// // Create main router and merge API router
635 /// let mut main_router = Router::new();
636 /// main_router.route(Method::GET, "/", web_handler);
637 /// main_router.merge(api_router);
638 /// ```
639 pub fn merge(&mut self, other: Router) {
640 let upstream_globals = other.middlewares.read().clone();
641
642 other.routes.iter_sync(|method, weak_vec| {
643 let mut target_router = self.inner.entry_sync(method.clone()).or_default();
644
645 for weak in weak_vec {
646 if let Some(route) = weak.upgrade() {
647 let mut rmw = route.middlewares.write();
648 for mw in upstream_globals.iter().rev() {
649 rmw.push_front(mw.clone());
650 }
651
652 let _ = target_router
653 .get_mut()
654 .insert(route.path.clone(), route.clone());
655
656 self
657 .routes
658 .entry_sync(method.clone())
659 .or_default()
660 .push(Arc::downgrade(&route));
661 }
662 }
663
664 true
665 });
666
667 #[cfg(feature = "signals")]
668 self.signals.merge_from(&other.signals);
669 }
670
671 /// Ensures the request HTTP version satisfies the route's configured protocol guard.
672 /// Returns `Some(Response)` with 505 HTTP Version Not Supported when the request
673 /// doesn't match the guard, otherwise returns `None` to continue dispatch.
674 fn enforce_protocol_guard(route: &Route, req: &Request) -> Option<Response> {
675 if let Some(guard) = route.protocol_guard()
676 && guard != req.version()
677 {
678 return Some(
679 http::Response::builder()
680 .status(StatusCode::HTTP_VERSION_NOT_SUPPORTED)
681 .body(TakoBody::empty())
682 .unwrap(),
683 );
684 }
685 None
686 }
687
688 // ─────────────────────────────────────────────────────────────────────────────
689 // OpenAPI route collection
690 // ─────────────────────────────────────────────────────────────────────────────
691
692 /// Collects OpenAPI metadata from all registered routes.
693 ///
694 /// Returns a vector of tuples containing the HTTP method, path, and OpenAPI
695 /// metadata for each route that has OpenAPI information attached.
696 ///
697 /// # Examples
698 ///
699 /// ```rust,ignore
700 /// use tako::{router::Router, Method};
701 ///
702 /// let mut router = Router::new();
703 /// router.route(Method::GET, "/users", list_users)
704 /// .summary("List users")
705 /// .tag("users");
706 ///
707 /// for (method, path, openapi) in router.collect_openapi_routes() {
708 /// println!("{} {} - {:?}", method, path, openapi.summary);
709 /// }
710 /// ```
711 #[cfg(any(feature = "utoipa", feature = "vespera"))]
712 #[cfg_attr(docsrs, doc(cfg(any(feature = "utoipa", feature = "vespera"))))]
713 pub fn collect_openapi_routes(&self) -> Vec<(Method, String, crate::openapi::RouteOpenApi)> {
714 let mut result = Vec::new();
715
716 self.routes.iter_sync(|method, weak_vec| {
717 for weak in weak_vec {
718 if let Some(route) = weak.upgrade() {
719 if let Some(openapi) = route.openapi_metadata() {
720 result.push((method.clone(), route.path.clone(), openapi));
721 }
722 }
723 }
724 true
725 });
726
727 result
728 }
729}