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