pincer_core/
response.rs

1//! HTTP response handling.
2//!
3//! [`Response`] provides access to status, headers, and body with JSON/text deserialization.
4//!
5//! # Example
6//!
7//! ```ignore
8//! let user: User = response.json()?;
9//! ```
10//!
11//! For large responses, enable the `streaming` feature for [`streaming::StreamingResponse`].
12
13use std::collections::HashMap;
14
15use bytes::Bytes;
16
17// ============================================================================
18// Streaming Response (feature-gated)
19// ============================================================================
20
21/// Streaming response support (requires `streaming` feature).
22#[cfg(feature = "streaming")]
23pub mod streaming {
24    use std::collections::HashMap;
25    use std::pin::Pin;
26
27    use bytes::Bytes;
28    use futures_core::Stream;
29    use futures_util::StreamExt;
30
31    /// A streaming body: chunks of bytes arriving over time.
32    pub type StreamingBody = Pin<Box<dyn Stream<Item = crate::Result<Bytes>> + Send>>;
33
34    /// HTTP response with streaming body, for large payloads.
35    ///
36    /// Unlike [`super::Response`], the body is consumed as a stream of chunks.
37    pub struct StreamingResponse {
38        status: u16,
39        headers: HashMap<String, String>,
40        body: StreamingBody,
41    }
42
43    impl StreamingResponse {
44        /// Creates a new streaming response.
45        #[must_use]
46        pub fn new(status: u16, headers: HashMap<String, String>, body: StreamingBody) -> Self {
47            Self {
48                status,
49                headers,
50                body,
51            }
52        }
53
54        /// HTTP status code.
55        #[must_use]
56        pub const fn status(&self) -> u16 {
57            self.status
58        }
59
60        /// Response headers.
61        #[must_use]
62        pub fn headers(&self) -> &HashMap<String, String> {
63            &self.headers
64        }
65
66        /// Single header value by name.
67        #[must_use]
68        pub fn header(&self, name: &str) -> Option<&str> {
69            self.headers.get(name).map(String::as_str)
70        }
71
72        /// Status is 2xx.
73        #[must_use]
74        pub const fn is_success(&self) -> bool {
75            self.status >= 200 && self.status < 300
76        }
77
78        /// Status is 4xx.
79        #[must_use]
80        pub const fn is_client_error(&self) -> bool {
81            self.status >= 400 && self.status < 500
82        }
83
84        /// Status is 5xx.
85        #[must_use]
86        pub const fn is_server_error(&self) -> bool {
87            self.status >= 500 && self.status < 600
88        }
89
90        /// Consume into the streaming body.
91        #[must_use]
92        pub fn into_body(self) -> StreamingBody {
93            self.body
94        }
95
96        /// Buffer the entire stream into a [`Response`].
97        ///
98        /// # Errors
99        ///
100        /// Returns an error if reading any chunk fails.
101        pub async fn collect(self) -> crate::Result<super::Response<Bytes>> {
102            let mut body = self.body;
103            let mut collected = Vec::new();
104
105            while let Some(chunk) = body.next().await {
106                collected.extend_from_slice(&chunk?);
107            }
108
109            Ok(super::Response::new(
110                self.status,
111                self.headers,
112                Bytes::from(collected),
113            ))
114        }
115    }
116}
117
118// ============================================================================
119// Buffered Response
120// ============================================================================
121
122/// HTTP response with status, headers, and body.
123#[derive(Debug, Clone)]
124pub struct Response<B = Bytes> {
125    status: u16,
126    headers: HashMap<String, String>,
127    body: B,
128}
129
130impl<B> Response<B> {
131    /// Creates a new response.
132    #[must_use]
133    pub fn new(status: u16, headers: HashMap<String, String>, body: B) -> Self {
134        Self {
135            status,
136            headers,
137            body,
138        }
139    }
140
141    /// HTTP status code.
142    #[must_use]
143    pub const fn status(&self) -> u16 {
144        self.status
145    }
146
147    /// Response headers.
148    #[must_use]
149    pub fn headers(&self) -> &HashMap<String, String> {
150        &self.headers
151    }
152
153    /// Single header value by name.
154    #[must_use]
155    pub fn header(&self, name: &str) -> Option<&str> {
156        self.headers.get(name).map(String::as_str)
157    }
158
159    /// Response body.
160    #[must_use]
161    pub const fn body(&self) -> &B {
162        &self.body
163    }
164
165    /// Consume into body.
166    #[must_use]
167    pub fn into_body(self) -> B {
168        self.body
169    }
170
171    /// Consume into (status, headers, body).
172    #[must_use]
173    pub fn into_parts(self) -> (u16, HashMap<String, String>, B) {
174        (self.status, self.headers, self.body)
175    }
176
177    /// Status is 2xx.
178    #[must_use]
179    pub const fn is_success(&self) -> bool {
180        self.status >= 200 && self.status < 300
181    }
182
183    /// Status is 3xx.
184    #[must_use]
185    pub const fn is_redirection(&self) -> bool {
186        self.status >= 300 && self.status < 400
187    }
188
189    /// Status is 4xx.
190    #[must_use]
191    pub const fn is_client_error(&self) -> bool {
192        self.status >= 400 && self.status < 500
193    }
194
195    /// Status is 5xx.
196    #[must_use]
197    pub const fn is_server_error(&self) -> bool {
198        self.status >= 500 && self.status < 600
199    }
200
201    /// Transform the body with a function.
202    pub fn map_body<F, B2>(self, f: F) -> Response<B2>
203    where
204        F: FnOnce(B) -> B2,
205    {
206        Response {
207            status: self.status,
208            headers: self.headers,
209            body: f(self.body),
210        }
211    }
212}
213
214impl Response<Bytes> {
215    /// Deserialize the response body as JSON.
216    ///
217    /// # Errors
218    ///
219    /// Returns an error if deserialization fails.
220    pub fn json<T: serde::de::DeserializeOwned>(self) -> crate::Result<T> {
221        crate::from_json(&self.body)
222    }
223
224    /// Get the response body as text.
225    ///
226    /// # Errors
227    ///
228    /// Returns an error if the body is not valid UTF-8.
229    pub fn text(self) -> Result<String, std::string::FromUtf8Error> {
230        String::from_utf8(self.body.to_vec())
231    }
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237
238    #[test]
239    fn response_basic() {
240        let mut headers = HashMap::new();
241        headers.insert("Content-Type".to_string(), "application/json".to_string());
242
243        let response = Response::new(200, headers, Bytes::from(r#"{"id":1}"#));
244
245        assert_eq!(response.status(), 200);
246        assert_eq!(response.header("Content-Type"), Some("application/json"));
247        assert!(response.is_success());
248        assert!(!response.is_client_error());
249        assert!(!response.is_server_error());
250    }
251
252    #[test]
253    fn response_status_checks() {
254        let response = Response::new(301, HashMap::new(), Bytes::new());
255        assert!(response.is_redirection());
256
257        let response = Response::new(404, HashMap::new(), Bytes::new());
258        assert!(response.is_client_error());
259
260        let response = Response::new(500, HashMap::new(), Bytes::new());
261        assert!(response.is_server_error());
262    }
263
264    #[test]
265    fn response_json() {
266        #[derive(Debug, PartialEq, serde::Deserialize)]
267        struct User {
268            id: u64,
269            name: String,
270        }
271
272        let body = Bytes::from(r#"{"id":1,"name":"test"}"#);
273        let response = Response::new(200, HashMap::new(), body);
274
275        let user: User = response.json().expect("deserialize");
276        assert_eq!(
277            user,
278            User {
279                id: 1,
280                name: "test".to_string()
281            }
282        );
283    }
284
285    #[test]
286    fn response_text() {
287        let body = Bytes::from("Hello, World!");
288        let response = Response::new(200, HashMap::new(), body);
289
290        let text = response.text().expect("text");
291        assert_eq!(text, "Hello, World!");
292    }
293
294    #[test]
295    fn response_map_body() {
296        let response = Response::new(200, HashMap::new(), Bytes::from("test"));
297        let mapped = response.map_body(|b| b.len());
298
299        assert_eq!(mapped.status(), 200);
300        assert_eq!(*mapped.body(), 4);
301    }
302}