toolkit_zero/socket/server.rs
1
2//! Typed, fluent HTTP server construction.
3//!
4//! This module provides a builder-oriented API for declaring HTTP routes and
5//! serving them with [`warp`] under the hood. The entry point for every route
6//! is [`ServerMechanism`], which pairs an HTTP method with a URL path and
7//! supports incremental enrichment — attaching a JSON body expectation, URL
8//! query parameter deserialisation, or shared state — before being finalised
9//! into a [`SocketType`] route handle via `onconnect`.
10//!
11//! Completed routes are registered on a [`Server`] and served with a single
12//! `.await`. Graceful shutdown is available via
13//! [`Server::serve_with_graceful_shutdown`] and [`Server::serve_from_listener`].
14//!
15//! # Builder chains at a glance
16//!
17//! | Chain | Handler receives |
18//! |---|---|
19//! | `ServerMechanism::method(path).onconnect(f)` | nothing |
20//! | `.json::<T>().onconnect(f)` | `T: DeserializeOwned` |
21//! | `.query::<T>().onconnect(f)` | `T: DeserializeOwned` |
22//! | `.encryption::<T>(key).onconnect(f)` | `T: bincode::Decode<()>` (VEIL-decrypted body) |
23//! | `.encrypted_query::<T>(key).onconnect(f)` | `T: bincode::Decode<()>` (VEIL-decrypted query) |
24//! | `.state(s).onconnect(f)` | `S: Clone + Send + Sync` |
25//! | `.state(s).json::<T>().onconnect(f)` | `(S, T)` |
26//! | `.state(s).query::<T>().onconnect(f)` | `(S, T)` |
27//! | `.state(s).encryption::<T>(key).onconnect(f)` | `(S, T)` — VEIL-decrypted body |
28//! | `.state(s).encrypted_query::<T>(key).onconnect(f)` | `(S, T)` — VEIL-decrypted query |
29//!
30//! For blocking handlers (not recommended in production) every finaliser also
31//! has an unsafe `onconnect_sync` counterpart.
32//!
33//! # `#[mechanism]` attribute macro
34//!
35//! As an alternative to spelling out the builder chain by hand, the
36//! [`mechanism`] attribute macro collapses the entire
37//! `server.mechanism(ServerMechanism::method(path) … .onconnect(handler))` call
38//! into a single decorated `async fn`:
39//!
40//! ```rust,no_run
41//! # use toolkit_zero::socket::server::{Server, mechanism, reply, Status};
42//! # use serde::Deserialize;
43//! # #[derive(Deserialize)] struct NewItem { name: String }
44//! # #[derive(serde::Serialize)] struct Item { id: u32, name: String }
45//! # let mut server = Server::default();
46//! // Fluent form:
47//! // server.mechanism(
48//! // ServerMechanism::post("/items")
49//! // .json::<NewItem>()
50//! // .onconnect(|body: NewItem| async move {
51//! // reply!(json => Item { id: 1, name: body.name }, status => Status::Created)
52//! // })
53//! // );
54//!
55//! // Equivalent attribute form:
56//! #[mechanism(server, POST, "/items", json)]
57//! async fn create(body: NewItem) {
58//! reply!(json => Item { id: 1, name: body.name }, status => Status::Created)
59//! }
60//! ```
61//!
62//! See the [`mechanism`] item for the full syntax reference.
63//!
64//! # Response helpers
65//!
66//! Use the [`reply!`] macro as the most concise way to build a response:
67//!
68//! ```rust,no_run
69//! # use toolkit_zero::socket::server::*;
70//! # use serde::Serialize;
71//! # #[derive(Serialize)] struct Item { id: u32 }
72//! # let item = Item { id: 1 };
73//! // 200 OK, empty body
74//! // reply!()
75//!
76//! // 200 OK, JSON body
77//! // reply!(json => item)
78//!
79//! // 201 Created, JSON body
80//! // reply!(json => item, status => Status::Created)
81//! ```
82
83use std::{future::Future, net::SocketAddr};
84use serde::{de::DeserializeOwned, Serialize};
85use warp::{Filter, Rejection, Reply, filters::BoxedFilter};
86
87pub use super::SerializationKey;
88pub use toolkit_zero_macros::mechanism;
89
90/// A fully assembled, type-erased HTTP route ready to be registered on a [`Server`].
91///
92/// This is the final product of every builder chain. Pass it to [`Server::mechanism`] to mount it.
93pub type SocketType = BoxedFilter<(warp::reply::Response, )>;
94
95#[derive(Clone, Copy, Debug)]
96enum HttpMethod {
97 Get,
98 Post,
99 Put,
100 Delete,
101 Patch,
102 Head,
103 Options,
104}
105
106impl HttpMethod {
107 fn filter(&self) -> BoxedFilter<()> {
108 match self {
109 HttpMethod::Get => warp::get().boxed(),
110 HttpMethod::Post => warp::post().boxed(),
111 HttpMethod::Put => warp::put().boxed(),
112 HttpMethod::Delete => warp::delete().boxed(),
113 HttpMethod::Patch => warp::patch().boxed(),
114 HttpMethod::Head => warp::head().boxed(),
115 HttpMethod::Options => warp::options().boxed(),
116 }
117 }
118}
119
120
121fn path_filter(path: &str) -> BoxedFilter<()> {
122 log::trace!("Building path filter for: '{}'", path);
123 let segs: Vec<&'static str> = path
124 .trim_matches('/')
125 .split('/')
126 .filter(|s| !s.is_empty())
127 .map(|s| -> &'static str { Box::leak(s.to_owned().into_boxed_str()) })
128 .collect();
129
130 if segs.is_empty() {
131 return warp::path::end().boxed();
132 }
133
134 // Start with the first segment, then and-chain the rest.
135 let mut f: BoxedFilter<()> = warp::path(segs[0]).boxed();
136 for seg in &segs[1..] {
137 f = f.and(warp::path(*seg)).boxed();
138 }
139 f.and(warp::path::end()).boxed()
140}
141
142/// Entry point for building an HTTP route.
143///
144/// Pairs an HTTP method with a URL path and acts as the root of a fluent builder chain.
145/// Optionally attach shared state, a JSON body expectation, or URL query parameter
146/// deserialisation — then finalise with [`onconnect`](ServerMechanism::onconnect) (async) or
147/// [`onconnect_sync`](ServerMechanism::onconnect_sync) (sync) to produce a [`SocketType`]
148/// ready to be mounted on a [`Server`].
149///
150/// # Example
151/// ```rust,no_run
152/// # use serde::{Deserialize, Serialize};
153/// # use std::sync::{Arc, Mutex};
154/// # #[derive(Deserialize, Serialize)] struct Item { id: u32, name: String }
155/// # #[derive(Deserialize)] struct CreateItem { name: String }
156/// # #[derive(Deserialize)] struct SearchQuery { q: String }
157///
158/// // Plain GET — no body, no state
159/// let health = ServerMechanism::get("/health")
160/// .onconnect(|| async { reply!() });
161///
162/// // POST — JSON body deserialised into `CreateItem`
163/// let create = ServerMechanism::post("/items")
164/// .json::<CreateItem>()
165/// .onconnect(|body| async move {
166/// let item = Item { id: 1, name: body.name };
167/// reply!(json => item, status => Status::Created)
168/// });
169///
170/// // GET — URL query params deserialised into `SearchQuery`
171/// let search = ServerMechanism::get("/search")
172/// .query::<SearchQuery>()
173/// .onconnect(|params| async move {
174/// let _q = params.q;
175/// reply!()
176/// });
177///
178/// // GET — shared counter state injected on every request
179/// let counter: Arc<Mutex<u64>> = Arc::new(Mutex::new(0));
180/// let count_route = ServerMechanism::get("/count")
181/// .state(counter.clone())
182/// .onconnect(|state| async move {
183/// let n = *state.lock().unwrap();
184/// reply!(json => n)
185/// });
186/// ```
187pub struct ServerMechanism {
188 method: HttpMethod,
189 path: String,
190}
191
192impl ServerMechanism {
193
194 fn instance(method: HttpMethod, path: impl Into<String>) -> Self {
195 let path = path.into();
196 log::debug!("Creating {:?} route at '{}'", method, path);
197 Self { method, path }
198 }
199
200 /// Creates a route matching HTTP `GET` requests at `path`.
201 pub fn get(path: impl Into<String>) -> Self { Self::instance(HttpMethod::Get, path) }
202
203 /// Creates a route matching HTTP `POST` requests at `path`.
204 pub fn post(path: impl Into<String>) -> Self { Self::instance(HttpMethod::Post, path) }
205
206 /// Creates a route matching HTTP `PUT` requests at `path`.
207 pub fn put(path: impl Into<String>) -> Self { Self::instance(HttpMethod::Put, path) }
208
209 /// Creates a route matching HTTP `DELETE` requests at `path`.
210 pub fn delete(path: impl Into<String>) -> Self { Self::instance(HttpMethod::Delete, path) }
211
212 /// Creates a route matching HTTP `PATCH` requests at `path`.
213 pub fn patch(path: impl Into<String>) -> Self { Self::instance(HttpMethod::Patch, path) }
214
215 /// Creates a route matching HTTP `HEAD` requests at `path`.
216 pub fn head(path: impl Into<String>) -> Self { Self::instance(HttpMethod::Head, path) }
217
218 /// Creates a route matching HTTP `OPTIONS` requests at `path`.
219 pub fn options(path: impl Into<String>) -> Self { Self::instance(HttpMethod::Options, path) }
220
221 /// Attaches shared state `S` to this route, transitioning to [`StatefulSocketBuilder`].
222 ///
223 /// A fresh clone of `S` is injected into the handler on every request. For mutable
224 /// shared state, wrap the inner value in `Arc<Mutex<_>>` or `Arc<RwLock<_>>` before
225 /// passing it here — only the outer `Arc` is cloned per request; the inner data stays
226 /// shared across all requests.
227 ///
228 /// `S` must be `Clone + Send + Sync + 'static`.
229 ///
230 /// From [`StatefulSocketBuilder`] you can further add a JSON body (`.json`), query
231 /// parameters (`.query`), or encryption (`.encryption` / `.encrypted_query`) before
232 /// finalising with `onconnect`.
233 ///
234 /// # Example
235 /// ```rust,no_run
236 /// use std::sync::{Arc, Mutex};
237 ///
238 /// let db: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![]));
239 ///
240 /// let route = ServerMechanism::get("/list")
241 /// .state(db.clone())
242 /// .onconnect(|state| async move {
243 /// let items = state.lock().unwrap().clone();
244 /// reply!(json => items)
245 /// });
246 /// ```
247 pub fn state<S: Clone + Send + Sync + 'static>(self, state: S) -> StatefulSocketBuilder<S> {
248 log::trace!("Attaching state to {:?} route at '{}'", self.method, self.path);
249 StatefulSocketBuilder { base: self, state }
250 }
251
252 /// Declares that this route expects a JSON-encoded request body, transitioning to
253 /// [`JsonSocketBuilder`].
254 ///
255 /// On each incoming request the body is parsed as `Content-Type: application/json`
256 /// and deserialised into `T`. If the body is absent, malformed, or fails to
257 /// deserialise, the request is rejected before the handler is ever called.
258 /// When you subsequently call [`onconnect`](JsonSocketBuilder::onconnect), the handler
259 /// receives a fully-deserialised, ready-to-use `T`.
260 ///
261 /// `T` must implement `serde::de::DeserializeOwned`.
262 ///
263 /// # Example
264 /// ```rust,no_run
265 /// # use serde::Deserialize;
266 /// # #[derive(Deserialize)] struct Payload { value: i32 }
267 ///
268 /// let route = ServerMechanism::post("/submit")
269 /// .json::<Payload>()
270 /// .onconnect(|body| async move {
271 /// reply!(json => body.value)
272 /// });
273 /// ```
274 pub fn json<T: DeserializeOwned + Send>(self) -> JsonSocketBuilder<T> {
275 log::trace!("Attaching JSON body expectation to {:?} route at '{}'", self.method, self.path);
276 JsonSocketBuilder { base: self, _phantom: std::marker::PhantomData }
277 }
278
279 /// Declares that this route extracts its input from URL query parameters, transitioning
280 /// to [`QuerySocketBuilder`].
281 ///
282 /// On each incoming request the query string (`?field=value&...`) is deserialised into
283 /// `T`. A missing or malformed query string is rejected before the handler is called.
284 /// When you subsequently call [`onconnect`](QuerySocketBuilder::onconnect), the handler
285 /// receives a fully-deserialised, ready-to-use `T`.
286 ///
287 /// `T` must implement `serde::de::DeserializeOwned`.
288 ///
289 /// # Example
290 /// ```rust,no_run
291 /// # use serde::Deserialize;
292 /// # #[derive(Deserialize)] struct Filter { page: u32, per_page: u32 }
293 ///
294 /// let route = ServerMechanism::get("/items")
295 /// .query::<Filter>()
296 /// .onconnect(|filter| async move {
297 /// let _ = (filter.page, filter.per_page);
298 /// reply!()
299 /// });
300 /// ```
301 pub fn query<T: DeserializeOwned + Send>(self) -> QuerySocketBuilder<T> {
302 log::trace!("Attaching query parameter expectation to {:?} route at '{}'", self.method, self.path);
303 QuerySocketBuilder { base: self, _phantom: std::marker::PhantomData }
304 }
305
306 /// Declares that this route expects a VEIL-encrypted request body, transitioning to
307 /// [`EncryptedBodyBuilder`].
308 ///
309 /// On each incoming request the raw body bytes are decrypted using the provided
310 /// [`SerializationKey`] before the handler is called. If the key does not match or
311 /// the body is corrupt, the route responds with `403 Forbidden` and the handler is
312 /// never invoked — meaning the `T` your handler receives is always a legitimate,
313 /// trusted, fully-decrypted value.
314 ///
315 /// Use `SerializationKey::Default` when both client and server share the built-in key,
316 /// or `SerializationKey::Value("your-key")` for a custom shared secret.
317 /// For plain-JSON routes (no encryption) use [`.json::<T>()`](ServerMechanism::json)
318 /// instead.
319 ///
320 /// `T` must implement `bincode::Decode<()>`.
321 ///
322 /// # Example
323 /// ```rust,no_run
324 /// # use bincode::{Encode, Decode};
325 /// # #[derive(Encode, Decode)] struct Payload { value: i32 }
326 ///
327 /// let route = ServerMechanism::post("/submit")
328 /// .encryption::<Payload>(SerializationKey::Default)
329 /// .onconnect(|body| async move {
330 /// // `body` is already decrypted and deserialised — use it directly.
331 /// reply!(json => body.value)
332 /// });
333 /// ```
334 pub fn encryption<T>(self, key: SerializationKey) -> EncryptedBodyBuilder<T> {
335 log::trace!("Attaching encrypted body to {:?} route at '{}'", self.method, self.path);
336 EncryptedBodyBuilder { base: self, key, _phantom: std::marker::PhantomData }
337 }
338
339 /// Declares that this route expects VEIL-encrypted URL query parameters, transitioning
340 /// to [`EncryptedQueryBuilder`].
341 ///
342 /// The client must send a single `?data=<base64url>` query parameter whose value is
343 /// the URL-safe base64 encoding of the VEIL-encrypted struct bytes. On each request
344 /// the server base64-decodes then decrypts the payload using the provided
345 /// [`SerializationKey`]. If the `data` parameter is missing, the encoding is invalid,
346 /// the key does not match, or the bytes are corrupt, the route responds with
347 /// `403 Forbidden` and the handler is never invoked — meaning the `T` your handler
348 /// receives is always a legitimate, trusted, fully-decrypted value.
349 ///
350 /// Use `SerializationKey::Default` or `SerializationKey::Value("your-key")`. For
351 /// plain query-string routes (no encryption) use
352 /// [`.query::<T>()`](ServerMechanism::query) instead.
353 ///
354 /// `T` must implement `bincode::Decode<()>`.
355 pub fn encrypted_query<T>(self, key: SerializationKey) -> EncryptedQueryBuilder<T> {
356 log::trace!("Attaching encrypted query to {:?} route at '{}'", self.method, self.path);
357 EncryptedQueryBuilder { base: self, key, _phantom: std::marker::PhantomData }
358 }
359
360 /// Finalises this route with an async handler that receives no arguments.
361 ///
362 /// Neither a request body nor query parameters are read. The handler runs on every
363 /// matching request and must return `Result<impl Reply, Rejection>`. Use the
364 /// [`reply!`] macro or the standalone reply helpers ([`reply_with_json`],
365 /// [`reply_with_status`], etc.) to construct a response.
366 ///
367 /// Returns a [`SocketType`] ready to be passed to [`Server::mechanism`].
368 ///
369 /// # Example
370 /// ```rust,no_run
371 /// # use serde::Serialize;
372 /// # #[derive(Serialize)] struct Pong { ok: bool }
373 ///
374 /// let route = ServerMechanism::get("/ping")
375 /// .onconnect(|| async {
376 /// reply!(json => Pong { ok: true })
377 /// });
378 /// ```
379 pub fn onconnect<F, Fut, Re>(self, handler: F) -> SocketType
380 where
381 F: Fn() -> Fut + Clone + Send + Sync + 'static,
382 Fut: Future<Output = Result<Re, Rejection>> + Send,
383 Re: Reply + Send,
384 {
385 log::debug!("Finalising async {:?} route at '{}' (no args)", self.method, self.path);
386 let m = self.method.filter();
387 let p = path_filter(&self.path);
388 m.and(p)
389 .and_then(handler)
390 .map(|r: Re| r.into_response())
391 .boxed()
392 }
393
394 /// Finalises this route with a **synchronous** handler that receives no arguments.
395 ///
396 /// Behaviour and contract are identical to the async variant — neither a body nor
397 /// query parameters are read — but the closure may block. Each request is dispatched
398 /// to the blocking thread pool, so the handler must complete quickly to avoid starving
399 /// other requests.
400 ///
401 /// Returns a [`SocketType`] ready to be passed to [`Server::mechanism`].
402 ///
403 /// # Safety
404 ///
405 /// Every incoming request spawns an independent task on Tokio's blocking thread pool.
406 /// The pool caps the number of live OS threads (default 512), but the **queue of waiting
407 /// tasks is unbounded** — under a traffic surge, tasks accumulate without limit, consuming
408 /// unbounded memory and causing severe latency spikes or OOM crashes before any queued task
409 /// gets a chance to run. Additionally, any panic inside the handler is silently converted
410 /// into a `Rejection`, masking runtime errors. Callers must ensure the handler completes
411 /// quickly and that adequate backpressure or rate limiting is applied externally.
412 pub unsafe fn onconnect_sync<F, Re>(self, handler: F) -> SocketType
413 where
414 F: Fn() -> Result<Re, Rejection> + Clone + Send + Sync + 'static,
415 Re: Reply + Send + 'static,
416 {
417 log::warn!("Registering sync handler on {:?} '{}' — ensure rate-limiting is applied externally", self.method, self.path);
418 let m = self.method.filter();
419 let p = path_filter(&self.path);
420 m.and(p)
421 .and_then(move || {
422 let handler = handler.clone();
423 async move {
424 tokio::task::spawn_blocking(move || handler())
425 .await
426 .unwrap_or_else(|_| {
427 log::warn!("Sync handler panicked; converting to Rejection");
428 Err(warp::reject())
429 })
430 }
431 })
432 .map(|r: Re| r.into_response())
433 .boxed()
434 }
435}
436
437
438/// Route builder that expects and deserialises a JSON request body of type `T`.
439///
440/// Obtained from [`ServerMechanism::json`]. Optionally attach shared state via
441/// [`state`](JsonSocketBuilder::state), or finalise immediately with
442/// [`onconnect`](JsonSocketBuilder::onconnect) /
443/// [`onconnect_sync`](JsonSocketBuilder::onconnect_sync).
444pub struct JsonSocketBuilder<T> {
445 base: ServerMechanism,
446 _phantom: std::marker::PhantomData<T>,
447}
448
449impl<T: DeserializeOwned + Send + 'static> JsonSocketBuilder<T> {
450
451 /// Finalises this route with an async handler that receives the deserialised JSON body.
452 ///
453 /// Before the handler is called, the incoming request body is parsed as
454 /// `Content-Type: application/json` and deserialised into `T`. The handler receives
455 /// a ready-to-use `T` — no manual parsing is needed. If the body is absent or cannot
456 /// be decoded the request is rejected before the handler is ever invoked.
457 ///
458 /// The handler must return `Result<impl Reply, Rejection>`.
459 /// Returns a [`SocketType`] ready to be passed to [`Server::mechanism`].
460 pub fn onconnect<F, Fut, Re>(self, handler: F) -> SocketType
461 where
462 F: Fn(T) -> Fut + Clone + Send + Sync + 'static,
463 Fut: Future<Output = Result<Re, Rejection>> + Send,
464 Re: Reply + Send,
465 {
466 log::debug!("Finalising async {:?} route at '{}' (JSON body)", self.base.method, self.base.path);
467 let m = self.base.method.filter();
468 let p = path_filter(&self.base.path);
469 m.and(p)
470 .and(warp::body::json::<T>())
471 .and_then(handler)
472 .map(|r: Re| r.into_response())
473 .boxed()
474 }
475
476 /// Finalises this route with a **synchronous** handler that receives the deserialised
477 /// JSON body.
478 ///
479 /// The body is decoded into `T` before the handler is dispatched, identical to the
480 /// async variant. The closure may block but must complete quickly to avoid exhausting
481 /// the blocking thread pool.
482 ///
483 /// Returns a [`SocketType`] ready to be passed to [`Server::mechanism`].
484 ///
485 /// # Safety
486 ///
487 /// Every incoming request spawns an independent task on Tokio's blocking thread pool.
488 /// The pool caps the number of live OS threads (default 512), but the **queue of waiting
489 /// tasks is unbounded** — under a traffic surge, tasks accumulate without limit, consuming
490 /// unbounded memory and causing severe latency spikes or OOM crashes before any queued task
491 /// gets a chance to run. Additionally, any panic inside the handler is silently converted
492 /// into a `Rejection`, masking runtime errors. Callers must ensure the handler completes
493 /// quickly and that adequate backpressure or rate limiting is applied externally.
494 pub unsafe fn onconnect_sync<F, Re>(self, handler: F) -> SocketType
495 where
496 F: Fn(T) -> Result<Re, Rejection> + Clone + Send + Sync + 'static,
497 Re: Reply + Send + 'static,
498 {
499 log::warn!("Registering sync handler on {:?} '{}' (JSON body) — ensure rate-limiting is applied externally", self.base.method, self.base.path);
500 let m = self.base.method.filter();
501 let p = path_filter(&self.base.path);
502 m.and(p)
503 .and(warp::body::json::<T>())
504 .and_then(move |body: T| {
505 let handler = handler.clone();
506 async move {
507 tokio::task::spawn_blocking(move || handler(body))
508 .await
509 .unwrap_or_else(|_| {
510 log::warn!("Sync handler (JSON body) panicked; converting to Rejection");
511 Err(warp::reject())
512 })
513 }
514 })
515 .map(|r: Re| r.into_response())
516 .boxed()
517 }
518
519 /// Attaches shared state `S`, transitioning to [`StatefulJsonSocketBuilder`].
520 ///
521 /// Alternative ordering to `.state(s).json::<T>()` — both produce the same route.
522 /// A fresh clone of `S` is injected alongside the JSON-decoded `T` on every request.
523 /// The handler will receive `(state: S, body: T)`.
524 ///
525 /// `S` must be `Clone + Send + Sync + 'static`.
526 pub fn state<S: Clone + Send + Sync + 'static>(self, state: S) -> StatefulJsonSocketBuilder<T, S> {
527 StatefulJsonSocketBuilder { base: self.base, state, _phantom: std::marker::PhantomData }
528 }
529
530}
531
532/// Route builder that expects and deserialises URL query parameters of type `T`.
533///
534/// Obtained from [`ServerMechanism::query`]. Optionally attach shared state via
535/// [`state`](QuerySocketBuilder::state), or finalise immediately with
536/// [`onconnect`](QuerySocketBuilder::onconnect) /
537/// [`onconnect_sync`](QuerySocketBuilder::onconnect_sync).
538pub struct QuerySocketBuilder<T> {
539 base: ServerMechanism,
540 _phantom: std::marker::PhantomData<T>,
541}
542
543impl<T: DeserializeOwned + Send + 'static> QuerySocketBuilder<T> {
544 /// Finalises this route with an async handler that receives the deserialised query
545 /// parameters.
546 ///
547 /// Before the handler is called, the URL query string (`?field=value&...`) is parsed
548 /// and deserialised into `T`. The handler receives a ready-to-use `T` — no manual
549 /// parsing is needed. A missing or malformed query string is rejected before the
550 /// handler is ever invoked.
551 ///
552 /// The handler must return `Result<impl Reply, Rejection>`.
553 /// Returns a [`SocketType`] ready to be passed to [`Server::mechanism`].
554 pub fn onconnect<F, Fut, Re>(self, handler: F) -> SocketType
555 where
556 F: Fn(T) -> Fut + Clone + Send + Sync + 'static,
557 Fut: Future<Output = Result<Re, Rejection>> + Send,
558 Re: Reply + Send,
559 {
560 log::debug!("Finalising async {:?} route at '{}' (query params)", self.base.method, self.base.path);
561 let m = self.base.method.filter();
562 let p = path_filter(&self.base.path);
563 m.and(p)
564 .and(warp::filters::query::query::<T>())
565 .and_then(handler)
566 .map(|r: Re| r.into_response())
567 .boxed()
568 }
569
570 /// Finalises this route with a **synchronous** handler that receives the deserialised
571 /// query parameters.
572 ///
573 /// The query string is decoded into `T` before the handler is dispatched, identical to
574 /// the async variant. The closure may block but must complete quickly.
575 ///
576 /// Returns a [`SocketType`] ready to be passed to [`Server::mechanism`].
577 ///
578 /// # Safety
579 ///
580 /// Every incoming request spawns an independent task on Tokio's blocking thread pool.
581 /// The pool caps the number of live OS threads (default 512), but the **queue of waiting
582 /// tasks is unbounded** — under a traffic surge, tasks accumulate without limit, consuming
583 /// unbounded memory and causing severe latency spikes or OOM crashes before any queued task
584 /// gets a chance to run. Additionally, any panic inside the handler is silently converted
585 /// into a `Rejection`, masking runtime errors. Callers must ensure the handler completes
586 /// quickly and that adequate backpressure or rate limiting is applied externally.
587 pub unsafe fn onconnect_sync<F, Re>(self, handler: F) -> SocketType
588 where
589 F: Fn(T) -> Result<Re, Rejection> + Clone + Send + Sync + 'static,
590 Re: Reply + Send + 'static,
591 {
592 log::warn!("Registering sync handler on {:?} '{}' (query params) — ensure rate-limiting is applied externally", self.base.method, self.base.path);
593 let m = self.base.method.filter();
594 let p = path_filter(&self.base.path);
595 m.and(p)
596 .and(warp::filters::query::query::<T>())
597 .and_then(move |query: T| {
598 let handler = handler.clone();
599 async move {
600 tokio::task::spawn_blocking(move || handler(query))
601 .await
602 .unwrap_or_else(|_| {
603 log::warn!("Sync handler (query params) panicked; converting to Rejection");
604 Err(warp::reject())
605 })
606 }
607 })
608 .map(|r: Re| r.into_response())
609 .boxed()
610 }
611
612 /// Attaches shared state `S`, transitioning to [`StatefulQuerySocketBuilder`].
613 ///
614 /// Alternative ordering to `.state(s).query::<T>()` — both produce the same route.
615 /// A fresh clone of `S` is injected alongside the query-decoded `T` on every request.
616 /// The handler will receive `(state: S, query: T)`.
617 ///
618 /// `S` must be `Clone + Send + Sync + 'static`.
619 pub fn state<S: Clone + Send + Sync + 'static>(self, state: S) -> StatefulQuerySocketBuilder<T, S> {
620 StatefulQuerySocketBuilder { base: self.base, state, _phantom: std::marker::PhantomData }
621 }
622
623}
624
625
626/// Route builder that carries shared state `S` with no body or query expectation.
627///
628/// Obtained from [`ServerMechanism::state`]. `S` must be `Clone + Send + Sync + 'static`.
629/// For mutable shared state, wrap it in `Arc<Mutex<_>>` or `Arc<RwLock<_>>` before passing
630/// it here. Finalise with [`onconnect`](StatefulSocketBuilder::onconnect) /
631/// [`onconnect_sync`](StatefulSocketBuilder::onconnect_sync).
632pub struct StatefulSocketBuilder<S> {
633 base: ServerMechanism,
634 state: S,
635}
636
637
638impl<S: Clone + Send + Sync + 'static> StatefulSocketBuilder<S> {
639 /// Adds a JSON body expectation, transitioning to [`StatefulJsonSocketBuilder`].
640 ///
641 /// Alternative ordering to `.json::<T>().state(s)` — both produce the same route.
642 /// On each request the incoming JSON body is deserialised into `T` and a fresh clone
643 /// of `S` is prepared; both are handed to the handler together as `(state: S, body: T)`.
644 ///
645 /// Allows `.state(s).json::<T>()` as an alternative ordering to `.json::<T>().state(s)`.
646 pub fn json<T: DeserializeOwned + Send>(self) -> StatefulJsonSocketBuilder<T, S> {
647 log::trace!("Attaching JSON body expectation (after state) to {:?} route at '{}'", self.base.method, self.base.path);
648 StatefulJsonSocketBuilder { base: self.base, state: self.state, _phantom: std::marker::PhantomData }
649 }
650
651 /// Adds a query parameter expectation, transitioning to [`StatefulQuerySocketBuilder`].
652 ///
653 /// Alternative ordering to `.query::<T>().state(s)` — both produce the same route.
654 /// On each request the URL query string is deserialised into `T` and a fresh clone
655 /// of `S` is prepared; both are handed to the handler together as `(state: S, query: T)`.
656 ///
657 /// Allows `.state(s).query::<T>()` as an alternative ordering to `.query::<T>().state(s)`.
658 pub fn query<T: DeserializeOwned + Send>(self) -> StatefulQuerySocketBuilder<T, S> {
659 log::trace!("Attaching query param expectation (after state) to {:?} route at '{}'", self.base.method, self.base.path);
660 StatefulQuerySocketBuilder { base: self.base, state: self.state, _phantom: std::marker::PhantomData }
661 }
662
663 /// Adds an encrypted body expectation, transitioning to [`StatefulEncryptedBodyBuilder`].
664 ///
665 /// Alternative ordering to `.encryption::<T>(key).state(s)` — both produce the same
666 /// route. On each request the raw body bytes are VEIL-decrypted using `key` before
667 /// the handler runs; a wrong key or corrupt body returns `403 Forbidden` without
668 /// invoking the handler. The handler will receive `(state: S, body: T)` where `T` is
669 /// always a trusted, fully-decrypted value.
670 ///
671 /// Allows `.state(s).encryption::<T>(key)` as an alternative ordering to
672 /// `.encryption::<T>(key).state(s)`.
673 pub fn encryption<T>(self, key: SerializationKey) -> StatefulEncryptedBodyBuilder<T, S> {
674 log::trace!("Attaching encrypted body (after state) to {:?} route at '{}'", self.base.method, self.base.path);
675 StatefulEncryptedBodyBuilder { base: self.base, key, state: self.state, _phantom: std::marker::PhantomData }
676 }
677
678 /// Adds an encrypted query expectation, transitioning to [`StatefulEncryptedQueryBuilder`].
679 ///
680 /// Alternative ordering to `.encrypted_query::<T>(key).state(s)` — both produce the
681 /// same route. On each request the `?data=<base64url>` query parameter is
682 /// base64-decoded then VEIL-decrypted using `key`; a missing, malformed, or
683 /// undecryptable payload returns `403 Forbidden` without invoking the handler. The
684 /// handler will receive `(state: S, query: T)` where `T` is always a trusted,
685 /// fully-decrypted value.
686 ///
687 /// Allows `.state(s).encrypted_query::<T>(key)` as an alternative ordering to
688 /// `.encrypted_query::<T>(key).state(s)`.
689 pub fn encrypted_query<T>(self, key: SerializationKey) -> StatefulEncryptedQueryBuilder<T, S> {
690 log::trace!("Attaching encrypted query (after state) to {:?} route at '{}'", self.base.method, self.base.path);
691 StatefulEncryptedQueryBuilder { base: self.base, key, state: self.state, _phantom: std::marker::PhantomData }
692 }
693
694 /// Finalises this route with an async handler that receives only the shared state.
695 ///
696 /// On each request a fresh clone of `S` is injected into the handler. No request body
697 /// or query parameters are read. The handler must return `Result<impl Reply, Rejection>`.
698 ///
699 /// Returns a [`SocketType`] ready to be passed to [`Server::mechanism`].
700 pub fn onconnect<F, Fut, Re>(self, handler: F) -> SocketType
701 where
702 F: Fn(S) -> Fut + Clone + Send + Sync + 'static,
703 Fut: Future<Output = Result<Re, Rejection>> + Send,
704 Re: Reply + Send,
705 {
706 log::debug!("Finalising async {:?} route at '{}' (state)", self.base.method, self.base.path);
707 let m = self.base.method.filter();
708 let p = path_filter(&self.base.path);
709 let state = self.state;
710 let state_filter = warp::any().map(move || state.clone());
711 m.and(p)
712 .and(state_filter)
713 .and_then(handler)
714 .map(|r: Re| r.into_response())
715 .boxed()
716 }
717
718 /// Finalises this route with a **synchronous** handler that receives only the shared
719 /// state.
720 ///
721 /// On each request a fresh clone of `S` is passed to the handler. If `S` wraps a mutex
722 /// or lock, contention across concurrent requests can stall the thread pool — ensure the
723 /// lock is held only briefly. The closure may block but must complete quickly.
724 ///
725 /// Returns a [`SocketType`] ready to be passed to [`Server::mechanism`].
726 ///
727 /// # Safety
728 ///
729 /// Every incoming request spawns an independent task on Tokio's blocking thread pool.
730 /// The pool caps the number of live OS threads (default 512), but the **queue of waiting
731 /// tasks is unbounded** — under a traffic surge, tasks accumulate without limit, consuming
732 /// unbounded memory and causing severe latency spikes or OOM crashes before any queued task
733 /// gets a chance to run. When the handler acquires a lock on `S` (e.g. `Arc<Mutex<_>>`),
734 /// concurrent blocking tasks contending on the same lock can stall indefinitely, causing the
735 /// thread pool queue to grow without bound and compounding the exhaustion risk. Additionally,
736 /// any panic inside the handler is silently converted into a `Rejection`, masking runtime
737 /// errors. Callers must ensure the handler completes quickly, that lock contention on `S`
738 /// cannot produce indefinite stalls, and that adequate backpressure or rate limiting is
739 /// applied externally.
740 pub unsafe fn onconnect_sync<F, Re>(self, handler: F) -> SocketType
741 where
742 F: Fn(S) -> Result<Re, Rejection> + Clone + Send + Sync + 'static,
743 Re: Reply + Send + 'static,
744 {
745 log::warn!("Registering sync handler on {:?} '{}' (state) — ensure rate-limiting and lock-free state are in place", self.base.method, self.base.path);
746 let m = self.base.method.filter();
747 let p = path_filter(&self.base.path);
748 let state = self.state;
749 let state_filter = warp::any().map(move || state.clone());
750 m.and(p)
751 .and(state_filter)
752 .and_then(move |s: S| {
753 let handler = handler.clone();
754 async move {
755 tokio::task::spawn_blocking(move || handler(s))
756 .await
757 .unwrap_or_else(|_| {
758 log::warn!("Sync handler (state) panicked; converting to Rejection");
759 Err(warp::reject())
760 })
761 }
762 })
763 .map(|r: Re| r.into_response())
764 .boxed()
765 }
766}
767
768
769/// Route builder that carries shared state `S` and expects a JSON body of type `T`.
770///
771/// Obtained from [`JsonSocketBuilder::state`]. `S` must be `Clone + Send + Sync + 'static`.
772/// `T` must implement `serde::de::DeserializeOwned`. Finalise with
773/// [`onconnect`](StatefulJsonSocketBuilder::onconnect) /
774/// [`onconnect_sync`](StatefulJsonSocketBuilder::onconnect_sync).
775pub struct StatefulJsonSocketBuilder<T, S> {
776 base: ServerMechanism,
777 state: S,
778 _phantom: std::marker::PhantomData<T>,
779}
780
781impl<T: DeserializeOwned + Send + 'static, S: Clone + Send + Sync + 'static>
782 StatefulJsonSocketBuilder<T, S>
783{
784 /// Finalises this route with an async handler that receives `(state: S, body: T)`.
785 ///
786 /// On each request the incoming JSON body is deserialised into `T` and a fresh clone
787 /// of `S` is prepared — both are handed to the handler together. The handler is only
788 /// called when the body can be decoded; a missing or malformed body is rejected first.
789 ///
790 /// The handler must return `Result<impl Reply, Rejection>`.
791 /// Returns a [`SocketType`] ready to be passed to [`Server::mechanism`].
792 pub fn onconnect<F, Fut, Re>(self, handler: F) -> SocketType
793 where
794 F: Fn(S, T) -> Fut + Clone + Send + Sync + 'static,
795 Fut: Future<Output = Result<Re, Rejection>> + Send,
796 Re: Reply + Send,
797 {
798 log::debug!("Finalising async {:?} route at '{}' (state + JSON body)", self.base.method, self.base.path);
799 let m = self.base.method.filter();
800 let p = path_filter(&self.base.path);
801 let state = self.state;
802 let state_filter = warp::any().map(move || state.clone());
803 m.and(p)
804 .and(state_filter)
805 .and(warp::body::json::<T>())
806 .and_then(handler)
807 .map(|r: Re| r.into_response())
808 .boxed()
809 }
810
811 /// Finalises this route with a **synchronous** handler that receives
812 /// `(state: S, body: T)`.
813 ///
814 /// The body is decoded into `T` and a clone of `S` is prepared before the blocking
815 /// handler is dispatched. If `S` wraps a lock, keep it held only briefly. See
816 /// [`ServerMechanism::onconnect_sync`] for the full thread-pool safety notes.
817 ///
818 /// Returns a [`SocketType`] ready to be passed to [`Server::mechanism`].
819 ///
820 /// # Safety
821 ///
822 /// Every incoming request spawns an independent task on Tokio's blocking thread pool.
823 /// The pool caps the number of live OS threads (default 512), but the **queue of waiting
824 /// tasks is unbounded** — under a traffic surge, tasks accumulate without limit, consuming
825 /// unbounded memory and causing severe latency spikes or OOM crashes before any queued task
826 /// gets a chance to run. When the handler acquires a lock on `S` (e.g. `Arc<Mutex<_>>`),
827 /// concurrent blocking tasks contending on the same lock can stall indefinitely, causing the
828 /// thread pool queue to grow without bound and compounding the exhaustion risk. Additionally,
829 /// any panic inside the handler is silently converted into a `Rejection`, masking runtime
830 /// errors. Callers must ensure the handler completes quickly, that lock contention on `S`
831 /// cannot produce indefinite stalls, and that adequate backpressure or rate limiting is
832 /// applied externally.
833 pub unsafe fn onconnect_sync<F, Re>(self, handler: F) -> SocketType
834 where
835 F: Fn(S, T) -> Result<Re, Rejection> + Clone + Send + Sync + 'static,
836 Re: Reply + Send + 'static,
837 {
838 log::warn!("Registering sync handler on {:?} '{}' (state + JSON body) — ensure rate-limiting and lock-free state are in place", self.base.method, self.base.path);
839 let m = self.base.method.filter();
840 let p = path_filter(&self.base.path);
841 let state = self.state;
842 let state_filter = warp::any().map(move || state.clone());
843 m.and(p)
844 .and(state_filter)
845 .and(warp::body::json::<T>())
846 .and_then(move |s: S, body: T| {
847 let handler = handler.clone();
848 async move {
849 tokio::task::spawn_blocking(move || handler(s, body))
850 .await
851 .unwrap_or_else(|_| {
852 log::warn!("Sync handler (state + JSON body) panicked; converting to Rejection");
853 Err(warp::reject())
854 })
855 }
856 })
857 .map(|r: Re| r.into_response())
858 .boxed()
859 }
860}
861
862/// Route builder that carries shared state `S` and expects URL query parameters of type `T`.
863///
864/// Obtained from [`QuerySocketBuilder::state`]. `S` must be `Clone + Send + Sync + 'static`.
865/// `T` must implement `serde::de::DeserializeOwned`. Finalise with
866/// [`onconnect`](StatefulQuerySocketBuilder::onconnect) /
867/// [`onconnect_sync`](StatefulQuerySocketBuilder::onconnect_sync).
868pub struct StatefulQuerySocketBuilder<T, S> {
869 base: ServerMechanism,
870 state: S,
871 _phantom: std::marker::PhantomData<T>,
872}
873
874impl<T: DeserializeOwned + Send + 'static, S: Clone + Send + Sync + 'static>
875 StatefulQuerySocketBuilder<T, S>
876{
877 /// Finalises this route with an async handler that receives `(state: S, query: T)`.
878 ///
879 /// On each request the URL query string is deserialised into `T` and a fresh clone of
880 /// `S` is prepared — both are handed to the handler together. A missing or malformed
881 /// query string is rejected before the handler is ever invoked.
882 ///
883 /// The handler must return `Result<impl Reply, Rejection>`.
884 /// Returns a [`SocketType`] ready to be passed to [`Server::mechanism`].
885 pub fn onconnect<F, Fut, Re>(self, handler: F) -> SocketType
886 where
887 F: Fn(S, T) -> Fut + Clone + Send + Sync + 'static,
888 Fut: Future<Output = Result<Re, Rejection>> + Send,
889 Re: Reply + Send,
890 {
891 log::debug!("Finalising async {:?} route at '{}' (state + query params)", self.base.method, self.base.path);
892 let m = self.base.method.filter();
893 let p = path_filter(&self.base.path);
894 let state = self.state;
895 let state_filter = warp::any().map(move || state.clone());
896 m.and(p)
897 .and(state_filter)
898 .and(warp::filters::query::query::<T>())
899 .and_then(handler)
900 .map(|r: Re| r.into_response())
901 .boxed()
902 }
903
904 /// Finalises this route with a **synchronous** handler that receives
905 /// `(state: S, query: T)`.
906 ///
907 /// The query string is decoded into `T` and a clone of `S` is prepared before the
908 /// blocking handler is dispatched. If `S` wraps a lock, keep it held only briefly.
909 /// See [`ServerMechanism::onconnect_sync`] for the full thread-pool safety notes.
910 ///
911 /// Returns a [`SocketType`] ready to be passed to [`Server::mechanism`].
912 ///
913 /// # Safety
914 ///
915 /// Every incoming request spawns an independent task on Tokio's blocking thread pool.
916 /// The pool caps the number of live OS threads (default 512), but the **queue of waiting
917 /// tasks is unbounded** — under a traffic surge, tasks accumulate without limit, consuming
918 /// unbounded memory and causing severe latency spikes or OOM crashes before any queued task
919 /// gets a chance to run. When the handler acquires a lock on `S` (e.g. `Arc<Mutex<_>>`),
920 /// concurrent blocking tasks contending on the same lock can stall indefinitely, causing the
921 /// thread pool queue to grow without bound and compounding the exhaustion risk. Additionally,
922 /// any panic inside the handler is silently converted into a `Rejection`, masking runtime
923 /// errors. Callers must ensure the handler completes quickly, that lock contention on `S`
924 /// cannot produce indefinite stalls, and that adequate backpressure or rate limiting is
925 /// applied externally.
926 pub unsafe fn onconnect_sync<F, Re>(self, handler: F) -> SocketType
927 where
928 F: Fn(S, T) -> Result<Re, Rejection> + Clone + Send + Sync + 'static,
929 Re: Reply + Send + 'static,
930 {
931 log::warn!("Registering sync handler on {:?} '{}' (state + query params) — ensure rate-limiting and lock-free state are in place", self.base.method, self.base.path);
932 let m = self.base.method.filter();
933 let p = path_filter(&self.base.path);
934 let state = self.state;
935 let state_filter = warp::any().map(move || state.clone());
936 m.and(p)
937 .and(state_filter)
938 .and(warp::filters::query::query::<T>())
939 .and_then(move |s: S, query: T| {
940 let handler = handler.clone();
941 async move {
942 tokio::task::spawn_blocking(move || handler(s, query))
943 .await
944 .unwrap_or_else(|_| {
945 log::warn!("Sync handler (state + query params) panicked; converting to Rejection");
946 Err(warp::reject())
947 })
948 }
949 })
950 .map(|r: Re| r.into_response())
951 .boxed()
952 }
953}
954
955// ─── EncryptedBodyBuilder ────────────────────────────────────────────────────
956
957/// Route builder that expects a VEIL-encrypted request body of type `T`.
958///
959/// Obtained from [`ServerMechanism::encryption`]. On each matching request the raw
960/// body bytes are decrypted using the [`SerializationKey`] supplied there. If
961/// decryption fails for any reason (wrong key, mismatched secret, corrupted payload)
962/// the route immediately returns `403 Forbidden` — the handler is never invoked.
963///
964/// Optionally attach shared state via [`state`](EncryptedBodyBuilder::state) before
965/// finalising with [`onconnect`](EncryptedBodyBuilder::onconnect) /
966/// [`onconnect_sync`](EncryptedBodyBuilder::onconnect_sync).
967pub struct EncryptedBodyBuilder<T> {
968 base: ServerMechanism,
969 key: SerializationKey,
970 _phantom: std::marker::PhantomData<T>,
971}
972
973impl<T> EncryptedBodyBuilder<T>
974where
975 T: bincode::Decode<()> + Send + 'static,
976{
977 /// Finalises this route with an async handler that receives the decrypted body as `T`.
978 ///
979 /// On each request the raw body bytes are VEIL-decrypted using the [`SerializationKey`]
980 /// configured on this builder. The handler is only invoked when decryption succeeds —
981 /// a wrong key, mismatched secret, or corrupted body causes the route to return
982 /// `403 Forbidden` without ever reaching the handler. The `T` the closure receives is
983 /// therefore always a trusted, fully-decrypted value ready to use.
984 ///
985 /// The handler must return `Result<impl Reply, Rejection>`.
986 /// Returns a [`SocketType`] ready to be passed to [`Server::mechanism`].
987 pub fn onconnect<F, Fut, Re>(self, handler: F) -> SocketType
988 where
989 F: Fn(T) -> Fut + Clone + Send + Sync + 'static,
990 Fut: Future<Output = Result<Re, Rejection>> + Send,
991 Re: Reply + Send,
992 {
993 log::debug!("Finalising async {:?} route at '{}' (encrypted body)", self.base.method, self.base.path);
994 let m = self.base.method.filter();
995 let p = path_filter(&self.base.path);
996 let key = self.key;
997 m.and(p)
998 .and(warp::body::bytes())
999 .and_then(move |raw: bytes::Bytes| {
1000 let key = key.clone();
1001 let handler = handler.clone();
1002 async move {
1003 let value: T = decode_body(&raw, &key)?;
1004 handler(value).await.map(|r| r.into_response())
1005 }
1006 })
1007 .boxed()
1008 }
1009
1010 /// Finalises this route with a **synchronous** handler that receives the decrypted
1011 /// body as `T`.
1012 ///
1013 /// Decryption happens before the handler is dispatched to the thread pool: if the key
1014 /// is wrong or the body is corrupt the request is rejected with `403 Forbidden` and
1015 /// the thread pool is not touched at all. The `T` handed to the closure is always a
1016 /// trusted, fully-decrypted value. The closure may block but must complete quickly.
1017 ///
1018 /// Returns a [`SocketType`] ready to be passed to [`Server::mechanism`].
1019 ///
1020 /// # Safety
1021 /// See [`ServerMechanism::onconnect_sync`] for the thread-pool safety notes.
1022 pub unsafe fn onconnect_sync<F, Re>(self, handler: F) -> SocketType
1023 where
1024 F: Fn(T) -> Result<Re, Rejection> + Clone + Send + Sync + 'static,
1025 Re: Reply + Send + 'static,
1026 {
1027 log::warn!("Registering sync handler on {:?} '{}' (encrypted body) — ensure rate-limiting is applied externally", self.base.method, self.base.path);
1028 let m = self.base.method.filter();
1029 let p = path_filter(&self.base.path);
1030 let key = self.key;
1031 m.and(p)
1032 .and(warp::body::bytes())
1033 .and_then(move |raw: bytes::Bytes| {
1034 let key = key.clone();
1035 let handler = handler.clone();
1036 async move {
1037 let value: T = decode_body(&raw, &key)?;
1038 tokio::task::spawn_blocking(move || handler(value))
1039 .await
1040 .unwrap_or_else(|_| {
1041 log::warn!("Sync encrypted handler panicked; converting to Rejection");
1042 Err(warp::reject())
1043 })
1044 .map(|r| r.into_response())
1045 }
1046 })
1047 .boxed()
1048 }
1049
1050 /// Attaches shared state `S`, transitioning to [`StatefulEncryptedBodyBuilder`].
1051 ///
1052 /// A fresh clone of `S` is injected alongside the decrypted `T` on every request.
1053 /// The handler will receive `(state: S, body: T)`.
1054 ///
1055 /// `S` must be `Clone + Send + Sync + 'static`.
1056 pub fn state<S: Clone + Send + Sync + 'static>(self, state: S) -> StatefulEncryptedBodyBuilder<T, S> {
1057 StatefulEncryptedBodyBuilder { base: self.base, key: self.key, state, _phantom: std::marker::PhantomData }
1058 }
1059}
1060
1061// ─── EncryptedQueryBuilder ────────────────────────────────────────────────────
1062
1063/// Route builder that expects VEIL-encrypted URL query parameters of type `T`.
1064///
1065/// Obtained from [`ServerMechanism::encrypted_query`]. The client must send a single
1066/// `?data=<base64url>` query parameter whose value is the URL-safe base64 encoding of
1067/// the VEIL-encrypted struct bytes. On each matching request the server base64-decodes
1068/// then decrypts the value using the configured [`SerializationKey`]. If any step fails
1069/// (missing `data` parameter, invalid base64, wrong key, corrupt bytes) the route
1070/// returns `403 Forbidden` — the handler is never invoked.
1071///
1072/// Optionally attach shared state via [`state`](EncryptedQueryBuilder::state) before
1073/// finalising with [`onconnect`](EncryptedQueryBuilder::onconnect).
1074pub struct EncryptedQueryBuilder<T> {
1075 base: ServerMechanism,
1076 key: SerializationKey,
1077 _phantom: std::marker::PhantomData<T>,
1078}
1079
1080impl<T> EncryptedQueryBuilder<T>
1081where
1082 T: bincode::Decode<()> + Send + 'static,
1083{
1084 /// Finalises this route with an async handler that receives the decrypted query
1085 /// parameters as `T`.
1086 ///
1087 /// On each request the `?data=<base64url>` query parameter is base64-decoded then
1088 /// VEIL-decrypted using the [`SerializationKey`] on this builder. The handler is only
1089 /// invoked when every step succeeds — a missing `data` parameter, invalid base64,
1090 /// wrong key, or corrupt payload causes the route to return `403 Forbidden` without
1091 /// ever reaching the handler. The `T` the closure receives is therefore always a
1092 /// trusted, fully-decrypted value ready to use.
1093 ///
1094 /// The handler must return `Result<impl Reply, Rejection>`.
1095 /// Returns a [`SocketType`] ready to be passed to [`Server::mechanism`].
1096 pub fn onconnect<F, Fut, Re>(self, handler: F) -> SocketType
1097 where
1098 F: Fn(T) -> Fut + Clone + Send + Sync + 'static,
1099 Fut: Future<Output = Result<Re, Rejection>> + Send,
1100 Re: Reply + Send,
1101 {
1102 log::debug!("Finalising async {:?} route at '{}' (encrypted query)", self.base.method, self.base.path);
1103 let m = self.base.method.filter();
1104 let p = path_filter(&self.base.path);
1105 let key = self.key;
1106 m.and(p)
1107 .and(warp::filters::query::raw())
1108 .and_then(move |raw_query: String| {
1109 let key = key.clone();
1110 let handler = handler.clone();
1111 async move {
1112 let value: T = decode_query(&raw_query, &key)?;
1113 handler(value).await.map(|r| r.into_response())
1114 }
1115 })
1116 .boxed()
1117 }
1118
1119 /// Attaches shared state `S`, transitioning to [`StatefulEncryptedQueryBuilder`].
1120 ///
1121 /// A fresh clone of `S` is injected alongside the decrypted `T` on every request.
1122 /// The handler will receive `(state: S, query: T)`.
1123 ///
1124 /// `S` must be `Clone + Send + Sync + 'static`.
1125 pub fn state<S: Clone + Send + Sync + 'static>(self, state: S) -> StatefulEncryptedQueryBuilder<T, S> {
1126 StatefulEncryptedQueryBuilder { base: self.base, key: self.key, state, _phantom: std::marker::PhantomData }
1127 }
1128}
1129
1130// ─── StatefulEncryptedBodyBuilder ────────────────────────────────────────────
1131
1132/// Route builder carrying shared state `S` and a VEIL-encrypted request body of type `T`.
1133///
1134/// Obtained from [`EncryptedBodyBuilder::state`] or [`StatefulSocketBuilder::encryption`].
1135/// On each matching request the body is VEIL-decrypted and a fresh clone of `S` is
1136/// prepared before the handler is called. A wrong key or corrupt body returns
1137/// `403 Forbidden` without ever invoking the handler.
1138///
1139/// Finalise with [`onconnect`](StatefulEncryptedBodyBuilder::onconnect).
1140pub struct StatefulEncryptedBodyBuilder<T, S> {
1141 base: ServerMechanism,
1142 key: SerializationKey,
1143 state: S,
1144 _phantom: std::marker::PhantomData<T>,
1145}
1146
1147impl<T, S> StatefulEncryptedBodyBuilder<T, S>
1148where
1149 T: bincode::Decode<()> + Send + 'static,
1150 S: Clone + Send + Sync + 'static,
1151{
1152 /// Finalises this route with an async handler that receives `(state: S, body: T)`.
1153 ///
1154 /// On each request the raw body bytes are VEIL-decrypted using the configured
1155 /// [`SerializationKey`] and a fresh clone of `S` is prepared — both are handed to the
1156 /// handler together. If decryption fails (wrong key or corrupt body) the route returns
1157 /// `403 Forbidden` and the handler is never invoked. The `T` the closure receives is
1158 /// always a trusted, fully-decrypted value ready to use.
1159 ///
1160 /// The handler must return `Result<impl Reply, Rejection>`.
1161 /// Returns a [`SocketType`] ready to be passed to [`Server::mechanism`].
1162 pub fn onconnect<F, Fut, Re>(self, handler: F) -> SocketType
1163 where
1164 F: Fn(S, T) -> Fut + Clone + Send + Sync + 'static,
1165 Fut: Future<Output = Result<Re, Rejection>> + Send,
1166 Re: Reply + Send,
1167 {
1168 log::debug!("Finalising async {:?} route at '{}' (state + encrypted body)", self.base.method, self.base.path);
1169 let m = self.base.method.filter();
1170 let p = path_filter(&self.base.path);
1171 let key = self.key;
1172 let state = self.state;
1173 let state_filter = warp::any().map(move || state.clone());
1174 m.and(p)
1175 .and(state_filter)
1176 .and(warp::body::bytes())
1177 .and_then(move |s: S, raw: bytes::Bytes| {
1178 let key = key.clone();
1179 let handler = handler.clone();
1180 async move {
1181 let value: T = decode_body(&raw, &key)?;
1182 handler(s, value).await.map(|r| r.into_response())
1183 }
1184 })
1185 .boxed()
1186 }
1187}
1188
1189// ─── StatefulEncryptedQueryBuilder ───────────────────────────────────────────
1190
1191/// Route builder carrying shared state `S` and VEIL-encrypted query parameters of
1192/// type `T`.
1193///
1194/// Obtained from [`EncryptedQueryBuilder::state`] or
1195/// [`StatefulSocketBuilder::encrypted_query`]. On each matching request the
1196/// `?data=<base64url>` parameter is base64-decoded then VEIL-decrypted and a fresh
1197/// clone of `S` is prepared before the handler is called. Any decode or decryption
1198/// failure returns `403 Forbidden` without ever invoking the handler.
1199///
1200/// Finalise with [`onconnect`](StatefulEncryptedQueryBuilder::onconnect).
1201pub struct StatefulEncryptedQueryBuilder<T, S> {
1202 base: ServerMechanism,
1203 key: SerializationKey,
1204 state: S,
1205 _phantom: std::marker::PhantomData<T>,
1206}
1207
1208impl<T, S> StatefulEncryptedQueryBuilder<T, S>
1209where
1210 T: bincode::Decode<()> + Send + 'static,
1211 S: Clone + Send + Sync + 'static,
1212{
1213 /// Finalises this route with an async handler that receives `(state: S, query: T)`.
1214 ///
1215 /// On each request the `?data=<base64url>` query parameter is base64-decoded then
1216 /// VEIL-decrypted using the configured [`SerializationKey`] and a fresh clone of `S`
1217 /// is prepared — both are handed to the handler together. If any step fails (missing
1218 /// parameter, bad encoding, wrong key, or corrupt data) the route returns
1219 /// `403 Forbidden` and the handler is never invoked. The `T` the closure receives is
1220 /// always a trusted, fully-decrypted value ready to use.
1221 ///
1222 /// The handler must return `Result<impl Reply, Rejection>`.
1223 /// Returns a [`SocketType`] ready to be passed to [`Server::mechanism`].
1224 pub fn onconnect<F, Fut, Re>(self, handler: F) -> SocketType
1225 where
1226 F: Fn(S, T) -> Fut + Clone + Send + Sync + 'static,
1227 Fut: Future<Output = Result<Re, Rejection>> + Send,
1228 Re: Reply + Send,
1229 {
1230 log::debug!("Finalising async {:?} route at '{}' (state + encrypted query)", self.base.method, self.base.path);
1231 let m = self.base.method.filter();
1232 let p = path_filter(&self.base.path);
1233 let key = self.key;
1234 let state = self.state;
1235 let state_filter = warp::any().map(move || state.clone());
1236 m.and(p)
1237 .and(state_filter)
1238 .and(warp::filters::query::raw())
1239 .and_then(move |s: S, raw_query: String| {
1240 let key = key.clone();
1241 let handler = handler.clone();
1242 async move {
1243 let value: T = decode_query(&raw_query, &key)?;
1244 handler(s, value).await.map(|r| r.into_response())
1245 }
1246 })
1247 .boxed()
1248 }
1249}
1250
1251// ─── Internal decode helpers ─────────────────────────────────────────────────
1252
1253/// Decode a VEIL-sealed request body into `T`, returning `403 Forbidden` on any failure.
1254fn decode_body<T: bincode::Decode<()>>(raw: &bytes::Bytes, key: &SerializationKey) -> Result<T, Rejection> {
1255 crate::serialization::open(raw, key.veil_key()).map_err(|e| {
1256 log::debug!("VEIL open failed (key mismatch or corrupt body): {e}");
1257 forbidden()
1258 })
1259}
1260
1261/// Decode a VEIL-sealed query parameter (`?data=<base64url>`) into `T`,
1262/// returning `403 Forbidden` on any failure.
1263fn decode_query<T: bincode::Decode<()>>(raw_query: &str, key: &SerializationKey) -> Result<T, Rejection> {
1264 // Extract the `data` parameter value from the raw query string.
1265 let data_value = serde_urlencoded::from_str::<std::collections::HashMap<String, String>>(raw_query)
1266 .ok()
1267 .and_then(|mut m| m.remove("data"));
1268
1269 let b64 = data_value.ok_or_else(|| {
1270 log::debug!("Encrypted query missing `data` parameter");
1271 forbidden()
1272 })?;
1273
1274 let bytes = base64::engine::Engine::decode(
1275 &base64::engine::general_purpose::URL_SAFE_NO_PAD,
1276 b64.trim_end_matches('='),
1277 )
1278 .map_err(|e| {
1279 log::debug!("base64url decode failed: {e}");
1280 forbidden()
1281 })?;
1282
1283 crate::serialization::open(&bytes, key.veil_key()).map_err(|e| {
1284 log::debug!("VEIL open (query) failed: {e}");
1285 forbidden()
1286 })
1287}
1288
1289/// Builds a `403 Forbidden` rejection response.
1290fn forbidden() -> Rejection {
1291 // Warp doesn't expose a built-in 403 rejection, so we use a custom one.
1292 warp::reject::custom(ForbiddenError)
1293}
1294
1295#[derive(Debug)]
1296struct ForbiddenError;
1297
1298impl warp::reject::Reject for ForbiddenError {}
1299
1300/// A collection of common HTTP status codes used with the reply helpers.
1301///
1302/// Covers 2xx success, 3xx redirect, 4xx client error, and 5xx server error ranges.
1303/// Converts into `warp::http::StatusCode` via [`From`] and is accepted by
1304/// [`reply_with_status`] and [`reply_with_status_and_json`]. Also usable directly in the
1305/// [`reply!`] macro via the `status => Status::X` argument.
1306#[derive(Clone, Copy, Debug)]
1307pub enum Status {
1308 // 2xx
1309 Ok,
1310 Created,
1311 Accepted,
1312 NoContent,
1313 // 3xx
1314 MovedPermanently,
1315 Found,
1316 NotModified,
1317 TemporaryRedirect,
1318 PermanentRedirect,
1319 // 4xx
1320 BadRequest,
1321 Unauthorized,
1322 Forbidden,
1323 NotFound,
1324 MethodNotAllowed,
1325 Conflict,
1326 Gone,
1327 UnprocessableEntity,
1328 TooManyRequests,
1329 // 5xx
1330 InternalServerError,
1331 NotImplemented,
1332 BadGateway,
1333 ServiceUnavailable,
1334 GatewayTimeout,
1335}
1336
1337impl From<Status> for warp::http::StatusCode {
1338 fn from(s: Status) -> Self {
1339 match s {
1340 Status::Ok => warp::http::StatusCode::OK,
1341 Status::Created => warp::http::StatusCode::CREATED,
1342 Status::Accepted => warp::http::StatusCode::ACCEPTED,
1343 Status::NoContent => warp::http::StatusCode::NO_CONTENT,
1344 Status::MovedPermanently => warp::http::StatusCode::MOVED_PERMANENTLY,
1345 Status::Found => warp::http::StatusCode::FOUND,
1346 Status::NotModified => warp::http::StatusCode::NOT_MODIFIED,
1347 Status::TemporaryRedirect => warp::http::StatusCode::TEMPORARY_REDIRECT,
1348 Status::PermanentRedirect => warp::http::StatusCode::PERMANENT_REDIRECT,
1349 Status::BadRequest => warp::http::StatusCode::BAD_REQUEST,
1350 Status::Unauthorized => warp::http::StatusCode::UNAUTHORIZED,
1351 Status::Forbidden => warp::http::StatusCode::FORBIDDEN,
1352 Status::NotFound => warp::http::StatusCode::NOT_FOUND,
1353 Status::MethodNotAllowed => warp::http::StatusCode::METHOD_NOT_ALLOWED,
1354 Status::Conflict => warp::http::StatusCode::CONFLICT,
1355 Status::Gone => warp::http::StatusCode::GONE,
1356 Status::UnprocessableEntity => warp::http::StatusCode::UNPROCESSABLE_ENTITY,
1357 Status::TooManyRequests => warp::http::StatusCode::TOO_MANY_REQUESTS,
1358 Status::InternalServerError => warp::http::StatusCode::INTERNAL_SERVER_ERROR,
1359 Status::NotImplemented => warp::http::StatusCode::NOT_IMPLEMENTED,
1360 Status::BadGateway => warp::http::StatusCode::BAD_GATEWAY,
1361 Status::ServiceUnavailable => warp::http::StatusCode::SERVICE_UNAVAILABLE,
1362 Status::GatewayTimeout => warp::http::StatusCode::GATEWAY_TIMEOUT,
1363 }
1364 }
1365}
1366
1367/// The HTTP server that owns and dispatches a collection of [`SocketType`] routes.
1368///
1369/// Build routes through the [`ServerMechanism`] builder chain, register each with
1370/// [`mechanism`](Server::mechanism), then start the server with [`serve`](Server::serve).
1371///
1372/// # Example
1373/// ```rust,no_run
1374/// # use serde::Serialize;
1375/// # #[derive(Serialize)] struct Pong { ok: bool }
1376///
1377/// let mut server = Server::default();
1378///
1379/// server
1380/// .mechanism(
1381/// ServerMechanism::get("/ping")
1382/// .onconnect(|| async { reply!(json => Pong { ok: true }) })
1383/// )
1384/// .mechanism(
1385/// ServerMechanism::delete("/session")
1386/// .onconnect(|| async { reply!(message => warp::reply(), status => Status::NoContent) })
1387/// );
1388///
1389/// // Blocks forever — call only to actually run the server:
1390/// // server.serve(([0, 0, 0, 0], 8080)).await;
1391/// ```
1392///
1393/// # Caution
1394/// Calling [`serve`](Server::serve) with no routes registered will **panic**.
1395pub struct Server {
1396 mechanisms: Vec<SocketType>,
1397}
1398
1399impl Default for Server {
1400 fn default() -> Self {
1401 Self::new()
1402 }
1403}
1404
1405impl Server {
1406 fn new() -> Self {
1407 Self { mechanisms: Vec::new() }
1408 }
1409
1410 /// Registers a [`SocketType`] route on this server.
1411 ///
1412 /// Routes are evaluated in registration order. Returns `&mut Self` for method chaining.
1413 pub fn mechanism(&mut self, mech: SocketType) -> &mut Self {
1414 self.mechanisms.push(mech);
1415 log::debug!("Route registered (total: {})", self.mechanisms.len());
1416 self
1417 }
1418
1419 /// Binds to `addr` and starts serving all registered routes.
1420 ///
1421 /// This is an infinite async loop — it never returns under normal operation.
1422 ///
1423 /// # Panics
1424 /// Panics immediately if no routes have been registered via [`mechanism`](Server::mechanism).
1425 pub async fn serve(self, addr: impl Into<SocketAddr>) {
1426 let addr = addr.into();
1427 log::info!("Server binding to {}", addr);
1428 let mut iter = self.mechanisms.into_iter();
1429 let first = iter.next().unwrap_or_else(|| {
1430 log::trace!("No mechanisms are defined on the server, this will result in a panic!");
1431 log::error!("The server contains no mechanisms to follow through");
1432 panic!();
1433 });
1434
1435 let combined = iter.fold(first.boxed(), |acc, next| {
1436 acc.or(next).unify().boxed()
1437 });
1438
1439 log::info!("Server running on {}", addr);
1440 warp::serve(combined).run(addr).await;
1441 }
1442
1443 /// Binds to `addr`, serves all registered routes, and shuts down gracefully when
1444 /// `shutdown` resolves.
1445 ///
1446 /// Equivalent to calling [`serve_from_listener`](Server::serve_from_listener) with an
1447 /// address instead of a pre-bound listener.
1448 ///
1449 /// # Example
1450 ///
1451 /// ```rust,no_run
1452 /// use tokio::sync::oneshot;
1453 ///
1454 /// # async fn run() {
1455 /// let (tx, rx) = oneshot::channel::<()>();
1456 /// // ... build server and mount routes ...
1457 /// // trigger shutdown later by calling tx.send(())
1458 /// server.serve_with_graceful_shutdown(([127, 0, 0, 1], 8080), async move {
1459 /// rx.await.ok();
1460 /// }).await;
1461 /// # }
1462 /// ```
1463 pub async fn serve_with_graceful_shutdown(
1464 self,
1465 addr: impl Into<std::net::SocketAddr>,
1466 shutdown: impl std::future::Future<Output = ()> + Send + 'static,
1467 ) {
1468 let addr = addr.into();
1469 log::info!("Server binding to {} (graceful shutdown enabled)", addr);
1470 let mut iter = self.mechanisms.into_iter();
1471 let first = iter.next().unwrap_or_else(|| {
1472 log::error!("The server contains no mechanisms to follow through");
1473 panic!("serve_with_graceful_shutdown called with no routes registered");
1474 });
1475 let combined = iter.fold(first.boxed(), |acc, next| acc.or(next).unify().boxed());
1476 log::info!("Server running on {} (graceful shutdown enabled)", addr);
1477 warp::serve(combined)
1478 .bind(addr)
1479 .await
1480 .graceful(shutdown)
1481 .run()
1482 .await;
1483 }
1484
1485 /// Serves all registered routes from an already-bound `listener`, shutting down gracefully
1486 /// when `shutdown` resolves.
1487 ///
1488 /// Use this when port `0` is passed to `TcpListener::bind` and you need to know the actual
1489 /// OS-assigned port before the server starts (e.g. to open a browser to the correct URL).
1490 ///
1491 /// # Example
1492 ///
1493 /// ```rust,no_run
1494 /// use tokio::net::TcpListener;
1495 /// use tokio::sync::oneshot;
1496 ///
1497 /// # async fn run() -> std::io::Result<()> {
1498 /// let listener = TcpListener::bind("127.0.0.1:0").await?;
1499 /// let port = listener.local_addr()?.port();
1500 /// println!("Will listen on port {port}");
1501 ///
1502 /// let (tx, rx) = oneshot::channel::<()>();
1503 /// // ... build server and mount routes ...
1504 /// server.serve_from_listener(listener, async move { rx.await.ok(); }).await;
1505 /// # Ok(())
1506 /// # }
1507 /// ```
1508 pub async fn serve_from_listener(
1509 self,
1510 listener: tokio::net::TcpListener,
1511 shutdown: impl std::future::Future<Output = ()> + Send + 'static,
1512 ) {
1513 log::info!("Server running on {} (graceful shutdown enabled)",
1514 listener.local_addr().map(|a| a.to_string()).unwrap_or_else(|_| "?".into()));
1515 let mut iter = self.mechanisms.into_iter();
1516 let first = iter.next().unwrap_or_else(|| {
1517 log::error!("The server contains no mechanisms to follow through");
1518 panic!("serve_from_listener called with no routes registered");
1519 });
1520 let combined = iter.fold(first.boxed(), |acc, next| acc.or(next).unify().boxed());
1521 warp::serve(combined)
1522 .incoming(listener)
1523 .graceful(shutdown)
1524 .run()
1525 .await;
1526 }
1527}
1528
1529/// Wraps `reply` with the given HTTP `status` code and returns it as a warp result.
1530///
1531/// Use when an endpoint needs to respond with a specific status alongside a plain message.
1532/// Pairs with the [`reply!`] macro form `reply!(message => ..., status => ...)`.
1533pub fn reply_with_status(status: Status, reply: impl Reply) -> Result<impl Reply, Rejection> {
1534 Ok::<_, Rejection>(warp::reply::with_status(reply, status.into()))
1535}
1536
1537/// Returns an empty `200 OK` warp reply result.
1538///
1539/// Useful for endpoints that need only to acknowledge a request with no body.
1540/// Equivalent to `reply!()`.
1541pub fn reply() -> Result<impl Reply, Rejection> {
1542 Ok::<_, Rejection>(warp::reply())
1543}
1544
1545/// Serialises `json` as a JSON body and returns it as a `200 OK` warp reply result.
1546///
1547/// `T` must implement `serde::Serialize`. Equivalent to `reply!(json => ...)`.
1548pub fn reply_with_json<T: Serialize>(json: &T) -> Result<impl Reply + use<T>, Rejection> {
1549 Ok::<_, Rejection>(warp::reply::json(json))
1550}
1551
1552/// Serialises `json` as a JSON body, attaches the given HTTP `status`, and returns a warp result.
1553///
1554/// `T` must implement `serde::Serialize`. Equivalent to `reply!(json => ..., status => ...)`.
1555pub fn reply_with_status_and_json<T: Serialize>(status: Status, json: &T) -> Result<impl Reply + use<T>, Rejection> {
1556 Ok::<_, Rejection>(warp::reply::with_status(warp::reply::json(json), status.into()))
1557}
1558
1559/// Seals `value` with `key` and returns it as an `application/octet-stream` response (`200 OK`).
1560///
1561/// `T` must implement `bincode::Encode`.
1562/// Equivalent to `reply!(sealed => value, key => key)`.
1563pub fn reply_sealed<T: bincode::Encode>(
1564 value: &T,
1565 key: SerializationKey,
1566) -> Result<warp::reply::Response, Rejection> {
1567 sealed_response(value, key, None)
1568}
1569
1570/// Seals `value` with `key`, attaches the given HTTP `status`, and returns it as a warp result.
1571///
1572/// Equivalent to `reply!(sealed => value, key => key, status => Status::X)`.
1573pub fn reply_sealed_with_status<T: bincode::Encode>(
1574 value: &T,
1575 key: SerializationKey,
1576 status: Status,
1577) -> Result<warp::reply::Response, Rejection> {
1578 sealed_response(value, key, Some(status))
1579}
1580
1581fn sealed_response<T: bincode::Encode>(
1582 value: &T,
1583 key: SerializationKey,
1584 status: Option<Status>,
1585) -> Result<warp::reply::Response, Rejection> {
1586 use warp::http::StatusCode;
1587 use warp::Reply;
1588 let code: StatusCode = status.map(Into::into).unwrap_or(StatusCode::OK);
1589 let sealed = crate::serialization::seal(value, key.veil_key()).map_err(|_| warp::reject())?;
1590 Ok(warp::reply::with_status(sealed, code).into_response())
1591}
1592
1593/// Convenience macro for constructing warp reply results inside route handlers.
1594///
1595/// | Syntax | Equivalent | Description |
1596/// |---|---|---|
1597/// | `reply!()` | [`reply()`] | Empty `200 OK` response. |
1598/// | `reply!(message => expr, status => Status::X)` | [`reply_with_status`] | Plain reply with a status code. |
1599/// | `reply!(json => expr)` | [`reply_with_json`] | JSON body with `200 OK`. |
1600/// | `reply!(json => expr, status => Status::X)` | [`reply_with_status_and_json`] | JSON body with a status code. |
1601/// | `reply!(sealed => expr, key => key)` | [`reply_sealed`] | VEIL-sealed (or JSON for `Plain`) body, `200 OK`. |
1602/// | `reply!(sealed => expr, key => key, status => Status::X)` | [`reply_sealed_with_status`] | VEIL-sealed (or JSON for `Plain`) body with status. |
1603///
1604/// # Example
1605/// ```rust,no_run
1606/// # use serde::{Deserialize, Serialize};
1607/// # #[derive(Deserialize, Serialize)] struct Item { id: u32, name: String }
1608///
1609/// // Empty 200 OK
1610/// let ping = ServerMechanism::get("/ping")
1611/// .onconnect(|| async { reply!() });
1612///
1613/// // Plain reply with a custom status
1614/// let gone = ServerMechanism::delete("/v1")
1615/// .onconnect(|| async {
1616/// reply!(message => warp::reply::html("endpoint deprecated"), status => Status::Gone)
1617/// });
1618///
1619/// // JSON body, 200 OK
1620/// let list = ServerMechanism::get("/items")
1621/// .onconnect(|| async {
1622/// let items: Vec<Item> = vec![];
1623/// reply!(json => items)
1624/// });
1625///
1626/// // JSON body with a custom status
1627/// let create = ServerMechanism::post("/items")
1628/// .json::<Item>()
1629/// .onconnect(|item| async move {
1630/// reply!(json => item, status => Status::Created)
1631/// });
1632/// ```
1633#[macro_export]
1634macro_rules! reply {
1635 () => {{
1636 $crate::socket::server::reply()
1637 }};
1638
1639 (message => $message: expr, status => $status: expr) => {{
1640 $crate::socket::server::reply_with_status($status, $message)
1641 }};
1642
1643 (json => $json: expr) => {{
1644 $crate::socket::server::reply_with_json(&$json)
1645 }};
1646
1647 (json => $json: expr, status => $status: expr) => {{
1648 $crate::socket::server::reply_with_status_and_json($status, &$json)
1649 }};
1650
1651 (sealed => $val: expr, key => $key: expr) => {{
1652 $crate::socket::server::reply_sealed(&$val, $key)
1653 }};
1654
1655 (sealed => $val: expr, key => $key: expr, status => $status: expr) => {{
1656 $crate::socket::server::reply_sealed_with_status(&$val, $key, $status)
1657 }};
1658}
1659