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}