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#[async_trait]
81impl<T: FromCallParts> FromCall for T {
82    async fn from_call(mut call: Call) -> Result<Self> {
83        T::from_call_parts(&mut call).await
84    }
85}
86
87/// The whole `Call` as the final argument (Ktor call-style base case).
88/// NOTE: deliberately NOT `FromCallParts` — that would conflict with the
89/// blanket impl above.
90#[async_trait]
91impl FromCall for Call {
92    async fn from_call(call: Call) -> Result<Self> {
93        Ok(call)
94    }
95}
96
97/// Extracts a single path parameter, parsed into `T`.
98///
99/// For a route such as `"/users/{id}"`, `Path::<u64>` reads the first captured
100/// parameter (`{id}`). It reads positionally, so it is intended for routes with
101/// exactly one path parameter; for routes with several, read each by name with
102/// [`Call::param`](crate::Call::param). Extraction fails with `400 Bad Request`
103/// if there is no parameter or the value does not parse into `T`.
104///
105/// ```
106/// use churust_core::{Churust, Path, TestClient};
107/// # tokio::runtime::Runtime::new().unwrap().block_on(async {
108/// let app = Churust::server()
109///     .routing(|r| {
110///         r.get("/double/{n}", |Path(n): Path<i64>| async move {
111///             format!("{}", n * 2)
112///         });
113///     })
114///     .build();
115/// let res = TestClient::new(app).get("/double/21").send().await;
116/// assert_eq!(res.text(), "42");
117/// # });
118/// ```
119#[derive(Debug, Clone)]
120pub struct Path<T>(
121    /// The parsed path parameter value.
122    pub T,
123);
124
125#[async_trait]
126impl<T> FromCallParts for Path<T>
127where
128    T: std::str::FromStr + Send,
129    T::Err: std::fmt::Display,
130{
131    async fn from_call_parts(call: &mut Call) -> Result<Self> {
132        let mut params = call.params_iter();
133        let (_name, raw) = params
134            .next()
135            .ok_or_else(|| Error::bad_request("no path parameter to extract"))?;
136        let value = raw
137            .parse::<T>()
138            .map_err(|e| Error::bad_request(format!("bad path param: {e}")))?;
139        Ok(Path(value))
140    }
141}
142
143/// Deserializes the URL query string into `T` via `serde_urlencoded`.
144///
145/// `T` must implement [`serde::Deserialize`]. Missing required fields or
146/// otherwise malformed query strings fail with `400 Bad Request`.
147///
148/// ```
149/// use churust_core::{Churust, Query, TestClient};
150/// use serde::Deserialize;
151/// # tokio::runtime::Runtime::new().unwrap().block_on(async {
152/// #[derive(Deserialize)]
153/// struct Pager { page: u32 }
154///
155/// let app = Churust::server()
156///     .routing(|r| {
157///         r.get("/list", |Query(p): Query<Pager>| async move {
158///             format!("page {}", p.page)
159///         });
160///     })
161///     .build();
162/// let res = TestClient::new(app).get("/list?page=3").send().await;
163/// assert_eq!(res.text(), "page 3");
164/// # });
165/// ```
166#[derive(Debug, Clone)]
167pub struct Query<T>(
168    /// The deserialized query value.
169    pub T,
170);
171
172#[async_trait]
173impl<T> FromCallParts for Query<T>
174where
175    T: DeserializeOwned + Send,
176{
177    async fn from_call_parts(call: &mut Call) -> Result<Self> {
178        let q = call.query_string();
179        let value = serde_urlencoded::from_str::<T>(q)
180            .map_err(|e| Error::bad_request(format!("invalid query string: {e}")))?;
181        Ok(Query(value))
182    }
183}
184
185/// Extracts a shared handle to application state of type `T`.
186///
187/// The state must have been registered with
188/// [`AppBuilder::state`](crate::AppBuilder::state); if no value of type `T` was
189/// registered, extraction fails with `500 Internal Server Error`. `State<T>`
190/// derefs to `T`, so the inner value can be used directly.
191///
192/// ```
193/// use churust_core::{Churust, State, TestClient};
194/// # tokio::runtime::Runtime::new().unwrap().block_on(async {
195/// #[derive(Clone)]
196/// struct Config { greeting: &'static str }
197///
198/// let app = Churust::server()
199///     .state(Config { greeting: "hi" })
200///     .routing(|r| {
201///         r.get("/", |cfg: State<Config>| async move { cfg.greeting });
202///     })
203///     .build();
204/// let res = TestClient::new(app).get("/").send().await;
205/// assert_eq!(res.text(), "hi");
206/// # });
207/// ```
208#[derive(Debug, Clone)]
209pub struct State<T>(
210    /// The shared handle to the registered state value.
211    pub Arc<T>,
212);
213
214#[async_trait]
215impl<T> FromCallParts for State<T>
216where
217    T: Send + Sync + 'static,
218{
219    async fn from_call_parts(call: &mut Call) -> Result<Self> {
220        match call.state::<T>() {
221            Some(v) => Ok(State(v)),
222            None => Err(Error::internal(format!(
223                "missing application state: {}",
224                std::any::type_name::<T>()
225            ))),
226        }
227    }
228}
229
230impl<T> std::ops::Deref for State<T> {
231    type Target = T;
232    fn deref(&self) -> &T {
233        &self.0
234    }
235}
236
237/// Extracts the token from an `Authorization: Bearer <token>` header.
238///
239/// The `Bearer` scheme prefix is matched case-insensitively and stripped; the
240/// remaining token is trimmed. Extraction fails with `401 Unauthorized` if the
241/// `Authorization` header is missing or does not use the `Bearer` scheme.
242///
243/// ```
244/// use churust_core::{Churust, BearerToken, TestClient};
245/// # tokio::runtime::Runtime::new().unwrap().block_on(async {
246/// let app = Churust::server()
247///     .routing(|r| {
248///         r.get("/me", |BearerToken(t): BearerToken| async move {
249///             format!("token={t}")
250///         });
251///     })
252///     .build();
253/// let res = TestClient::new(app)
254///     .get("/me")
255///     .header("authorization", "Bearer abc123")
256///     .send()
257///     .await;
258/// assert_eq!(res.text(), "token=abc123");
259/// # });
260/// ```
261#[derive(Debug, Clone)]
262pub struct BearerToken(
263    /// The extracted bearer token (without the `Bearer ` prefix).
264    pub String,
265);
266
267#[async_trait]
268impl FromCallParts for BearerToken {
269    async fn from_call_parts(call: &mut Call) -> Result<Self> {
270        let raw = call.header("authorization").ok_or_else(|| {
271            Error::new(
272                http::StatusCode::UNAUTHORIZED,
273                "missing Authorization header",
274            )
275        })?;
276        let token = raw
277            .strip_prefix("Bearer ")
278            .or_else(|| raw.strip_prefix("bearer "))
279            .ok_or_else(|| Error::new(http::StatusCode::UNAUTHORIZED, "expected Bearer scheme"))?;
280        Ok(BearerToken(token.trim().to_string()))
281    }
282}
283
284#[cfg(test)]
285mod tests {
286    use super::*;
287    use bytes::Bytes;
288    use http::{HeaderMap, Method, Uri};
289
290    // A trivial parts extractor used to prove the machinery.
291    struct MethodName(String);
292
293    #[async_trait]
294    impl FromCallParts for MethodName {
295        async fn from_call_parts(call: &mut Call) -> Result<Self> {
296            Ok(MethodName(call.method().as_str().to_string()))
297        }
298    }
299
300    fn call() -> Call {
301        Call::new(
302            Method::GET,
303            "/".parse::<Uri>().unwrap(),
304            HeaderMap::new(),
305            Bytes::new(),
306        )
307    }
308
309    #[tokio::test]
310    async fn parts_extractor_runs() {
311        let mut c = call();
312        let m = MethodName::from_call_parts(&mut c).await.unwrap();
313        assert_eq!(m.0, "GET");
314    }
315
316    #[tokio::test]
317    async fn call_is_from_call() {
318        let c = call();
319        let back = Call::from_call(c).await.unwrap();
320        assert_eq!(back.method(), &Method::GET);
321    }
322
323    use std::collections::HashMap;
324
325    #[tokio::test]
326    async fn path_extracts_single_param() {
327        let mut c = call();
328        let mut p = HashMap::new();
329        p.insert("id".to_string(), "42".to_string());
330        c.set_params(p);
331        let Path(id) = Path::<u64>::from_call_parts(&mut c).await.unwrap();
332        assert_eq!(id, 42);
333    }
334
335    #[tokio::test]
336    async fn path_bad_value_is_400() {
337        let mut c = call();
338        let mut p = HashMap::new();
339        p.insert("id".to_string(), "notnum".to_string());
340        c.set_params(p);
341        let err = Path::<u64>::from_call_parts(&mut c).await.unwrap_err();
342        assert_eq!(err.status(), http::StatusCode::BAD_REQUEST);
343    }
344
345    use serde::Deserialize;
346
347    #[derive(Deserialize, Debug, PartialEq)]
348    struct Pager {
349        page: u32,
350        q: String,
351    }
352
353    fn call_with_query(qs: &str) -> Call {
354        Call::new(
355            Method::GET,
356            format!("/s?{qs}").parse::<Uri>().unwrap(),
357            HeaderMap::new(),
358            Bytes::new(),
359        )
360    }
361
362    #[tokio::test]
363    async fn query_deserializes() {
364        let mut c = call_with_query("page=2&q=rust");
365        let Query(p) = Query::<Pager>::from_call_parts(&mut c).await.unwrap();
366        assert_eq!(
367            p,
368            Pager {
369                page: 2,
370                q: "rust".into()
371            }
372        );
373    }
374
375    #[tokio::test]
376    async fn query_missing_field_is_400() {
377        let mut c = call_with_query("q=rust");
378        let err = Query::<Pager>::from_call_parts(&mut c).await.unwrap_err();
379        assert_eq!(err.status(), http::StatusCode::BAD_REQUEST);
380    }
381
382    fn call_with_auth(value: &str) -> Call {
383        let mut headers = HeaderMap::new();
384        headers.insert(
385            http::header::AUTHORIZATION,
386            http::HeaderValue::from_str(value).unwrap(),
387        );
388        Call::new(
389            Method::GET,
390            "/".parse::<Uri>().unwrap(),
391            headers,
392            Bytes::new(),
393        )
394    }
395
396    #[tokio::test]
397    async fn bearer_token_extracted() {
398        let mut c = call_with_auth("Bearer abc123");
399        let BearerToken(t) = BearerToken::from_call_parts(&mut c).await.unwrap();
400        assert_eq!(t, "abc123");
401    }
402
403    #[tokio::test]
404    async fn missing_bearer_is_401() {
405        let mut c = call();
406        let err = BearerToken::from_call_parts(&mut c).await.unwrap_err();
407        assert_eq!(err.status(), http::StatusCode::UNAUTHORIZED);
408    }
409}