Skip to main content

rainier_http/
extract.rs

1//! [`FromRequest`] — typed extractors for controller actions.
2//!
3//! A controller signature says what it needs and the framework supplies
4//! it, with the types doing the describing:
5//!
6//! ```ignore
7//! async fn store(Json(post): Json<NewPost>, Query(page): Query<Pagination>) -> Response
8//! ```
9//!
10//! Every extractor takes `Arc<Request>` and returns a `'static` future. Sharing
11//! one `Arc` is what lets several extractors read the same request
12//! concurrently-shaped without cloning it or fighting over a borrow, and the
13//! `'static` future is what lets the router store handlers of many different
14//! shapes in one table.
15//!
16//! Extraction is `async` because some extractors genuinely need to be: route
17//! **model binding** loads a record from the database, and an authenticated-user
18//! extractor asks the guard. Those live in `rainier-database` and
19//! `rainier-auth` respectively; the ones here need no I/O but share the shape.
20
21use std::sync::Arc;
22
23use rainier_support::{BoxedFuture, Error, Result};
24use serde::de::DeserializeOwned;
25
26use crate::coerce;
27use crate::request::Request;
28use crate::upload::UploadedFile;
29
30/// Something a handler parameter can be built from.
31pub trait FromRequest: Sized + Send + 'static {
32    /// Build `Self` from the request, or fail with the error the client should
33    /// see (a 400 for a malformed body, a 422 for a failed contract).
34    fn from_request(request: Arc<Request>) -> BoxedFuture<Result<Self>>;
35}
36
37/// The whole request.
38impl FromRequest for Arc<Request> {
39    fn from_request(request: Arc<Request>) -> BoxedFuture<Result<Self>> {
40        Box::pin(async move { Ok(request) })
41    }
42}
43
44/// An extractor that never fails: `Option<T>` is `None` when `T` would have
45/// errored. For inputs that are genuinely optional.
46impl<T: FromRequest> FromRequest for Option<T> {
47    fn from_request(request: Arc<Request>) -> BoxedFuture<Result<Self>> {
48        Box::pin(async move { Ok(T::from_request(request).await.ok()) })
49    }
50}
51
52/// Hands the extraction error to the handler instead of short-circuiting, for
53/// actions that want to answer a bad request themselves.
54impl<T: FromRequest> FromRequest for Result<T> {
55    fn from_request(request: Arc<Request>) -> BoxedFuture<Result<Self>> {
56        Box::pin(async move { Ok(T::from_request(request).await) })
57    }
58}
59
60/// The JSON request body, deserialised.
61///
62/// ```
63/// # use rainier_http::{extract::Json, Request, FromRequest};
64/// # use std::sync::Arc;
65/// #[derive(serde::Deserialize)]
66/// struct NewPost { title: String }
67///
68/// # #[tokio::main] async fn main() {
69/// let request = Arc::new(
70///     Request::builder().json(&serde_json::json!({ "title": "Hello" })).build(),
71/// );
72/// let Json(post) = Json::<NewPost>::from_request(request).await.unwrap();
73/// assert_eq!(post.title, "Hello");
74/// # }
75/// ```
76#[derive(Debug, Clone, Copy, Default)]
77pub struct Json<T>(pub T);
78
79impl<T: DeserializeOwned + Send + 'static> FromRequest for Json<T> {
80    fn from_request(request: Arc<Request>) -> BoxedFuture<Result<Self>> {
81        Box::pin(async move { request.json::<T>().map(Json) })
82    }
83}
84
85/// The merged request input (query string plus body), deserialised with string
86/// coercion — so a urlencoded `page=2` fills a `u32` field.
87#[derive(Debug, Clone, Copy, Default)]
88pub struct Form<T>(pub T);
89
90impl<T: DeserializeOwned + Send + 'static> FromRequest for Form<T> {
91    fn from_request(request: Arc<Request>) -> BoxedFuture<Result<Self>> {
92        Box::pin(async move {
93            coerce::from_value(&request.all())
94                .map(Form)
95                .map_err(|e| Error::bad_request(format!("invalid form input: {e}")))
96        })
97    }
98}
99
100/// The query string, deserialised with string coercion.
101#[derive(Debug, Clone, Copy, Default)]
102pub struct Query<T>(pub T);
103
104impl<T: DeserializeOwned + Send + 'static> FromRequest for Query<T> {
105    fn from_request(request: Arc<Request>) -> BoxedFuture<Result<Self>> {
106        Box::pin(async move {
107            coerce::from_value(request.query())
108                .map(Query)
109                .map_err(|e| Error::bad_request(format!("invalid query string: {e}")))
110        })
111    }
112}
113
114/// The route parameters, deserialised with string coercion.
115///
116/// A struct reads them by name; a bare scalar reads the **only** parameter, so
117/// `Path<u64>` works for `/posts/{post}` without a wrapper struct.
118///
119/// ```
120/// # use rainier_http::{extract::Path, Request, FromRequest};
121/// # use std::sync::Arc;
122/// # #[tokio::main] async fn main() {
123/// let request = Arc::new(Request::builder().route_param("post", "42").build());
124/// let Path(id) = Path::<u64>::from_request(request).await.unwrap();
125/// assert_eq!(id, 42);
126/// # }
127/// ```
128#[derive(Debug, Clone, Copy, Default)]
129pub struct Path<T>(pub T);
130
131impl<T: DeserializeOwned + Send + 'static> FromRequest for Path<T> {
132    fn from_request(request: Arc<Request>) -> BoxedFuture<Result<Self>> {
133        Box::pin(async move {
134            let params = request.route_params();
135            let as_object = coerce::object_from_strings(params.iter());
136
137            // Try the whole map first — that is what a struct or a map wants.
138            if let Ok(value) = coerce::from_value::<T>(&as_object) {
139                return Ok(Path(value));
140            }
141
142            // Otherwise a bare scalar: only unambiguous with exactly one
143            // parameter. With two, refusing beats silently picking one.
144            if params.len() == 1 {
145                let only = params.values().next().expect("just checked len == 1");
146                let scalar = serde_json::Value::String(only.clone());
147                if let Ok(value) = coerce::from_value::<T>(&scalar) {
148                    return Ok(Path(value));
149                }
150            }
151
152            Err(Error::bad_request(format!(
153                "could not read the route parameters {:?} as the expected type",
154                params.keys().collect::<Vec<_>>()
155            )))
156        })
157    }
158}
159
160/// A compile-time name for an extractor that needs one — a header, a form
161/// field.
162///
163/// Rust has no stable `const N: &'static str` generic parameter, so the name
164/// travels as a marker type instead of as a literal. [`static_name!`](crate::static_name) writes
165/// the marker for you.
166pub trait StaticName: Send + Sync + 'static {
167    /// The name.
168    const NAME: &'static str;
169}
170
171/// Declare a [`StaticName`] marker type.
172///
173/// ```
174/// # use rainier_http::static_name;
175/// static_name!(XRequestId, "x-request-id");
176/// ```
177#[macro_export]
178macro_rules! static_name {
179    ($(#[$meta:meta])* $vis:vis $name:ident, $value:expr) => {
180        $(#[$meta])*
181        #[doc = concat!("A [`StaticName`](", stringify!($crate), "::extract::StaticName) for `", $value, "`.")]
182        #[derive(Debug, Clone, Copy, Default)]
183        $vis struct $name;
184
185        impl $crate::extract::StaticName for $name {
186            const NAME: &'static str = $value;
187        }
188    };
189    ($(#[$meta:meta])* $name:ident, $value:expr) => {
190        $crate::static_name!($(#[$meta])* pub $name, $value);
191    };
192}
193
194/// A named header's value, required.
195///
196/// ```
197/// # use rainier_http::{extract::Header, static_name, FromRequest, Request};
198/// # use std::sync::Arc;
199/// static_name!(XRequestId, "x-request-id");
200///
201/// # #[tokio::main] async fn main() {
202/// let request = Arc::new(Request::builder().header("x-request-id", "abc").build());
203/// let Header(id, ..) = Header::<XRequestId>::from_request(request).await.unwrap();
204/// assert_eq!(id, "abc");
205/// # }
206/// ```
207#[derive(Debug, Clone)]
208pub struct Header<K: StaticName>(pub String, pub std::marker::PhantomData<K>);
209
210impl<K: StaticName> FromRequest for Header<K> {
211    fn from_request(request: Arc<Request>) -> BoxedFuture<Result<Self>> {
212        Box::pin(async move {
213            request
214                .header(K::NAME)
215                .map(|value| Header(value.to_string(), std::marker::PhantomData))
216                .ok_or_else(|| Error::bad_request(format!("the `{}` header is required", K::NAME)))
217        })
218    }
219}
220
221/// The `Authorization: Bearer …` token.
222#[derive(Debug, Clone)]
223pub struct Bearer(pub String);
224
225impl FromRequest for Bearer {
226    fn from_request(request: Arc<Request>) -> BoxedFuture<Result<Self>> {
227        Box::pin(async move {
228            request
229                .bearer_token()
230                .map(|token| Bearer(token.to_string()))
231                .ok_or_else(|| Error::unauthenticated("a bearer token is required"))
232        })
233    }
234}
235
236/// The raw request body.
237#[derive(Debug, Clone)]
238pub struct RawBody(pub bytes::Bytes);
239
240impl FromRequest for RawBody {
241    fn from_request(request: Arc<Request>) -> BoxedFuture<Result<Self>> {
242        Box::pin(async move { Ok(RawBody(request.body().clone())) })
243    }
244}
245
246/// Every file uploaded under a named field. Empty when none arrived.
247#[derive(Debug, Clone)]
248pub struct Files<K: StaticName>(pub Vec<UploadedFile>, pub std::marker::PhantomData<K>);
249
250impl<K: StaticName> FromRequest for Files<K> {
251    fn from_request(request: Arc<Request>) -> BoxedFuture<Result<Self>> {
252        Box::pin(
253            async move { Ok(Files(request.files(K::NAME).to_vec(), std::marker::PhantomData)) },
254        )
255    }
256}
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261    use http::Method;
262    use serde::Deserialize;
263    use serde_json::json;
264
265    #[derive(Debug, PartialEq, Deserialize)]
266    struct NewPost {
267        title: String,
268        draft: bool,
269    }
270
271    #[derive(Debug, PartialEq, Deserialize)]
272    struct Page {
273        page: u32,
274    }
275
276    fn shared(request: Request) -> Arc<Request> {
277        Arc::new(request)
278    }
279
280    #[tokio::test]
281    async fn json_extracts_the_body() {
282        let request =
283            shared(Request::builder().json(&json!({ "title": "Hi", "draft": true })).build());
284        let Json(post) = Json::<NewPost>::from_request(request).await.unwrap();
285        assert_eq!(post, NewPost { title: "Hi".into(), draft: true });
286    }
287
288    #[tokio::test]
289    async fn json_reports_a_bad_body_as_a_400() {
290        let request = shared(Request::builder().json(&json!({ "title": "Hi" })).build());
291        let err = Json::<NewPost>::from_request(request).await.unwrap_err();
292        assert_eq!(err.status(), 400);
293    }
294
295    #[tokio::test]
296    async fn query_coerces_strings() {
297        let request = shared(Request::builder().uri("/x?page=3").build());
298        let Query(page) = Query::<Page>::from_request(request).await.unwrap();
299        assert_eq!(page, Page { page: 3 });
300    }
301
302    #[tokio::test]
303    async fn form_reads_the_merged_input() {
304        let request = shared(
305            Request::builder()
306                .method(Method::POST)
307                .uri("/x?page=1")
308                .form(&[("title", "Hi"), ("draft", "1")])
309                .build(),
310        );
311        let Form(post) = Form::<NewPost>::from_request(request).await.unwrap();
312        assert_eq!(post, NewPost { title: "Hi".into(), draft: true });
313    }
314
315    #[tokio::test]
316    async fn path_reads_a_single_scalar_parameter() {
317        let request = shared(Request::builder().route_param("post", "42").build());
318        let Path(id) = Path::<u64>::from_request(request).await.unwrap();
319        assert_eq!(id, 42);
320    }
321
322    #[tokio::test]
323    async fn path_reads_a_struct_of_parameters() {
324        #[derive(Debug, PartialEq, Deserialize)]
325        struct Params {
326            post: u64,
327            comment: u64,
328        }
329
330        let request =
331            shared(Request::builder().route_param("post", "1").route_param("comment", "2").build());
332        let Path(params) = Path::<Params>::from_request(request).await.unwrap();
333        assert_eq!(params, Params { post: 1, comment: 2 });
334    }
335
336    #[tokio::test]
337    async fn path_refuses_to_guess_between_two_parameters() {
338        let request =
339            shared(Request::builder().route_param("post", "1").route_param("comment", "2").build());
340        let err = Path::<u64>::from_request(request).await.unwrap_err();
341        assert_eq!(err.status(), 400);
342    }
343
344    crate::static_name!(XRequestId, "x-request-id");
345    crate::static_name!(XMissing, "x-missing");
346    crate::static_name!(DocField, "doc");
347
348    #[tokio::test]
349    async fn header_and_bearer_extractors() {
350        let request = shared(
351            Request::builder()
352                .header("x-request-id", "abc")
353                .header("authorization", "Bearer tok")
354                .build(),
355        );
356
357        let Header(id, ..) =
358            Header::<XRequestId>::from_request(Arc::clone(&request)).await.unwrap();
359        assert_eq!(id, "abc");
360
361        let Bearer(token) = Bearer::from_request(Arc::clone(&request)).await.unwrap();
362        assert_eq!(token, "tok");
363
364        let missing = Header::<XMissing>::from_request(request).await.unwrap_err();
365        assert_eq!(missing.status(), 400);
366    }
367
368    #[tokio::test]
369    async fn a_missing_bearer_token_is_a_401() {
370        let request = shared(Request::builder().build());
371        assert_eq!(Bearer::from_request(request).await.unwrap_err().status(), 401);
372    }
373
374    #[tokio::test]
375    async fn option_swallows_the_failure() {
376        let request = shared(Request::builder().build());
377        let extracted = Option::<Json<NewPost>>::from_request(request).await.unwrap();
378        assert!(extracted.is_none());
379    }
380
381    #[tokio::test]
382    async fn result_hands_the_failure_to_the_handler() {
383        let request = shared(Request::builder().build());
384        let extracted = Result::<Json<NewPost>>::from_request(request).await.unwrap();
385        assert!(extracted.is_err());
386    }
387
388    #[tokio::test]
389    async fn the_whole_request_is_an_extractor() {
390        let request = shared(Request::builder().uri("/here").build());
391        let extracted = Arc::<Request>::from_request(request).await.unwrap();
392        assert_eq!(extracted.path(), "/here");
393    }
394
395    #[tokio::test]
396    async fn raw_body_and_files() {
397        let boundary = "B";
398        let body = format!(
399            "--{boundary}\r\nContent-Disposition: form-data; name=\"doc\"; filename=\"a.txt\"\r\n\r\nDATA\r\n--{boundary}--\r\n"
400        );
401        let request = shared(
402            Request::builder()
403                .method(Method::POST)
404                .header("content-type", &format!("multipart/form-data; boundary={boundary}"))
405                .body(body.clone())
406                .build(),
407        );
408
409        let RawBody(raw) = RawBody::from_request(Arc::clone(&request)).await.unwrap();
410        assert_eq!(raw.len(), body.len());
411
412        let Files(files, ..) = Files::<DocField>::from_request(request).await.unwrap();
413        assert_eq!(files.len(), 1);
414        assert_eq!(files[0].bytes().as_ref(), b"DATA");
415    }
416}