pincer_core/
request.rs

1//! HTTP request building.
2//!
3//! Use [`Request::builder`] to construct requests with headers, query parameters, and bodies.
4//!
5//! # Example
6//!
7//! ```
8//! use pincer_core::{Request, Method};
9//! use bytes::Bytes;
10//!
11//! let request = Request::<Bytes>::builder(Method::Get, "https://api.example.com".parse().unwrap())
12//!     .header("Accept", "application/json")
13//!     .query("page", "1")
14//!     .build();
15//! ```
16
17use std::collections::HashMap;
18
19use bytes::Bytes;
20use http::Extensions;
21
22use crate::Method;
23
24/// An HTTP request with method, URL, headers, optional body, and extensions.
25#[derive(Debug, Clone)]
26pub struct Request<B = Bytes> {
27    method: Method,
28    url: url::Url,
29    headers: HashMap<String, String>,
30    body: Option<B>,
31    extensions: Extensions,
32}
33
34impl<B> Request<B> {
35    /// Creates a new [`RequestBuilder`].
36    #[must_use]
37    pub fn builder(method: Method, url: url::Url) -> RequestBuilder<B> {
38        RequestBuilder::new(method, url)
39    }
40
41    /// HTTP method.
42    #[must_use]
43    pub const fn method(&self) -> Method {
44        self.method
45    }
46
47    /// Request URL.
48    #[must_use]
49    pub fn url(&self) -> &url::Url {
50        &self.url
51    }
52
53    /// Request headers.
54    #[must_use]
55    pub fn headers(&self) -> &HashMap<String, String> {
56        &self.headers
57    }
58
59    /// Mutable access to headers.
60    #[must_use]
61    pub fn headers_mut(&mut self) -> &mut HashMap<String, String> {
62        &mut self.headers
63    }
64
65    /// Single header value by name.
66    #[must_use]
67    pub fn header(&self, name: &str) -> Option<&str> {
68        self.headers.get(name).map(String::as_str)
69    }
70
71    /// Request body.
72    #[must_use]
73    pub const fn body(&self) -> Option<&B> {
74        self.body.as_ref()
75    }
76
77    /// Request extensions.
78    ///
79    /// Extensions allow middleware to attach arbitrary typed data to requests.
80    #[must_use]
81    pub fn extensions(&self) -> &Extensions {
82        &self.extensions
83    }
84
85    /// Mutable access to extensions.
86    #[must_use]
87    pub fn extensions_mut(&mut self) -> &mut Extensions {
88        &mut self.extensions
89    }
90
91    /// Consume into (method, url, headers, body, extensions).
92    #[must_use]
93    pub fn into_parts(
94        self,
95    ) -> (
96        Method,
97        url::Url,
98        HashMap<String, String>,
99        Option<B>,
100        Extensions,
101    ) {
102        (
103            self.method,
104            self.url,
105            self.headers,
106            self.body,
107            self.extensions,
108        )
109    }
110
111    /// Construct a request from its parts.
112    ///
113    /// This is the inverse of [`into_parts`](Self::into_parts) and is useful
114    /// for middleware that needs to modify a request and reconstruct it.
115    #[must_use]
116    pub fn from_parts(
117        method: Method,
118        url: url::Url,
119        headers: HashMap<String, String>,
120        body: Option<B>,
121        extensions: Extensions,
122    ) -> Self {
123        Self {
124            method,
125            url,
126            headers,
127            body,
128            extensions,
129        }
130    }
131}
132
133/// Builder for constructing [`Request`] instances.
134#[derive(Debug, Clone)]
135pub struct RequestBuilder<B = Bytes> {
136    method: Method,
137    url: url::Url,
138    headers: HashMap<String, String>,
139    body: Option<B>,
140    extensions: Extensions,
141}
142
143impl<B> RequestBuilder<B> {
144    /// Creates a new builder.
145    #[must_use]
146    pub fn new(method: Method, url: url::Url) -> Self {
147        Self {
148            method,
149            url,
150            headers: HashMap::new(),
151            body: None,
152            extensions: Extensions::new(),
153        }
154    }
155
156    /// Sets a header.
157    #[must_use]
158    pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
159        self.headers.insert(name.into(), value.into());
160        self
161    }
162
163    /// Sets multiple headers.
164    #[must_use]
165    pub fn headers(mut self, headers: impl IntoIterator<Item = (String, String)>) -> Self {
166        self.headers.extend(headers);
167        self
168    }
169
170    /// Appends a query parameter to the URL.
171    #[must_use]
172    pub fn query(mut self, name: &str, value: &str) -> Self {
173        self.url.query_pairs_mut().append_pair(name, value);
174        self
175    }
176
177    /// Appends multiple query parameters to the URL.
178    #[must_use]
179    pub fn query_pairs(mut self, pairs: impl IntoIterator<Item = (String, String)>) -> Self {
180        {
181            let mut query = self.url.query_pairs_mut();
182            for (name, value) in pairs {
183                query.append_pair(&name, &value);
184            }
185        }
186        self
187    }
188
189    /// Sets the request body.
190    #[must_use]
191    pub fn body(mut self, body: B) -> Self {
192        self.body = Some(body);
193        self
194    }
195
196    /// Insert a typed extension value.
197    ///
198    /// Extensions allow middleware to attach arbitrary typed data to requests.
199    #[must_use]
200    pub fn extension<T: Clone + Send + Sync + 'static>(mut self, value: T) -> Self {
201        self.extensions.insert(value);
202        self
203    }
204
205    /// Set extensions from an existing `Extensions` container.
206    ///
207    /// This replaces any previously set extensions.
208    #[must_use]
209    pub fn extensions(mut self, extensions: Extensions) -> Self {
210        self.extensions = extensions;
211        self
212    }
213
214    /// Builds the [`Request`].
215    #[must_use]
216    pub fn build(self) -> Request<B> {
217        Request {
218            method: self.method,
219            url: self.url,
220            headers: self.headers,
221            body: self.body,
222            extensions: self.extensions,
223        }
224    }
225}
226
227impl RequestBuilder<Bytes> {
228    /// Set a JSON body.
229    ///
230    /// # Errors
231    ///
232    /// Returns an error if serialization fails.
233    pub fn json<T: serde::Serialize>(self, value: &T) -> crate::Result<Self> {
234        let body = crate::to_json(value)?;
235        Ok(self.header("Content-Type", "application/json").body(body))
236    }
237
238    /// Set a form-urlencoded body.
239    ///
240    /// # Errors
241    ///
242    /// Returns an error if serialization fails.
243    pub fn form<T: serde::Serialize>(self, value: &T) -> crate::Result<Self> {
244        let body = crate::to_form(value)?;
245        Ok(self
246            .header("Content-Type", "application/x-www-form-urlencoded")
247            .body(body))
248    }
249}
250
251#[cfg(test)]
252mod tests {
253    use super::*;
254
255    #[test]
256    fn request_builder_basic() {
257        let url = url::Url::parse("https://api.example.com/users").expect("valid URL");
258        let request = Request::<Bytes>::builder(Method::Get, url.clone())
259            .header("Accept", "application/json")
260            .build();
261
262        assert_eq!(request.method(), Method::Get);
263        assert_eq!(request.url().as_str(), "https://api.example.com/users");
264        assert_eq!(request.header("Accept"), Some("application/json"));
265        assert!(request.body().is_none());
266    }
267
268    #[test]
269    fn request_builder_with_query() {
270        let url = url::Url::parse("https://api.example.com/users").expect("valid URL");
271        let request = Request::<Bytes>::builder(Method::Get, url)
272            .query("page", "1")
273            .query("limit", "10")
274            .build();
275
276        assert_eq!(
277            request.url().as_str(),
278            "https://api.example.com/users?page=1&limit=10"
279        );
280    }
281
282    #[test]
283    fn request_builder_with_body() {
284        let url = url::Url::parse("https://api.example.com/users").expect("valid URL");
285        let body = Bytes::from(r#"{"name":"test"}"#);
286        let request = Request::builder(Method::Post, url)
287            .header("Content-Type", "application/json")
288            .body(body.clone())
289            .build();
290
291        assert_eq!(request.method(), Method::Post);
292        assert_eq!(request.body(), Some(&body));
293    }
294
295    #[test]
296    fn request_builder_json() {
297        #[derive(serde::Serialize)]
298        struct User {
299            name: String,
300        }
301
302        let url = url::Url::parse("https://api.example.com/users").expect("valid URL");
303        let request = Request::builder(Method::Post, url)
304            .json(&User {
305                name: "test".to_string(),
306            })
307            .expect("json")
308            .build();
309
310        assert_eq!(request.header("Content-Type"), Some("application/json"));
311        assert!(request.body().is_some());
312    }
313
314    #[test]
315    fn request_extensions() {
316        #[derive(Debug, Clone, PartialEq)]
317        struct RequestId(u64);
318
319        let url = url::Url::parse("https://api.example.com").expect("valid URL");
320        let mut request = Request::<Bytes>::builder(Method::Get, url)
321            .extension(RequestId(42))
322            .build();
323
324        // Read extension
325        assert_eq!(
326            request.extensions().get::<RequestId>(),
327            Some(&RequestId(42))
328        );
329
330        // Mutate extension
331        request.extensions_mut().insert(RequestId(100));
332        assert_eq!(
333            request.extensions().get::<RequestId>(),
334            Some(&RequestId(100))
335        );
336    }
337
338    #[test]
339    fn request_from_parts_roundtrip() {
340        #[derive(Debug, Clone, PartialEq)]
341        struct TraceId(String);
342
343        let url = url::Url::parse("https://api.example.com").expect("valid URL");
344        let request = Request::<Bytes>::builder(Method::Post, url)
345            .header("Content-Type", "application/json")
346            .body(Bytes::from(r#"{"name":"test"}"#))
347            .extension(TraceId("abc123".into()))
348            .build();
349
350        let (method, url, headers, body, extensions) = request.into_parts();
351        let reconstructed = Request::from_parts(method, url, headers, body, extensions);
352
353        assert_eq!(reconstructed.method(), Method::Post);
354        assert_eq!(
355            reconstructed.header("Content-Type"),
356            Some("application/json")
357        );
358        assert!(reconstructed.body().is_some());
359        assert_eq!(
360            reconstructed.extensions().get::<TraceId>(),
361            Some(&TraceId("abc123".into()))
362        );
363    }
364
365    #[test]
366    fn request_builder_extensions_replace() {
367        #[derive(Debug, Clone, PartialEq)]
368        struct Marker(u32);
369
370        let url = url::Url::parse("https://api.example.com").expect("valid URL");
371
372        let mut ext = Extensions::new();
373        ext.insert(Marker(99));
374
375        let request = Request::<Bytes>::builder(Method::Get, url)
376            .extension(Marker(1)) // This will be replaced
377            .extensions(ext)
378            .build();
379
380        assert_eq!(request.extensions().get::<Marker>(), Some(&Marker(99)));
381    }
382}