Skip to main content

churust_core/
call.rs

1//! The per-request [`Call`] context — the single object every handler receives.
2
3use crate::error::{Error, Result};
4use crate::response::Response;
5use crate::state::StateMap;
6use bytes::Bytes;
7use http::{HeaderMap, Method, StatusCode, Uri};
8use std::collections::HashMap;
9use std::sync::Arc;
10
11/// Per-request context: the single object a handler receives (Ktor-style).
12///
13/// A `Call` bundles the request method, URI, headers, captured path
14/// parameters, the (buffered) body, a handle to shared application
15/// [state](crate::StateMap), and a per-call extension map. Handlers receive it
16/// either directly (`|c: Call| async { ... }`) or indirectly through
17/// [extractors](crate::extract) such as [`Path`](crate::Path) and
18/// [`Query`](crate::Query), which read from the `Call` for you.
19///
20/// Read-only accessors ([`method`](Call::method), [`uri`](Call::uri),
21/// [`path`](Call::path), [`header`](Call::header), [`query`](Call::query),
22/// [`param`](Call::param)) borrow `&self`; body consumption
23/// ([`receive_bytes`](Call::receive_bytes), [`receive_text`](Call::receive_text))
24/// takes `&mut self`. The [`insert`](Call::insert)/[`get`](Call::get) pair
25/// passes typed values between middleware and downstream handlers.
26///
27/// Construct one with [`Call::new`] in unit tests; in production the engine and
28/// the [`TestClient`](crate::TestClient) build it for you.
29///
30/// ```
31/// use churust_core::Call;
32/// use http::{HeaderMap, Method};
33/// use bytes::Bytes;
34///
35/// let call = Call::new(
36///     Method::GET,
37///     "/search?q=rust".parse().unwrap(),
38///     HeaderMap::new(),
39///     Bytes::new(),
40/// );
41/// assert_eq!(call.method(), &Method::GET);
42/// assert_eq!(call.path(), "/search");
43/// assert_eq!(call.query("q").as_deref(), Some("rust"));
44/// ```
45#[derive(Debug)]
46pub struct Call {
47    method: Method,
48    uri: Uri,
49    headers: HeaderMap,
50    params: HashMap<String, String>,
51    body: Bytes,
52    state: Arc<StateMap>,
53    extensions: http::Extensions,
54}
55
56impl Call {
57    /// Construct a Call from already-parsed request parts (used by the engine
58    /// and the test harness alike).
59    pub fn new(method: Method, uri: Uri, headers: HeaderMap, body: Bytes) -> Self {
60        Self {
61            method,
62            uri,
63            headers,
64            params: HashMap::new(),
65            body,
66            state: Arc::new(StateMap::default()),
67            extensions: http::Extensions::new(),
68        }
69    }
70
71    /// The request HTTP method.
72    pub fn method(&self) -> &Method {
73        &self.method
74    }
75    /// The full request URI (path, query, and any authority).
76    pub fn uri(&self) -> &Uri {
77        &self.uri
78    }
79    /// The request path, without the query string (e.g. `/users/42`).
80    pub fn path(&self) -> &str {
81        self.uri.path()
82    }
83    /// The full request header map.
84    pub fn headers(&self) -> &HeaderMap {
85        &self.headers
86    }
87
88    /// The value of header `name` as a UTF-8 string, or `None` if the header is
89    /// absent or its value is not valid UTF-8. Header name matching is
90    /// case-insensitive.
91    ///
92    /// ```
93    /// use churust_core::Call;
94    /// use http::{HeaderMap, Method, header::ACCEPT, HeaderValue};
95    /// use bytes::Bytes;
96    ///
97    /// let mut headers = HeaderMap::new();
98    /// headers.insert(ACCEPT, HeaderValue::from_static("application/json"));
99    /// let call = Call::new(Method::GET, "/".parse().unwrap(), headers, Bytes::new());
100    /// assert_eq!(call.header("accept"), Some("application/json"));
101    /// assert_eq!(call.header("x-missing"), None);
102    /// ```
103    pub fn header(&self, name: &str) -> Option<&str> {
104        self.headers.get(name).and_then(|v| v.to_str().ok())
105    }
106
107    /// The raw query string with the leading `?` stripped, or `""` if there is
108    /// none. To deserialize the whole query into a struct, prefer the
109    /// [`Query`](crate::Query) extractor.
110    ///
111    /// ```
112    /// use churust_core::Call;
113    /// use http::{HeaderMap, Method};
114    /// use bytes::Bytes;
115    ///
116    /// let call = Call::new(Method::GET, "/s?a=1&b=2".parse().unwrap(), HeaderMap::new(), Bytes::new());
117    /// assert_eq!(call.query_string(), "a=1&b=2");
118    /// ```
119    pub fn query_string(&self) -> &str {
120        self.uri.query().unwrap_or("")
121    }
122
123    /// The first value for query key `key`, percent- and `+`-decoded, or `None`
124    /// if the key is absent.
125    ///
126    /// ```
127    /// use churust_core::Call;
128    /// use http::{HeaderMap, Method};
129    /// use bytes::Bytes;
130    ///
131    /// let call = Call::new(Method::GET, "/s?q=hello+world".parse().unwrap(), HeaderMap::new(), Bytes::new());
132    /// assert_eq!(call.query("q").as_deref(), Some("hello world"));
133    /// assert_eq!(call.query("missing"), None);
134    /// ```
135    pub fn query(&self, key: &str) -> Option<String> {
136        form_urlencoded_first(self.query_string(), key)
137    }
138
139    /// Set by the router after a successful match.
140    pub(crate) fn set_params(&mut self, params: HashMap<String, String>) {
141        self.params = params;
142    }
143
144    /// Injected by `App::process` before the pipeline runs.
145    pub(crate) fn set_state(&mut self, state: Arc<StateMap>) {
146        self.state = state;
147    }
148
149    /// Merge externally-built extensions into this call (used by the engine to
150    /// inject per-connection data such as a pending WebSocket upgrade). Existing
151    /// entries of the same type are overwritten.
152    pub(crate) fn seed_extensions(&mut self, ext: http::Extensions) {
153        self.extensions.extend(ext);
154    }
155
156    /// A shared handle to application state of type `T`, or `None` if no value
157    /// of that type was registered with
158    /// [`AppBuilder::state`](crate::AppBuilder::state). For handler arguments,
159    /// the [`State`](crate::State) extractor is usually more convenient.
160    ///
161    /// ```
162    /// use churust_core::{Churust, Call, TestClient};
163    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
164    /// #[derive(Clone)]
165    /// struct AppName(&'static str);
166    ///
167    /// let app = Churust::server()
168    ///     .state(AppName("churust"))
169    ///     .routing(|r| {
170    ///         r.get("/", |c: Call| async move {
171    ///             format!("app={}", c.state::<AppName>().unwrap().0)
172    ///         });
173    ///     })
174    ///     .build();
175    /// let res = TestClient::new(app).get("/").send().await;
176    /// assert_eq!(res.text(), "app=churust");
177    /// # });
178    /// ```
179    pub fn state<T: Send + Sync + 'static>(&self) -> Option<Arc<T>> {
180        self.state.get::<T>()
181    }
182
183    /// Iterate over the captured path parameters as `(name, value)` pairs.
184    /// Iteration order is unspecified (the parameters are stored in a hash map).
185    pub fn params_iter(&self) -> impl Iterator<Item = (&str, &str)> {
186        self.params.iter().map(|(k, v)| (k.as_str(), v.as_str()))
187    }
188
189    /// The raw, unparsed value of path parameter `name`, or `None` if the route
190    /// has no such parameter. Use [`param`](Call::param) to parse it into a
191    /// typed value.
192    pub fn param_raw(&self, name: &str) -> Option<&str> {
193        self.params.get(name).map(|s| s.as_str())
194    }
195
196    /// Parse path parameter `name` into `T`.
197    ///
198    /// Returns a `400 Bad Request` [`Error`] if the parameter is
199    /// missing or fails to parse.
200    ///
201    /// ```
202    /// use churust_core::{Churust, Call, TestClient};
203    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
204    /// let app = Churust::server()
205    ///     .routing(|r| {
206    ///         r.get("/users/{id}", |c: Call| async move {
207    ///             let id: u64 = c.param("id")?;
208    ///             Ok::<_, churust_core::Error>(format!("user {id}"))
209    ///         });
210    ///     })
211    ///     .build();
212    /// let res = TestClient::new(app).get("/users/42").send().await;
213    /// assert_eq!(res.text(), "user 42");
214    /// # });
215    /// ```
216    pub fn param<T>(&self, name: &str) -> Result<T>
217    where
218        T: std::str::FromStr,
219        T::Err: std::fmt::Display,
220    {
221        let raw = self
222            .param_raw(name)
223            .ok_or_else(|| Error::bad_request(format!("missing path param `{name}`")))?;
224        raw.parse::<T>()
225            .map_err(|e| Error::bad_request(format!("bad path param `{name}`: {e}")))
226    }
227
228    /// Take the request body as raw [`Bytes`], leaving the call's body empty.
229    ///
230    /// This consumes the body: a second call returns an empty buffer. It is
231    /// `async` to leave room for future streaming bodies.
232    ///
233    /// ```
234    /// use churust_core::Call;
235    /// use http::{HeaderMap, Method};
236    /// use bytes::Bytes;
237    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
238    /// let mut call = Call::new(Method::POST, "/".parse().unwrap(), HeaderMap::new(), Bytes::from("data"));
239    /// assert_eq!(&call.receive_bytes().await[..], b"data");
240    /// assert!(call.receive_bytes().await.is_empty()); // already consumed
241    /// # });
242    /// ```
243    pub async fn receive_bytes(&mut self) -> Bytes {
244        std::mem::take(&mut self.body)
245    }
246
247    /// Take the request body decoded as UTF-8, consuming it.
248    ///
249    /// Returns a `400 Bad Request` [`Error`] if the body is not
250    /// valid UTF-8.
251    ///
252    /// ```
253    /// use churust_core::Call;
254    /// use http::{HeaderMap, Method};
255    /// use bytes::Bytes;
256    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
257    /// let mut call = Call::new(Method::POST, "/".parse().unwrap(), HeaderMap::new(), Bytes::from("ping"));
258    /// assert_eq!(call.receive_text().await.unwrap(), "ping");
259    /// # });
260    /// ```
261    pub async fn receive_text(&mut self) -> Result<String> {
262        let bytes = self.receive_bytes().await;
263        String::from_utf8(bytes.to_vec())
264            .map_err(|e| Error::bad_request(format!("body is not valid UTF-8: {e}")))
265    }
266
267    /// Insert a per-call typed value into the call's extension map, keyed by its
268    /// type. Typically used by a middleware to attach context (e.g. an
269    /// authenticated principal) that a downstream extractor or handler reads via
270    /// [`get`](Call::get). Inserting a second value of the same type replaces
271    /// the first.
272    ///
273    /// ```
274    /// use churust_core::Call;
275    /// use http::{HeaderMap, Method};
276    /// use bytes::Bytes;
277    ///
278    /// #[derive(Clone, PartialEq, Debug)]
279    /// struct UserId(u32);
280    ///
281    /// let mut call = Call::new(Method::GET, "/".parse().unwrap(), HeaderMap::new(), Bytes::new());
282    /// call.insert(UserId(7));
283    /// assert_eq!(call.get::<UserId>(), Some(UserId(7)));
284    /// ```
285    pub fn insert<T: Clone + Send + Sync + 'static>(&mut self, value: T) {
286        self.extensions.insert(value);
287    }
288
289    /// Get a clone of the per-call value of type `T` previously stored with
290    /// [`insert`](Call::insert), or `None` if none was stored.
291    ///
292    /// ```
293    /// use churust_core::Call;
294    /// use http::{HeaderMap, Method};
295    /// use bytes::Bytes;
296    ///
297    /// let call = Call::new(Method::GET, "/".parse().unwrap(), HeaderMap::new(), Bytes::new());
298    /// assert_eq!(call.get::<u32>(), None);
299    /// ```
300    pub fn get<T: Clone + Send + Sync + 'static>(&self) -> Option<T> {
301        self.extensions.get::<T>().cloned()
302    }
303
304    // ---- response convenience (sync; body is in memory) ----
305
306    /// Build a `200 OK` `text/plain` [`Response`] — a shorthand for
307    /// [`Response::text`].
308    ///
309    /// ```
310    /// use churust_core::Call;
311    /// use http::{HeaderMap, Method};
312    /// use bytes::Bytes;
313    ///
314    /// let call = Call::new(Method::GET, "/".parse().unwrap(), HeaderMap::new(), Bytes::new());
315    /// let res = call.respond_text("ok");
316    /// assert_eq!(res.body.as_slice(), Some(&b"ok"[..]));
317    /// ```
318    pub fn respond_text(&self, body: impl Into<String>) -> Response {
319        Response::text(body)
320    }
321
322    /// Build an empty-bodied [`Response`] with the given status — a shorthand
323    /// for [`Response::new`].
324    ///
325    /// ```
326    /// use churust_core::Call;
327    /// use http::{HeaderMap, Method, StatusCode};
328    /// use bytes::Bytes;
329    ///
330    /// let call = Call::new(Method::GET, "/".parse().unwrap(), HeaderMap::new(), Bytes::new());
331    /// assert_eq!(call.respond_status(StatusCode::ACCEPTED).status, StatusCode::ACCEPTED);
332    /// ```
333    pub fn respond_status(&self, status: StatusCode) -> Response {
334        Response::new(status)
335    }
336}
337
338fn form_urlencoded_first(query: &str, key: &str) -> Option<String> {
339    for pair in query.split('&') {
340        let mut it = pair.splitn(2, '=');
341        let k = it.next().unwrap_or("");
342        if k == key {
343            let v = it.next().unwrap_or("");
344            return Some(percent_decode(v));
345        }
346    }
347    None
348}
349
350fn percent_decode(s: &str) -> String {
351    // Minimal `+` and %XX decoding sufficient for v1 query parsing.
352    let bytes = s.as_bytes();
353    let mut out = Vec::with_capacity(bytes.len());
354    let mut i = 0;
355    while i < bytes.len() {
356        match bytes[i] {
357            b'+' => {
358                out.push(b' ');
359                i += 1;
360            }
361            b'%' if i + 2 < bytes.len() => {
362                let hi = (bytes[i + 1] as char).to_digit(16);
363                let lo = (bytes[i + 2] as char).to_digit(16);
364                match (hi, lo) {
365                    (Some(h), Some(l)) => {
366                        out.push((h * 16 + l) as u8);
367                        i += 3;
368                    }
369                    _ => {
370                        out.push(bytes[i]);
371                        i += 1;
372                    }
373                }
374            }
375            b => {
376                out.push(b);
377                i += 1;
378            }
379        }
380    }
381    String::from_utf8_lossy(&out).into_owned()
382}
383
384#[cfg(test)]
385mod tests {
386    use super::*;
387
388    fn call(path: &str, body: &str) -> Call {
389        Call::new(
390            Method::GET,
391            path.parse::<Uri>().unwrap(),
392            HeaderMap::new(),
393            Bytes::from(body.to_string()),
394        )
395    }
396
397    #[test]
398    fn reads_method_and_path() {
399        let c = call("/a/b?x=1", "");
400        assert_eq!(c.method(), &Method::GET);
401        assert_eq!(c.path(), "/a/b");
402    }
403
404    #[test]
405    fn parses_query() {
406        let c = call("/s?q=hello+world&n=5", "");
407        assert_eq!(c.query("q").as_deref(), Some("hello world"));
408        assert_eq!(c.query("n").as_deref(), Some("5"));
409        assert_eq!(c.query("missing"), None);
410    }
411
412    #[test]
413    fn parses_query_percent_encoded() {
414        let c = call("/s?q=hello%20world", "");
415        assert_eq!(c.query("q").as_deref(), Some("hello world"));
416    }
417
418    #[test]
419    fn parses_path_param() {
420        let mut c = call("/users/42", "");
421        let mut p = HashMap::new();
422        p.insert("id".to_string(), "42".to_string());
423        c.set_params(p);
424        let id: u64 = c.param("id").unwrap();
425        assert_eq!(id, 42);
426        assert!(c.param::<u64>("missing").is_err());
427    }
428
429    #[tokio::test]
430    async fn receives_text_body() {
431        let mut c = call("/", "payload");
432        assert_eq!(c.receive_text().await.unwrap(), "payload");
433    }
434
435    #[test]
436    fn state_round_trips() {
437        use crate::state::StateMap;
438        let mut c = call("/", "");
439        let mut sm = StateMap::default();
440        sm.insert(99u32);
441        c.set_state(std::sync::Arc::new(sm));
442        assert_eq!(*c.state::<u32>().unwrap(), 99);
443    }
444
445    #[test]
446    fn extensions_round_trip() {
447        #[derive(Clone, PartialEq, Debug)]
448        struct User(u32);
449        let mut c = call("/", "");
450        assert!(c.get::<User>().is_none());
451        c.insert(User(7));
452        assert_eq!(c.get::<User>(), Some(User(7)));
453    }
454}