Skip to main content

tako_rs_core/route/
builder.rs

1//! The chainable route configuration API.
2//!
3//! These `&self -> &Self` builder methods are chained off the `&Route`
4//! returned by the router: middleware and plugin registration, the HTTP
5//! protocol-version guard, per-route signal wiring, the timeout override,
6//! and the SIMD JSON dispatch mode — plus the crate-private getters the
7//! dispatch hot path reads back.
8
9use std::sync::Arc;
10use std::sync::atomic::Ordering;
11use std::time::Duration;
12
13use super::Route;
14use crate::extractors::json::SimdJsonMode;
15use crate::middleware::Next;
16#[cfg(feature = "plugins")]
17use crate::plugins::TakoPlugin;
18use crate::responder::Responder;
19#[cfg(feature = "signals")]
20use crate::signals::Signal;
21#[cfg(feature = "signals")]
22use crate::signals::SignalArbiter;
23use crate::types::BoxMiddleware;
24use crate::types::Request;
25
26impl Route {
27  /// Adds middleware to this route's execution chain.
28  pub fn middleware<F, Fut, R>(&self, f: F) -> &Self
29  where
30    F: Fn(Request, Next) -> Fut + Clone + Send + Sync + 'static,
31    Fut: std::future::Future<Output = R> + Send + 'static,
32    R: Responder + Send + 'static,
33  {
34    let mw: BoxMiddleware = Arc::new(move |req, next| {
35      let fut = f(req, next); // Fut<'a>
36
37      Box::pin(async move { fut.await.into_response() })
38    });
39
40    // RCU-style append: ArcSwap retries the closure on CAS conflict, so
41    // concurrent route-level middleware pushes cannot lose entries.
42    self.middlewares.rcu(move |current| {
43      let mut next = Vec::with_capacity(current.len() + 1);
44      next.extend(current.iter().cloned());
45      next.push(mw.clone());
46      Arc::new(next)
47    });
48    self.has_middleware.store(true, Ordering::Release);
49    self
50  }
51
52  /// Adds a plugin to this route.
53  ///
54  /// Route-level plugins allow applying functionality like compression, CORS,
55  /// or rate limiting to specific routes instead of globally. Plugins added
56  /// to a route are initialized when the route is first accessed.
57  ///
58  /// # Examples
59  ///
60  /// ```rust
61  /// # #[cfg(feature = "plugins")]
62  /// use tako::{router::Router, Method, responder::Responder, types::Request};
63  /// # #[cfg(feature = "plugins")]
64  /// use tako::plugins::cors::CorsBuilder;
65  ///
66  /// # #[cfg(feature = "plugins")]
67  /// # async fn handler(_req: Request) -> impl Responder {
68  /// #     "Hello, World!"
69  /// # }
70  ///
71  /// # #[cfg(feature = "plugins")]
72  /// # fn example() {
73  /// let mut router = Router::new();
74  /// let route = router.route(Method::GET, "/api/data", handler);
75  ///
76  /// // Apply CORS only to this route
77  /// let cors = CorsBuilder::new()
78  ///     .allow_origin("https://example.com")
79  ///     .build();
80  /// route.plugin(cors);
81  /// # }
82  /// ```
83  #[cfg(feature = "plugins")]
84  #[cfg_attr(docsrs, doc(cfg(feature = "plugins")))]
85  pub fn plugin<P>(&self, plugin: P) -> &Self
86  where
87    P: TakoPlugin + Clone + Send + Sync + 'static,
88  {
89    self.plugins.write().push(Box::new(plugin));
90    self
91  }
92
93  /// Initializes route-level plugins exactly once.
94  ///
95  /// This method sets up all plugins registered with this route by calling
96  /// their setup method. It uses a mini-router to collect the middleware
97  /// that plugins register, then adds that middleware to the route's
98  /// middleware chain. This ensures plugins are only initialized once.
99  #[cfg(feature = "plugins")]
100  #[cfg_attr(docsrs, doc(cfg(feature = "plugins")))]
101  pub(crate) fn setup_plugins_once(&self) {
102    // Hot path: dispatch calls this on every matched route. After the
103    // first request the plugins are installed, so do a cheap Acquire
104    // load and bail before paying for a SeqCst RMW + fence each time.
105    // `Acquire` pairs with the `Release` half of the `swap` below so we
106    // observe the middleware writes published by the initializing thread.
107    if self.plugins_initialized.load(Ordering::Acquire) {
108      return;
109    }
110
111    if !self.plugins_initialized.swap(true, Ordering::SeqCst) {
112      // Create a temporary mini-router to capture plugin middleware
113      let mini_router = crate::router::Router::new();
114
115      let plugins = self.plugins.read();
116      for plugin in plugins.iter() {
117        // See `Router::setup_plugins_once`: log failures so an erroring
118        // route-level plugin (auth, rate-limit, ...) is visible instead
119        // of silently dropped — fail-open without diagnostics is
120        // exactly what the audit calls out.
121        if let Err(e) = plugin.setup(&mini_router) {
122          tracing::error!(
123            plugin = plugin.name(),
124            error = %e,
125            "route-level TakoPlugin::setup failed; plugin not active"
126          );
127        }
128      }
129
130      // Transfer middleware from mini-router to this route
131      let plugin_middlewares = mini_router.middlewares.load();
132      let existing = self.middlewares.load_full();
133      let mut merged = Vec::with_capacity(plugin_middlewares.len() + existing.len());
134      merged.extend(plugin_middlewares.iter().cloned());
135      merged.extend(existing.iter().cloned());
136      if !merged.is_empty() {
137        self.has_middleware.store(true, Ordering::Release);
138      }
139      self.middlewares.store(Arc::new(merged));
140    }
141  }
142
143  /// Restricts this route to a specific HTTP protocol version.
144  ///
145  /// Requests whose `version()` does not match are answered with
146  /// `505 HTTP Version Not Supported`. Set once at registration; later calls
147  /// are no-ops (lock-free reads in the hot path).
148  pub fn version(&self, version: http::Version) -> &Self {
149    if let Err(_existing) = self.http_protocol.set(version) {
150      tracing::warn!(
151        path = %self.path,
152        method = ?self.method,
153        existing = ?self.http_protocol.get().copied(),
154        requested = ?version,
155        "Route::version called twice; subsequent calls are ignored (OnceLock first-wins)",
156      );
157    }
158    self
159  }
160
161  /// HTTP/0.9 guard. Shorthand for [`Route::version`] with [`http::Version::HTTP_09`].
162  pub fn h09(&self) -> &Self {
163    self.version(http::Version::HTTP_09)
164  }
165
166  /// HTTP/1.0 guard. Shorthand for [`Route::version`] with [`http::Version::HTTP_10`].
167  pub fn h10(&self) -> &Self {
168    self.version(http::Version::HTTP_10)
169  }
170
171  /// HTTP/1.1 guard. Shorthand for [`Route::version`] with [`http::Version::HTTP_11`].
172  pub fn h11(&self) -> &Self {
173    self.version(http::Version::HTTP_11)
174  }
175
176  /// HTTP/2 guard. Shorthand for [`Route::version`] with [`http::Version::HTTP_2`].
177  pub fn h2(&self) -> &Self {
178    self.version(http::Version::HTTP_2)
179  }
180
181  /// Returns the configured protocol guard, if any.
182  #[inline]
183  pub(crate) fn protocol_guard(&self) -> Option<http::Version> {
184    self.http_protocol.get().copied()
185  }
186
187  #[cfg(feature = "signals")]
188  /// Returns a reference to this route's signal arbiter.
189  pub fn signals(&self) -> &SignalArbiter {
190    &self.signals
191  }
192
193  #[cfg(feature = "signals")]
194  /// Returns a clone of this route's signal arbiter for shared usage.
195  pub fn signal_arbiter(&self) -> SignalArbiter {
196    self.signals.clone()
197  }
198
199  #[cfg(feature = "signals")]
200  /// Registers a handler for a named signal on this route's arbiter.
201  pub fn on_signal<F, Fut>(&self, id: impl Into<String>, handler: F)
202  where
203    F: Fn(Signal) -> Fut + Send + Sync + 'static,
204    Fut: std::future::Future<Output = ()> + Send + 'static,
205  {
206    self.signals.on(id, handler);
207  }
208
209  #[cfg(feature = "signals")]
210  /// Emits a signal through this route's arbiter.
211  pub async fn emit_signal(&self, signal: Signal) {
212    self.signals.emit(signal).await;
213  }
214
215  /// Sets a timeout for this route, overriding the router-level timeout.
216  ///
217  /// When a request exceeds the timeout duration, the timeout fallback handler
218  /// is invoked (if configured on the router) or a 408 Request Timeout response
219  /// is returned.
220  ///
221  /// # Examples
222  ///
223  /// ```rust,ignore
224  /// use std::time::Duration;
225  ///
226  /// router.route(Method::POST, "/upload", upload_handler)
227  ///     .timeout(Duration::from_secs(60));
228  /// ```
229  pub fn timeout(&self, duration: Duration) -> &Self {
230    if let Err(_existing) = self.timeout.set(duration) {
231      tracing::warn!(
232        path = %self.path,
233        method = ?self.method,
234        existing_ms = self.timeout.get().copied().unwrap_or_default().as_millis() as u64,
235        requested_ms = duration.as_millis() as u64,
236        "Route::timeout called twice; subsequent calls are ignored (OnceLock first-wins)",
237      );
238    }
239    self
240  }
241
242  /// Returns the configured timeout for this route, if any.
243  #[inline]
244  pub(crate) fn get_timeout(&self) -> Option<Duration> {
245    self.timeout.get().copied()
246  }
247
248  /// Configures the SIMD JSON dispatch behavior for this route.
249  ///
250  /// When the `simd` feature is enabled, `Json<T>` can use the `sonic_rs` SIMD
251  /// parser for faster deserialization. By default, SIMD is used for payloads
252  /// above 2 MB. This method lets you override that threshold — or force
253  /// SIMD on/off — for individual routes.
254  ///
255  /// Without the `simd` feature this setting is accepted but has no effect.
256  ///
257  /// # Examples
258  ///
259  /// ```rust,ignore
260  /// use tako::extractors::json::SimdJsonMode;
261  ///
262  /// // Always use SIMD for a heavy ingest endpoint
263  /// router.route(Method::POST, "/api/ingest", ingest)
264  ///     .simd_json(SimdJsonMode::Always);
265  ///
266  /// // Use SIMD only above 4 KB
267  /// router.route(Method::POST, "/api/batch", batch)
268  ///     .simd_json(SimdJsonMode::Threshold(4096));
269  ///
270  /// // Disable SIMD for a latency-sensitive tiny-payload route
271  /// router.route(Method::POST, "/api/ping", ping)
272  ///     .simd_json(SimdJsonMode::Never);
273  /// ```
274  pub fn simd_json(&self, mode: SimdJsonMode) -> &Self {
275    if let Err(_existing) = self.simd_json_mode.set(mode) {
276      tracing::warn!(
277        path = %self.path,
278        method = ?self.method,
279        existing = ?self.simd_json_mode.get().copied(),
280        requested = ?mode,
281        "Route::simd_json called twice; subsequent calls are ignored (OnceLock first-wins)",
282      );
283    }
284    self
285  }
286
287  /// Returns the configured SIMD JSON mode for this route, if any.
288  #[inline]
289  pub(crate) fn get_simd_json_mode(&self) -> Option<SimdJsonMode> {
290    self.simd_json_mode.get().copied()
291  }
292}