Skip to main content

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}