Skip to main content

tako_rs_core/router/
plugins.rs

1//! Plugin registration/initialization, `OpenAPI` collection, and route-index GC.
2
3#[cfg(any(feature = "utoipa", feature = "vespera"))]
4use http::Method;
5
6use super::Router;
7#[cfg(feature = "plugins")]
8use crate::plugins::TakoPlugin;
9
10impl Router {
11  /// Registers a plugin with the router.
12  ///
13  /// Plugins extend the router's functionality by providing additional features
14  /// like compression, CORS handling, rate limiting, or custom behavior. Plugins
15  /// are initialized once when the server starts.
16  ///
17  /// # Examples
18  ///
19  /// ```rust
20  /// # #[cfg(feature = "plugins")]
21  /// use tako::{router::Router, plugins::TakoPlugin};
22  /// # #[cfg(feature = "plugins")]
23  /// use anyhow::Result;
24  ///
25  /// # #[cfg(feature = "plugins")]
26  /// struct LoggingPlugin;
27  ///
28  /// # #[cfg(feature = "plugins")]
29  /// impl TakoPlugin for LoggingPlugin {
30  ///     fn name(&self) -> &'static str {
31  ///         "logging"
32  ///     }
33  ///
34  ///     fn setup(&self, _router: &Router) -> Result<()> {
35  ///         println!("Logging plugin initialized");
36  ///         Ok(())
37  ///     }
38  /// }
39  ///
40  /// # #[cfg(feature = "plugins")]
41  /// # fn example() {
42  /// let mut router = Router::new();
43  /// router.plugin(LoggingPlugin);
44  /// # }
45  /// ```
46  #[cfg(feature = "plugins")]
47  #[cfg_attr(docsrs, doc(cfg(feature = "plugins")))]
48  pub fn plugin<P>(&mut self, plugin: P) -> &mut Self
49  where
50    P: TakoPlugin + Clone + Send + Sync + 'static,
51  {
52    self.plugins.push(Box::new(plugin));
53    self
54  }
55
56  /// Returns references to all registered plugins.
57  #[cfg(feature = "plugins")]
58  #[cfg_attr(docsrs, doc(cfg(feature = "plugins")))]
59  pub(crate) fn plugins(&self) -> Vec<&dyn TakoPlugin> {
60    self.plugins.iter().map(AsRef::as_ref).collect()
61  }
62
63  /// Initializes all registered plugins exactly once.
64  #[cfg(feature = "plugins")]
65  #[cfg_attr(docsrs, doc(cfg(feature = "plugins")))]
66  #[doc(hidden)]
67  pub fn setup_plugins_once(&self) {
68    use std::sync::atomic::Ordering;
69
70    // Hot-path fast exit: see `Route::setup_plugins_once`. Acquire-load
71    // pairs with the Release half of the swap so plugin-published state
72    // is visible by the time we skip the RMW.
73    if self.plugins_initialized.load(Ordering::Acquire) {
74      return;
75    }
76
77    if !self.plugins_initialized.swap(true, Ordering::SeqCst) {
78      for plugin in self.plugins() {
79        // Surface plugin setup errors loudly — a silently-skipped CORS,
80        // auth, rate-limit, or CSRF plugin would leave the server
81        // running without the protection the operator expected
82        // (security-relevant fail-open). Cold path — first dispatch only.
83        if let Err(e) = plugin.setup(self) {
84          tracing::error!(
85            plugin = plugin.name(),
86            error = %e,
87            "router-level TakoPlugin::setup failed; plugin not active"
88          );
89        }
90      }
91    }
92  }
93
94  /// Collects `OpenAPI` metadata from all registered routes.
95  ///
96  /// Returns a vector of tuples containing the HTTP method, path, and `OpenAPI`
97  /// metadata for each route that has `OpenAPI` information attached.
98  ///
99  /// # Examples
100  ///
101  /// ```rust,ignore
102  /// use tako::{router::Router, Method};
103  ///
104  /// let mut router = Router::new();
105  /// router.route(Method::GET, "/users", list_users)
106  ///     .summary("List users")
107  ///     .tag("users");
108  ///
109  /// for (method, path, openapi) in router.collect_openapi_routes() {
110  ///     println!("{} {} - {:?}", method, path, openapi.summary);
111  /// }
112  /// ```
113  #[cfg(any(feature = "utoipa", feature = "vespera"))]
114  #[cfg_attr(docsrs, doc(cfg(any(feature = "utoipa", feature = "vespera"))))]
115  pub fn collect_openapi_routes(&self) -> Vec<(Method, String, crate::openapi::RouteOpenApi)> {
116    let mut result = Vec::new();
117
118    for (method, weak_vec) in self.routes.iter() {
119      for weak in weak_vec {
120        if let Some(route) = weak.upgrade()
121          && let Some(openapi) = route.openapi_metadata()
122        {
123          result.push((method.clone(), route.path.clone(), openapi));
124        }
125      }
126    }
127
128    result
129  }
130
131  /// Drops dangling `Weak<Route>` entries from the per-method `routes` index.
132  ///
133  /// All current routes stay live for the router's lifetime, so this is a
134  /// no-op in well-behaved code. It exists as a safety valve: if any future
135  /// API ever removes from `inner` (hot reload, route deregistration), or if
136  /// downstream code holds the `Arc<Route>` returned from [`Router::route`]
137  /// past the router's lifetime, this method bounds the size of the index.
138  ///
139  /// Cold path; safe to call repeatedly. Linear in the total number of
140  /// registered routes.
141  pub fn compact_routes(&mut self) {
142    for weak_vec in self.routes.iter_mut() {
143      weak_vec.retain(|w| w.strong_count() > 0);
144    }
145  }
146}