Skip to main content

apimock_server/
types.rs

1use http_body_util::BodyExt;
2use hyper::body::Bytes;
3
4use std::convert::Infallible;
5
6pub type BoxBody = http_body_util::combinators::BoxBody<Bytes, Infallible>;
7
8/// A response fully read into memory — status, headers, body — rather
9/// than the streaming `hyper::Response<BoxBody>` every dispatch function
10/// returns.
11///
12/// # Why this exists (RFC 055)
13///
14/// `apimock get` answers a question; it does not write to a socket.
15/// Reusing `rule_set_response`/`dyn_route_content`/`respond_response`
16/// unchanged (the RFC's own "one implementation of matching" principle)
17/// still leaves a `BoxBody` on the way out, and a CLI command has
18/// nothing to stream it *to* — it needs the bytes, once, to print or
19/// serialise. This is the minimal additive surface that gap needed:
20/// no new dispatch logic, just a collector for what dispatch already
21/// produces.
22pub struct CollectedResponse {
23    pub status: hyper::StatusCode,
24    /// Header order preserved as received; values that aren't valid
25    /// UTF-8 are dropped, matching how `RequestSummary`'s own header
26    /// collection (`trace.rs`) already treats non-UTF-8 values.
27    pub headers: Vec<(String, String)>,
28    pub body: Vec<u8>,
29}
30
31impl CollectedResponse {
32    /// Consume a streaming response and read it fully into memory.
33    pub async fn collect(response: hyper::Response<BoxBody>) -> Self {
34        let (parts, body) = response.into_parts();
35        // `BoxBody`'s error type is `Infallible` (see the alias above) —
36        // collection cannot fail.
37        let bytes = body
38            .collect()
39            .await
40            .expect("BoxBody's error type is Infallible")
41            .to_bytes();
42        let headers = parts
43            .headers
44            .iter()
45            .filter_map(|(k, v)| v.to_str().ok().map(|v| (k.to_string(), v.to_owned())))
46            .collect();
47        Self {
48            status: parts.status,
49            headers,
50            body: bytes.to_vec(),
51        }
52    }
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58    use http_body_util::Full;
59
60    #[tokio::test]
61    async fn collect_reads_status_headers_and_body() {
62        let response = hyper::Response::builder()
63            .status(201)
64            .header("x-custom", "yes")
65            .body(
66                Full::new(Bytes::from_static(b"hello"))
67                    .map_err(|e| match e {})
68                    .boxed(),
69            )
70            .unwrap();
71
72        let collected = CollectedResponse::collect(response).await;
73        assert_eq!(collected.status, hyper::StatusCode::CREATED);
74        assert_eq!(collected.body, b"hello");
75        assert!(
76            collected
77                .headers
78                .iter()
79                .any(|(k, v)| k == "x-custom" && v == "yes")
80        );
81    }
82}