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}