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}