rama_http/service/web/endpoint/extract/mod.rs
1//! Extract utilities to develop endpoint services effortlessly.
2//!
3//! [`HeaderMap`] implements [`FromOwnedRequestParts`], allowing handlers to take ownership of all
4//! request headers without cloning them. Owned-parts extractors are placed immediately before a
5//! trailing [`FromRequestBody`] extractor.
6
7use super::IntoResponse;
8use crate::{HeaderMap, header, mime, request::Parts};
9
10pub mod host;
11#[doc(inline)]
12pub use host::Host;
13
14pub mod authority;
15#[doc(inline)]
16pub use authority::Authority;
17
18pub mod path;
19#[doc(inline)]
20pub use path::Path;
21
22pub mod query;
23#[doc(inline)]
24pub use query::Query;
25
26mod uri;
27
28mod header_map;
29mod method;
30mod request;
31
32mod state;
33pub use state::State;
34
35mod extensions;
36
37pub mod typed_header;
38#[doc(inline)]
39pub use typed_header::TypedHeader;
40
41pub mod body;
42#[doc(inline)]
43pub use body::{Body, Bytes, Csv, Form, Json, OctetStream, Text};
44
45#[cfg(feature = "multipart")]
46#[doc(inline)]
47pub use body::multipart;
48#[cfg(feature = "multipart")]
49#[doc(inline)]
50pub use body::multipart::Multipart;
51
52pub mod datastar;
53
54mod option;
55#[doc(inline)]
56pub use option::{OptionalFromPartsStateRefPair, OptionalFromRequest, OptionalFromRequestBody};
57
58/// Types that can be created from request parts.
59///
60/// Extractors that implement [`FromPartsStateRefPair`] cannot consume the request body and can thus
61/// be run in any order for handlers.
62///
63/// If your extractor needs to consume the request body then you should implement [`FromRequest`]
64/// and not [`FromPartsStateRefPair`].
65#[diagnostic::on_unimplemented(
66 note = "Function argument is not a valid web endpoint extractor. \nSee `https://ramaproxy.org/docs/rama/http/service/web/extract/index.html` for details"
67)]
68pub trait FromPartsStateRefPair<State>: Sized + Send + Sync + 'static {
69 /// If the extractor fails it'll use this "rejection" type. A rejection is
70 /// a kind of error that can be converted into a response.
71 type Rejection;
72
73 /// Perform the extraction.
74 fn from_parts_state_ref_pair(
75 parts: &Parts,
76 state: &State,
77 ) -> impl Future<Output = Result<Self, Self::Rejection>> + Send;
78}
79
80/// Types that can be created by consuming the request parts.
81///
82/// Exactly one owned-parts extractor can be used, either as the final handler argument or
83/// immediately before a trailing [`FromRequestBody`] extractor. Implementors automatically
84/// implement [`FromRequest`] for the terminal case. Unlike [`FromPartsStateRefPair`], this trait
85/// can move fields out of [`Parts`] without cloning them. When followed by a body extractor, the
86/// body extractor first prepares an owned future from borrowed parts, the owned-parts extractor
87/// runs, and only then is the body future awaited. This preserves argument-order rejection
88/// precedence without retaining or cloning the parts.
89///
90/// `HeaderMap`, [`Parts`], [`Extensions`](rama_core::extensions::Extensions), and
91/// [`Uri`](rama_net::uri::Uri) implement this trait.
92///
93/// # Example
94///
95/// ```
96/// use rama_http::{HeaderMap, StatusCode};
97/// use rama_http::service::web::{WebService, extract::Text};
98///
99/// let _service = WebService::default().with_post(
100/// "/",
101/// async |_headers: HeaderMap, Text(_payload): Text| StatusCode::OK,
102/// );
103/// ```
104#[diagnostic::on_unimplemented(
105 note = "Owned request-parts extractors must be the final argument, or immediately precede a body extractor that implements `FromRequestBody`."
106)]
107pub trait FromOwnedRequestParts: Sized + Send + 'static {
108 /// If the extractor fails it'll use this rejection type.
109 type Rejection;
110
111 /// Perform the extraction by consuming the request parts.
112 fn from_owned_request_parts(
113 parts: Parts,
114 ) -> impl Future<Output = Result<Self, Self::Rejection>> + Send + 'static;
115}
116
117/// Types that own the request body while only borrowing the other request parts.
118///
119/// This is the body-only counterpart to [`FromRequest`]. It enables an immediately preceding
120/// [`FromOwnedRequestParts`] extractor to take ownership of the request parts after the body
121/// extractor has finished inspecting them. Types implementing this trait also implement
122/// [`FromRequest`] so they remain usable as ordinary terminal extractors.
123#[diagnostic::on_unimplemented(
124 note = "The final argument after an owned request-parts extractor must implement `FromRequestBody`."
125)]
126pub trait FromRequestBody: FromRequest {
127 /// Prepare extraction using borrowed request parts and an owned request body.
128 ///
129 /// The returned future cannot borrow `parts`, allowing the request parts to be dropped or
130 /// moved before body processing is awaited.
131 fn from_request_body(
132 parts: &Parts,
133 body: crate::Body,
134 ) -> impl Future<Output = Result<Self, Self::Rejection>> + Send + 'static;
135}
136
137/// Types that can be created from requests.
138///
139/// Extractors that implement `FromRequest` can consume the request body and can thus only be run
140/// once for handlers.
141///
142/// If your extractor owns only the request body and merely borrows the request parts, also
143/// implement [`FromRequestBody`] to compose with a preceding [`FromOwnedRequestParts`] extractor.
144/// If it doesn't need to consume the request body at all, implement [`FromPartsStateRefPair`]
145/// instead.
146#[diagnostic::on_unimplemented(
147 note = "Function argument is not a valid web endpoint extractor. \nSee `https://ramaproxy.org/docs/rama/http/service/web/extract/index.html` for details"
148)]
149pub trait FromRequest: Sized + Send + 'static {
150 /// If the extractor fails it'll use this "rejection" type. A rejection is
151 /// a kind of error that can be converted into a response.
152 type Rejection;
153
154 /// Perform the extraction.
155 fn from_request(
156 req: crate::Request,
157 ) -> impl Future<Output = Result<Self, Self::Rejection>> + Send;
158}
159
160impl<T> FromRequest for T
161where
162 T: FromOwnedRequestParts,
163{
164 type Rejection = T::Rejection;
165
166 async fn from_request(req: crate::Request) -> Result<Self, Self::Rejection> {
167 Self::from_owned_request_parts(req.into_parts().0).await
168 }
169}
170
171fn has_any_content_type(headers: &HeaderMap, expected_content_types: &[&mime::Mime]) -> bool {
172 let Some(content_type) = headers.get(header::CONTENT_TYPE) else {
173 return false;
174 };
175
176 let Ok(content_type) = content_type.to_str() else {
177 return false;
178 };
179
180 expected_content_types
181 .iter()
182 .any(|ct| content_type.starts_with(ct.as_ref()))
183}