churust_core/extract.rs
1//! Extractors: typed handler arguments derived from a `Call`.
2//!
3//! Two traits, mirroring the proven axum split:
4//! - `FromCallParts`: borrows `&mut Call`, usable in ANY argument position.
5//! - `FromCall`: consumes the `Call`, usable ONLY as the LAST argument.
6//!
7//! Every `FromCallParts` is also a `FromCall` (blanket impl), so a parts-style
8//! extractor may also appear last. `Call` itself is `FromCall` (consuming), so
9//! a `|call: Call|` handler is just the arity-1, last-arg case.
10
11use crate::call::Call;
12use crate::error::{Error, Result};
13use async_trait::async_trait;
14use serde::de::DeserializeOwned;
15use std::sync::Arc;
16
17/// Extract a value from a borrowed `&mut Call`.
18///
19/// Implementors borrow the call rather than consuming it, so a `FromCallParts`
20/// extractor may appear in **any** handler argument position and several may be
21/// used together. Returning an `Err` short-circuits the handler and renders the
22/// [`Error`] as the response. The built-in [`Path`], [`Query`], [`State`], and
23/// [`BearerToken`] extractors implement this trait.
24///
25/// Implement it to define your own borrowing extractor:
26///
27/// ```
28/// use churust_core::{Call, Result, FromCallParts};
29/// use async_trait::async_trait;
30///
31/// struct MethodName(String);
32///
33/// #[async_trait]
34/// impl FromCallParts for MethodName {
35/// async fn from_call_parts(call: &mut Call) -> Result<Self> {
36/// Ok(MethodName(call.method().as_str().to_string()))
37/// }
38/// }
39/// # tokio::runtime::Runtime::new().unwrap().block_on(async {
40/// # use http::{HeaderMap, Method};
41/// # use bytes::Bytes;
42/// # let mut c = Call::new(Method::GET, "/".parse().unwrap(), HeaderMap::new(), Bytes::new());
43/// # assert_eq!(MethodName::from_call_parts(&mut c).await.unwrap().0, "GET");
44/// # });
45/// ```
46#[async_trait]
47pub trait FromCallParts: Sized + Send {
48 /// Build `Self` from a mutable borrow of the call, or return an [`Error`]
49 /// (which the handler renders as the response).
50 async fn from_call_parts(call: &mut Call) -> Result<Self>;
51}
52
53/// Extract a value by consuming the whole [`Call`].
54///
55/// Because it takes the `Call` by value, a `FromCall` extractor may appear only
56/// as the **last** handler argument. Body-consuming extractors (such as a JSON
57/// body) implement this directly. Every [`FromCallParts`] type is automatically
58/// a `FromCall` via a blanket impl, and [`Call`] itself is `FromCall` (so a
59/// `|c: Call|` handler is just the last-argument case).
60///
61/// ```
62/// use churust_core::{Call, FromCall};
63/// use http::{HeaderMap, Method};
64/// use bytes::Bytes;
65/// # tokio::runtime::Runtime::new().unwrap().block_on(async {
66/// let c = Call::new(Method::GET, "/".parse().unwrap(), HeaderMap::new(), Bytes::new());
67/// // `Call` is itself a `FromCall`:
68/// let back = Call::from_call(c).await.unwrap();
69/// assert_eq!(back.method(), &Method::GET);
70/// # });
71/// ```
72#[async_trait]
73pub trait FromCall: Sized + Send {
74 /// Build `Self` by consuming the call, or return an [`Error`] (which the
75 /// handler renders as the response).
76 async fn from_call(call: Call) -> Result<Self>;
77}
78
79/// Any parts extractor can also be the final argument.
80///
81/// `do_not_recommend`: when a *body* extractor is wrongly placed in a leading
82/// position, rustc would otherwise suggest implementing `FromCallParts` for it
83/// — which is the one thing that must not happen, since it would let the body
84/// be consumed twice.
85#[async_trait]
86#[diagnostic::do_not_recommend]
87impl<T: FromCallParts> FromCall for T {
88 async fn from_call(mut call: Call) -> Result<Self> {
89 T::from_call_parts(&mut call).await
90 }
91}
92
93/// An extractor that can distinguish *absent* from *malformed*.
94///
95/// Implement this and `Option<Self>` becomes usable as a handler argument. That
96/// is the whole design: one trait rather than a parallel `OptionalQuery`,
97/// `OptionalPath`, `OptionalHeader` type per extractor. The parallel-type
98/// approach doubles the API surface and its documentation, and it is the design
99/// axum-extra shipped, deprecated, and replaced with exactly this.
100///
101/// **`Ok(None)` means the input was not supplied. It does not mean extraction
102/// failed.** A malformed value must still be an `Err`, or a typo'd query string
103/// becomes a silent `None` and the handler quietly does the wrong thing with a
104/// default. The distinction is the reason the trait exists — without it,
105/// `Option<T>` could only ever mean "swallow every error".
106///
107/// ```
108/// use churust_core::{Churust, Query, TestClient};
109/// use serde::Deserialize;
110/// # tokio::runtime::Runtime::new().unwrap().block_on(async {
111/// #[derive(Deserialize)]
112/// struct Pager { page: u32 }
113///
114/// let app = Churust::server()
115/// .routing(|r| {
116/// r.get("/list", |p: Option<Query<Pager>>| async move {
117/// match p {
118/// Some(Query(p)) => format!("page {}", p.page),
119/// None => "unpaged".to_string(),
120/// }
121/// });
122/// })
123/// .build();
124/// let client = TestClient::new(app);
125/// assert_eq!(client.get("/list?page=2").send().await.text(), "page 2");
126/// assert_eq!(client.get("/list").send().await.text(), "unpaged");
127/// # });
128/// ```
129#[async_trait]
130pub trait OptionalFromCallParts: Sized + Send {
131 /// Build `Self`, or `Ok(None)` when the input is absent. Reserve `Err` for
132 /// input that was supplied and is wrong.
133 async fn from_call_parts_opt(call: &mut Call) -> Result<Option<Self>>;
134}
135
136/// `Option<T>` extracts wherever `T` opts into [`OptionalFromCallParts`].
137#[async_trait]
138impl<T: OptionalFromCallParts> FromCallParts for Option<T> {
139 async fn from_call_parts(call: &mut Call) -> Result<Self> {
140 T::from_call_parts_opt(call).await
141 }
142}
143
144/// Absent means no query string at all. A query string that is present but does
145/// not fit `T` is an error: the caller tried and got it wrong, which is not the
146/// same as not trying.
147#[async_trait]
148impl<T> OptionalFromCallParts for Query<T>
149where
150 T: DeserializeOwned + Send,
151{
152 async fn from_call_parts_opt(call: &mut Call) -> Result<Option<Self>> {
153 if call.query_string().is_empty() {
154 return Ok(None);
155 }
156 Query::<T>::from_call_parts(call).await.map(Some)
157 }
158}
159
160/// Absent means the route captured no parameters. A parameter that is present
161/// but does not parse into `T` is an error — reporting `None` there would claim
162/// the URL had no such parameter at all.
163#[async_trait]
164impl<T> OptionalFromCallParts for Path<T>
165where
166 T: DeserializeOwned + Send,
167{
168 async fn from_call_parts_opt(call: &mut Call) -> Result<Option<Self>> {
169 if call.params().is_empty() {
170 return Ok(None);
171 }
172 Path::<T>::from_call_parts(call).await.map(Some)
173 }
174}
175
176/// The whole `Call` as the final argument (Ktor call-style base case).
177/// NOTE: deliberately NOT `FromCallParts` — that would conflict with the
178/// blanket impl above.
179#[async_trait]
180impl FromCall for Call {
181 async fn from_call(call: Call) -> Result<Self> {
182 Ok(call)
183 }
184}
185
186/// Extracts a single path parameter, parsed into `T`.
187///
188/// For a route such as `"/users/{id}"`, `Path::<u64>` reads the first captured
189/// parameter (`{id}`). It reads positionally, so it is intended for routes with
190/// exactly one path parameter; for routes with several, read each by name with
191/// [`Call::param`](crate::Call::param). Extraction fails with `400 Bad Request`
192/// if there is no parameter or the value does not parse into `T`.
193///
194/// ```
195/// use churust_core::{Churust, Path, TestClient};
196/// # tokio::runtime::Runtime::new().unwrap().block_on(async {
197/// let app = Churust::server()
198/// .routing(|r| {
199/// r.get("/double/{n}", |Path(n): Path<i64>| async move {
200/// format!("{}", n * 2)
201/// });
202/// })
203/// .build();
204/// let res = TestClient::new(app).get("/double/21").send().await;
205/// assert_eq!(res.text(), "42");
206/// # });
207/// ```
208#[derive(Debug, Clone)]
209pub struct Path<T>(
210 /// The parsed path parameter value.
211 pub T,
212);
213
214#[async_trait]
215impl<T> FromCallParts for Path<T>
216where
217 T: serde::de::DeserializeOwned + Send,
218{
219 async fn from_call_parts(call: &mut Call) -> Result<Self> {
220 crate::path_de::from_params::<T>(call.params())
221 .map(Path)
222 .map_err(|e| Error::bad_request(format!("bad path parameters: {e}")))
223 }
224}
225
226/// Deserializes the URL query string into `T`.
227///
228/// `T` must implement [`serde::Deserialize`]. Missing required fields or
229/// otherwise malformed query strings fail with `400 Bad Request`.
230///
231/// A repeated key fills a `Vec<T>` field: `?tag=a&tag=b` deserializes into
232/// `Vec<String>`, which is what `<select multiple>` and a repeated checkbox
233/// send.
234///
235/// A key repeated against a *scalar* field is **rejected**, not resolved.
236/// Browsers take the last occurrence and some servers take the first, so
237/// picking a winner silently means a proxy and an origin can disagree about
238/// what the request said. Declare the field as `Vec<T>` to accept repetition
239/// deliberately.
240///
241/// ```
242/// use churust_core::{Churust, Query, TestClient};
243/// use serde::Deserialize;
244/// # tokio::runtime::Runtime::new().unwrap().block_on(async {
245/// #[derive(Deserialize)]
246/// struct Pager { page: u32 }
247///
248/// let app = Churust::server()
249/// .routing(|r| {
250/// r.get("/list", |Query(p): Query<Pager>| async move {
251/// format!("page {}", p.page)
252/// });
253/// })
254/// .build();
255/// let res = TestClient::new(app).get("/list?page=3").send().await;
256/// assert_eq!(res.text(), "page 3");
257/// # });
258/// ```
259#[derive(Debug, Clone)]
260pub struct Query<T>(
261 /// The deserialized query value.
262 pub T,
263);
264
265#[async_trait]
266impl<T> FromCallParts for Query<T>
267where
268 T: DeserializeOwned + Send,
269{
270 async fn from_call_parts(call: &mut Call) -> Result<Self> {
271 let q = call.query_string();
272 let value = serde_html_form::from_str::<T>(q)
273 .map_err(|e| Error::bad_request(format!("invalid query string: {e}")))?;
274 Ok(Query(value))
275 }
276}
277
278/// Extracts a shared handle to application state of type `T`.
279///
280/// The state must have been registered with
281/// [`AppBuilder::state`](crate::AppBuilder::state); if no value of type `T` was
282/// registered, extraction fails with `500 Internal Server Error`. `State<T>`
283/// derefs to `T`, so the inner value can be used directly.
284///
285/// ```
286/// use churust_core::{Churust, State, TestClient};
287/// # tokio::runtime::Runtime::new().unwrap().block_on(async {
288/// #[derive(Clone)]
289/// struct Config { greeting: &'static str }
290///
291/// let app = Churust::server()
292/// .state(Config { greeting: "hi" })
293/// .routing(|r| {
294/// r.get("/", |cfg: State<Config>| async move { cfg.greeting });
295/// })
296/// .build();
297/// let res = TestClient::new(app).get("/").send().await;
298/// assert_eq!(res.text(), "hi");
299/// # });
300/// ```
301#[derive(Debug, Clone)]
302pub struct State<T>(
303 /// The shared handle to the registered state value.
304 pub Arc<T>,
305);
306
307#[async_trait]
308impl<T> FromCallParts for State<T>
309where
310 T: Send + Sync + 'static,
311{
312 async fn from_call_parts(call: &mut Call) -> Result<Self> {
313 match call.state::<T>() {
314 Some(v) => Ok(State(v)),
315 None => Err(Error::internal(format!(
316 "missing application state: {}",
317 std::any::type_name::<T>()
318 ))),
319 }
320 }
321}
322
323impl<T> std::ops::Deref for State<T> {
324 type Target = T;
325 fn deref(&self) -> &T {
326 &self.0
327 }
328}
329
330/// Extracts the token from an `Authorization: Bearer <token>` header.
331///
332/// The `Bearer` scheme prefix is matched case-insensitively and stripped; the
333/// remaining token is trimmed. Extraction fails with `401 Unauthorized` if the
334/// `Authorization` header is missing or does not use the `Bearer` scheme.
335///
336/// ```
337/// use churust_core::{Churust, BearerToken, TestClient};
338/// # tokio::runtime::Runtime::new().unwrap().block_on(async {
339/// let app = Churust::server()
340/// .routing(|r| {
341/// r.get("/me", |BearerToken(t): BearerToken| async move {
342/// format!("token={t}")
343/// });
344/// })
345/// .build();
346/// let res = TestClient::new(app)
347/// .get("/me")
348/// .header("authorization", "Bearer abc123")
349/// .send()
350/// .await;
351/// assert_eq!(res.text(), "token=abc123");
352/// # });
353/// ```
354#[derive(Debug, Clone)]
355pub struct BearerToken(
356 /// The extracted bearer token (without the `Bearer ` prefix).
357 pub String,
358);
359
360#[async_trait]
361impl FromCallParts for BearerToken {
362 async fn from_call_parts(call: &mut Call) -> Result<Self> {
363 let raw = call.header("authorization").ok_or_else(|| {
364 Error::new(
365 http::StatusCode::UNAUTHORIZED,
366 "missing Authorization header",
367 )
368 })?;
369 // RFC 7235 §2.1 makes the auth scheme case-insensitive. Matching the
370 // two literals `Bearer ` and `bearer ` rejected `BEARER `, which real
371 // clients do send, with a 401 for a perfectly valid credential.
372 let token = raw
373 .split_once(' ')
374 .filter(|(scheme, _)| scheme.eq_ignore_ascii_case("bearer"))
375 .map(|(_, token)| token)
376 .ok_or_else(|| Error::new(http::StatusCode::UNAUTHORIZED, "expected Bearer scheme"))?;
377 Ok(BearerToken(token.trim().to_string()))
378 }
379}
380
381/// Absent means no `Authorization` header. A header that is present but is not
382/// a Bearer credential stays a `401`: the client did attempt to authenticate,
383/// and treating a malformed scheme as "anonymous" is how an auth check gets
384/// skipped by accident.
385#[async_trait]
386impl OptionalFromCallParts for BearerToken {
387 async fn from_call_parts_opt(call: &mut Call) -> Result<Option<Self>> {
388 if call.header("authorization").is_none() {
389 return Ok(None);
390 }
391 BearerToken::from_call_parts(call).await.map(Some)
392 }
393}
394
395/// Extracts a single named header, parsed into `T`.
396///
397/// The name is supplied by a type implementing [`HeaderName`], so the header a
398/// handler reads is part of its signature rather than a string repeated at the
399/// call site.
400///
401/// ```
402/// use churust_core::{Churust, Header, HeaderName, TestClient};
403/// # tokio::runtime::Runtime::new().unwrap().block_on(async {
404/// struct ApiVersion;
405/// impl HeaderName for ApiVersion {
406/// const NAME: &'static str = "x-api-version";
407/// }
408///
409/// let app = Churust::server()
410/// .routing(|r| {
411/// r.get("/", |Header(v, _): Header<u32, ApiVersion>| async move {
412/// format!("v{v}")
413/// });
414/// })
415/// .build();
416///
417/// let res = TestClient::new(app).get("/").header("x-api-version", "2").send().await;
418/// assert_eq!(res.text(), "v2");
419/// # });
420/// ```
421///
422/// Fails with `400 Bad Request` when the header is absent or does not parse
423/// into `T`. For an optional header, use `Header<Option<T>, N>`, or read it
424/// directly with [`Call::header`](crate::Call::header).
425///
426/// # Why a marker type
427///
428/// The v1 design named `Header<T>` without saying which header it reads. A
429/// const string parameter is not expressible on stable Rust, deserializing the
430/// whole header map would silently accept junk, and a `headers`-crate typed
431/// trait would pull in a dependency for a small win. A marker type keeps the
432/// name in the type system, costs nothing at runtime, and needs no dependency.
433pub struct Header<T, N: HeaderName>(
434 /// The parsed header value.
435 pub T,
436 /// Zero-sized marker naming the header. Ignore it.
437 pub std::marker::PhantomData<N>,
438);
439
440/// Names the header that a [`Header`] extractor reads.
441pub trait HeaderName: Send + Sync + 'static {
442 /// The header name, lower-case.
443 const NAME: &'static str;
444}
445
446#[async_trait]
447impl<T, N> FromCallParts for Header<T, N>
448where
449 T: std::str::FromStr + Send,
450 T::Err: std::fmt::Display,
451 N: HeaderName,
452{
453 async fn from_call_parts(call: &mut Call) -> Result<Self> {
454 let raw = call
455 .header(N::NAME)
456 .ok_or_else(|| Error::bad_request(format!("missing header `{}`", N::NAME)))?;
457 raw.parse::<T>()
458 .map(|v| Header(v, std::marker::PhantomData))
459 .map_err(|e| Error::bad_request(format!("bad header `{}`: {e}", N::NAME)))
460 }
461}
462
463/// Absent means the header was not sent. A header that was sent but does not
464/// parse into `T` is an error, since the client did state a value.
465#[async_trait]
466impl<T, N> OptionalFromCallParts for Header<T, N>
467where
468 T: std::str::FromStr + Send,
469 T::Err: std::fmt::Display,
470 N: HeaderName,
471{
472 async fn from_call_parts_opt(call: &mut Call) -> Result<Option<Self>> {
473 if call.header(N::NAME).is_none() {
474 return Ok(None);
475 }
476 Header::<T, N>::from_call_parts(call).await.map(Some)
477 }
478}
479
480/// The raw request body as text.
481///
482/// Fails with `400 Bad Request` if the body is not valid UTF-8. For bytes that
483/// may not be text, extract [`Bytes`](bytes::Bytes) instead.
484#[async_trait]
485impl FromCall for String {
486 async fn from_call(mut call: Call) -> Result<Self> {
487 let b = call.try_receive_bytes().await?;
488 check_body_limit(&call, b.len())?;
489 String::from_utf8(b.to_vec())
490 .map_err(|_| Error::bad_request("request body is not valid UTF-8"))
491 }
492}
493
494/// The raw request body as bytes.
495#[async_trait]
496impl FromCall for bytes::Bytes {
497 async fn from_call(mut call: Call) -> Result<Self> {
498 let b = call.try_receive_bytes().await?;
499 check_body_limit(&call, b.len())?;
500 Ok(b)
501 }
502}
503
504/// Accept one of two extractors, whichever succeeds.
505///
506/// The usual case is an endpoint that takes either JSON or a form:
507///
508/// ```
509/// use churust_core::{Churust, Either, Form, TestClient};
510/// use serde::Deserialize;
511/// # tokio::runtime::Runtime::new().unwrap().block_on(async {
512/// #[derive(Deserialize)]
513/// struct Note { text: String }
514///
515/// let app = Churust::server()
516/// .routing(|r| {
517/// r.post("/notes", |body: Either<Form<Note>, String>| async move {
518/// match body {
519/// Either::Left(Form(n)) => format!("form: {}", n.text),
520/// Either::Right(raw) => format!("raw: {raw}"),
521/// }
522/// });
523/// })
524/// .build();
525///
526/// let res = TestClient::new(app)
527/// .post("/notes")
528/// .header("content-type", "application/x-www-form-urlencoded")
529/// .body("text=hi")
530/// .send()
531/// .await;
532/// assert_eq!(res.text(), "form: hi");
533/// # });
534/// ```
535///
536/// `Left` is tried first. If it fails, the call is handed to `Right` — which is
537/// why the call is cloned rather than moved: a failed first attempt must not
538/// have consumed the body. When both fail, the **second** error is reported,
539/// since the right-hand side is the fallback and its complaint is usually the
540/// more informative one.
541pub enum Either<L, R> {
542 /// The first extractor succeeded.
543 Left(L),
544 /// The first failed and the second succeeded.
545 Right(R),
546}
547
548#[async_trait]
549impl<L, R> FromCall for Either<L, R>
550where
551 L: FromCall,
552 R: FromCall,
553{
554 async fn from_call(call: Call) -> Result<Self> {
555 // The body can only be read once, so buffer it up front and give each
556 // attempt its own copy.
557 let mut call = call;
558 let body = call.try_receive_bytes().await?;
559
560 let left_call = call.clone_with_body(body.clone());
561 if let Ok(l) = L::from_call(left_call).await {
562 return Ok(Either::Left(l));
563 }
564 R::from_call(call.clone_with_body(body))
565 .await
566 .map(Either::Right)
567 }
568}
569
570/// The request body as a stream, without buffering it.
571///
572/// Consumes the body, so it must be the last handler argument. Use this for a
573/// large upload that should not be held in memory — the counterpart to
574/// [`Json<T>`](../churust_json/index.html) and [`Form<T>`], which buffer.
575///
576/// ```
577/// use churust_core::{Churust, Payload, TestClient};
578/// use futures_util::StreamExt;
579/// # tokio::runtime::Runtime::new().unwrap().block_on(async {
580/// let app = Churust::server()
581/// .routing(|r| {
582/// r.post("/count", |Payload(mut body): Payload| async move {
583/// let mut n = 0usize;
584/// while let Some(Ok(chunk)) = body.next().await {
585/// n += chunk.len();
586/// }
587/// format!("{n} bytes")
588/// });
589/// })
590/// .build();
591///
592/// let res = TestClient::new(app).post("/count").body("hello").send().await;
593/// assert_eq!(res.text(), "5 bytes");
594/// # });
595/// ```
596///
597/// Both the server-wide `max_body_bytes` and any per-route
598/// [`max_body_bytes`](crate::RouteBuilder::max_body_bytes) apply. Because the
599/// response may already have begun, exceeding either arrives as an error item
600/// in the stream rather than as a `413` — propagate it with `?` to turn it back
601/// into the right status.
602///
603/// The stream is bounded; **collecting it into memory is your allocation, not
604/// the framework's**. A handler that gathers the whole body allocates up to
605/// whichever limit is tighter, so tighten the route where a large ceiling was
606/// raised for some other route's benefit.
607pub struct Payload(
608 /// The body's chunks.
609 pub crate::call::BodyStream,
610);
611
612#[async_trait]
613impl FromCall for Payload {
614 async fn from_call(mut call: Call) -> Result<Self> {
615 let route_limit = call.get::<RouteBodyLimit>().map(|RouteBodyLimit(n)| n);
616 // An absent body is an empty stream rather than an error: a POST with
617 // no body is a legitimate request, not a malformed one.
618 let stream = call
619 .body_stream()
620 .unwrap_or_else(|| Box::pin(futures_util::stream::empty()));
621
622 let Some(max) = route_limit else {
623 // No route cap: the engine's server-wide `Limited` is the only
624 // bound, and it is already wrapped around this stream.
625 return Ok(Payload(stream));
626 };
627
628 // The engine enforces the server-wide cap before the stream is built,
629 // so this only ever *tightens*. Counting here rather than collecting
630 // keeps the streaming property: a body is refused at the byte that
631 // crosses the line, not after it has all been read.
632 use futures_util::StreamExt;
633 let mut seen = 0usize;
634 let counted = stream.map(move |chunk| match chunk {
635 Ok(bytes) => {
636 seen += bytes.len();
637 if seen > max {
638 Err(Error::new(
639 http::StatusCode::PAYLOAD_TOO_LARGE,
640 "request body too large",
641 ))
642 } else {
643 Ok(bytes)
644 }
645 }
646 Err(e) => Err(e),
647 });
648 Ok(Payload(Box::pin(counted)))
649 }
650}
651
652/// A per-route body cap, seeded into the call by
653/// [`RouteBuilder::max_body_bytes`](crate::RouteBuilder::max_body_bytes).
654#[derive(Debug, Clone, Copy)]
655pub struct RouteBodyLimit(pub usize);
656
657/// Reject a body larger than the route's cap, if it set one.
658///
659/// Body extractors call this after reading. Public so plugin crates such as
660/// `churust-json` can honour the same per-route configuration.
661pub fn check_body_limit(call: &Call, len: usize) -> Result<()> {
662 match call.get::<RouteBodyLimit>() {
663 Some(RouteBodyLimit(max)) if len > max => Err(Error::new(
664 http::StatusCode::PAYLOAD_TOO_LARGE,
665 "request body too large",
666 )),
667 _ => Ok(()),
668 }
669}
670
671/// Deserializes an `application/x-www-form-urlencoded` request body into `T`.
672///
673/// The body counterpart to [`Query<T>`], and the classic HTML form POST. Like
674/// [`Json<T>`](../churust_json/index.html) it consumes the body, so it must be
675/// the last handler argument.
676///
677/// Fails with `415 Unsupported Media Type` when the content type is not
678/// `application/x-www-form-urlencoded`, and `400 Bad Request` when the body
679/// does not deserialize into `T`.
680///
681/// Note `+` means a space here — this is form encoding. In a *path* segment `+`
682/// is a literal plus, which is why path decoding uses its own decoder.
683///
684/// ```
685/// use churust_core::{Churust, Form, TestClient};
686/// use serde::Deserialize;
687/// # tokio::runtime::Runtime::new().unwrap().block_on(async {
688/// #[derive(Deserialize)]
689/// struct Login { user: String }
690///
691/// let app = Churust::server()
692/// .routing(|r| {
693/// r.post("/login", |Form(l): Form<Login>| async move { l.user });
694/// })
695/// .build();
696///
697/// let res = TestClient::new(app)
698/// .post("/login")
699/// .header("content-type", "application/x-www-form-urlencoded")
700/// .body("user=ana")
701/// .send()
702/// .await;
703/// assert_eq!(res.text(), "ana");
704/// # });
705/// ```
706pub struct Form<T>(
707 /// The deserialized form body.
708 pub T,
709);
710
711#[async_trait]
712impl<T> FromCall for Form<T>
713where
714 T: DeserializeOwned + Send,
715{
716 async fn from_call(mut call: Call) -> Result<Self> {
717 let ct = call
718 .header(http::header::CONTENT_TYPE.as_str())
719 .unwrap_or("")
720 .to_string();
721 // Compare only the media type: a charset parameter is legitimate.
722 let media = ct.split(';').next().unwrap_or("").trim();
723 if !media.eq_ignore_ascii_case("application/x-www-form-urlencoded") {
724 return Err(Error::new(
725 http::StatusCode::UNSUPPORTED_MEDIA_TYPE,
726 "expected application/x-www-form-urlencoded",
727 ));
728 }
729
730 let body = call.try_receive_bytes().await?;
731 check_body_limit(&call, body.len())?;
732 serde_html_form::from_bytes::<T>(&body)
733 .map(Form)
734 .map_err(|e| Error::bad_request(format!("invalid form body: {e}")))
735 }
736}
737
738#[cfg(test)]
739mod tests {
740 use super::*;
741 use bytes::Bytes;
742 use http::{HeaderMap, Method, Uri};
743
744 // A trivial parts extractor used to prove the machinery.
745 struct MethodName(String);
746
747 #[async_trait]
748 impl FromCallParts for MethodName {
749 async fn from_call_parts(call: &mut Call) -> Result<Self> {
750 Ok(MethodName(call.method().as_str().to_string()))
751 }
752 }
753
754 fn call() -> Call {
755 Call::new(
756 Method::GET,
757 "/".parse::<Uri>().unwrap(),
758 HeaderMap::new(),
759 Bytes::new(),
760 )
761 }
762
763 #[tokio::test]
764 async fn parts_extractor_runs() {
765 let mut c = call();
766 let m = MethodName::from_call_parts(&mut c).await.unwrap();
767 assert_eq!(m.0, "GET");
768 }
769
770 #[tokio::test]
771 async fn call_is_from_call() {
772 let c = call();
773 let back = Call::from_call(c).await.unwrap();
774 assert_eq!(back.method(), &Method::GET);
775 }
776
777 #[tokio::test]
778 async fn path_extracts_single_param() {
779 let mut c = call();
780 let mut p = crate::call::Params::new();
781 p.insert("id".to_string(), "42".to_string());
782 c.set_params(p);
783 let Path(id) = Path::<u64>::from_call_parts(&mut c).await.unwrap();
784 assert_eq!(id, 42);
785 }
786
787 #[tokio::test]
788 async fn path_bad_value_is_400() {
789 let mut c = call();
790 let mut p = crate::call::Params::new();
791 p.insert("id".to_string(), "notnum".to_string());
792 c.set_params(p);
793 let err = Path::<u64>::from_call_parts(&mut c).await.unwrap_err();
794 assert_eq!(err.status(), http::StatusCode::BAD_REQUEST);
795 }
796
797 use serde::Deserialize;
798
799 #[derive(Deserialize, Debug, PartialEq)]
800 struct Pager {
801 page: u32,
802 q: String,
803 }
804
805 fn call_with_query(qs: &str) -> Call {
806 Call::new(
807 Method::GET,
808 format!("/s?{qs}").parse::<Uri>().unwrap(),
809 HeaderMap::new(),
810 Bytes::new(),
811 )
812 }
813
814 #[tokio::test]
815 async fn query_deserializes() {
816 let mut c = call_with_query("page=2&q=rust");
817 let Query(p) = Query::<Pager>::from_call_parts(&mut c).await.unwrap();
818 assert_eq!(
819 p,
820 Pager {
821 page: 2,
822 q: "rust".into()
823 }
824 );
825 }
826
827 #[tokio::test]
828 async fn query_missing_field_is_400() {
829 let mut c = call_with_query("q=rust");
830 let err = Query::<Pager>::from_call_parts(&mut c).await.unwrap_err();
831 assert_eq!(err.status(), http::StatusCode::BAD_REQUEST);
832 }
833
834 fn call_with_auth(value: &str) -> Call {
835 let mut headers = HeaderMap::new();
836 headers.insert(
837 http::header::AUTHORIZATION,
838 http::HeaderValue::from_str(value).unwrap(),
839 );
840 Call::new(
841 Method::GET,
842 "/".parse::<Uri>().unwrap(),
843 headers,
844 Bytes::new(),
845 )
846 }
847
848 #[tokio::test]
849 async fn bearer_token_extracted() {
850 let mut c = call_with_auth("Bearer abc123");
851 let BearerToken(t) = BearerToken::from_call_parts(&mut c).await.unwrap();
852 assert_eq!(t, "abc123");
853 }
854
855 #[tokio::test]
856 async fn missing_bearer_is_401() {
857 let mut c = call();
858 let err = BearerToken::from_call_parts(&mut c).await.unwrap_err();
859 assert_eq!(err.status(), http::StatusCode::UNAUTHORIZED);
860 }
861}