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}