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}