Skip to main content

qubit_http/request/
http_request_builder.rs

1/*******************************************************************************
2 *
3 *    Copyright (c) 2025 - 2026.
4 *    Haixing Hu, Qubit Co. Ltd.
5 *
6 *    All rights reserved.
7 *
8 ******************************************************************************/
9//! Builder for [`super::http_request::HttpRequest`].
10
11use std::time::Duration;
12
13use bytes::Bytes;
14use http::header::CONTENT_TYPE;
15use http::{HeaderMap, HeaderValue, Method};
16use serde::Serialize;
17use tokio_util::sync::CancellationToken;
18use url::form_urlencoded;
19
20use crate::{HttpError, HttpResult, HttpRetryMethodPolicy};
21
22use super::http_request::HttpRequest;
23use super::http_request_body::HttpRequestBody;
24use super::http_request_retry_override::HttpRequestRetryOverride;
25use super::parse_header;
26
27/// Builder for [`HttpRequest`](super::http_request::HttpRequest).
28#[derive(Debug, Clone)]
29pub struct HttpRequestBuilder {
30    /// HTTP method (e.g. GET, POST).
31    method: Method,
32    /// Request path without the query string.
33    path: String,
34    /// Query parameters as `(key, value)` pairs, appended to the URL when built.
35    query: Vec<(String, String)>,
36    /// Request headers.
37    headers: HeaderMap,
38    /// Request body; empty if not set.
39    body: HttpRequestBody,
40    /// Per-request timeout; if unset, the client default applies.
41    request_timeout: Option<Duration>,
42    /// Optional cancellation token for this request.
43    cancellation_token: Option<CancellationToken>,
44    /// Per-request retry override for one-off retry behavior customization.
45    retry_override: HttpRequestRetryOverride,
46}
47
48impl HttpRequestBuilder {
49    /// Starts a builder with method and path; body empty, no query, no extra headers.
50    ///
51    /// # Parameters
52    /// - `method`: HTTP verb.
53    /// - `path`: URL or relative path string.
54    ///
55    /// # Returns
56    /// New [`HttpRequestBuilder`].
57    pub fn new(method: Method, path: &str) -> Self {
58        Self {
59            method,
60            path: path.to_string(),
61            query: Vec::new(),
62            headers: HeaderMap::new(),
63            body: HttpRequestBody::Empty,
64            request_timeout: None,
65            cancellation_token: None,
66            retry_override: HttpRequestRetryOverride::default(),
67        }
68    }
69
70    /// Appends a single `key=value` query pair (order preserved).
71    ///
72    /// # Parameters
73    /// - `key`: Query parameter name.
74    /// - `value`: Query parameter value.
75    ///
76    /// # Returns
77    /// `self` for chaining.
78    pub fn query_param(mut self, key: &str, value: &str) -> Self {
79        self.query.push((key.to_string(), value.to_string()));
80        self
81    }
82
83    /// Appends many query pairs via [`HttpRequestBuilder::query_param`].
84    ///
85    /// # Parameters
86    /// - `params`: Iterator of `(key, value)` pairs.
87    ///
88    /// # Returns
89    /// `self` for chaining.
90    pub fn query_params<'a, I>(mut self, params: I) -> Self
91    where
92        I: IntoIterator<Item = (&'a str, &'a str)>,
93    {
94        for (key, value) in params {
95            self = self.query_param(key, value);
96        }
97        self
98    }
99
100    /// Validates and inserts one header.
101    ///
102    /// # Parameters
103    /// - `name`: Header name (must be valid [`http::header::HeaderName`] bytes).
104    /// - `value`: Header value (must be valid [`http::header::HeaderValue`]).
105    ///
106    /// # Returns
107    /// `Ok(self)` or [`HttpError`] if name/value are invalid.
108    pub fn header(mut self, name: &str, value: &str) -> HttpResult<Self> {
109        let (header_name, header_value) = parse_header(name, value)?;
110        self.headers.insert(header_name, header_value);
111        Ok(self)
112    }
113
114    /// Merges all entries from `headers` into this builder (existing names may get extra values).
115    ///
116    /// # Parameters
117    /// - `headers`: Map to append.
118    ///
119    /// # Returns
120    /// `self` for chaining.
121    pub fn headers(mut self, headers: HeaderMap) -> Self {
122        self.headers.extend(headers);
123        self
124    }
125
126    /// Sets the body to raw bytes without changing `Content-Type` unless already set elsewhere.
127    ///
128    /// # Parameters
129    /// - `body`: Payload.
130    ///
131    /// # Returns
132    /// `self` for chaining.
133    pub fn bytes_body(mut self, body: impl Into<Bytes>) -> Self {
134        self.body = HttpRequestBody::Bytes(body.into());
135        self
136    }
137
138    /// Sets a UTF-8 text body and adds `text/plain; charset=utf-8` if `Content-Type` is absent.
139    ///
140    /// # Parameters
141    /// - `body`: Text payload.
142    ///
143    /// # Returns
144    /// `self` for chaining.
145    pub fn text_body(mut self, body: impl Into<String>) -> Self {
146        if !self.headers.contains_key(CONTENT_TYPE) {
147            self.headers.insert(
148                CONTENT_TYPE,
149                HeaderValue::from_static("text/plain; charset=utf-8"),
150            );
151        }
152        self.body = HttpRequestBody::Text(body.into());
153        self
154    }
155
156    /// Serializes `value` to JSON, sets body to those bytes, and adds `application/json` if needed.
157    ///
158    /// # Parameters
159    /// - `value`: Serializable value.
160    ///
161    /// # Returns
162    /// `Ok(self)` or [`HttpError`] if JSON encoding fails.
163    pub fn json_body<T>(mut self, value: &T) -> HttpResult<Self>
164    where
165        T: Serialize,
166    {
167        let bytes = serde_json::to_vec(value)
168            .map_err(|error| HttpError::decode(format!("Failed to encode JSON body: {}", error)))?;
169        if !self.headers.contains_key(CONTENT_TYPE) {
170            self.headers
171                .insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
172        }
173        self.body = HttpRequestBody::Json(Bytes::from(bytes));
174        Ok(self)
175    }
176
177    /// Serializes key-value pairs as `application/x-www-form-urlencoded`.
178    ///
179    /// # Parameters
180    /// - `fields`: Iterable of `(key, value)` string pairs.
181    ///
182    /// # Returns
183    /// `self` for chaining.
184    pub fn form_body<'a, I>(mut self, fields: I) -> Self
185    where
186        I: IntoIterator<Item = (&'a str, &'a str)>,
187    {
188        let mut serializer = form_urlencoded::Serializer::new(String::new());
189        for (key, value) in fields {
190            serializer.append_pair(key, value);
191        }
192        let body = serializer.finish();
193        if !self.headers.contains_key(CONTENT_TYPE) {
194            self.headers.insert(
195                CONTENT_TYPE,
196                HeaderValue::from_static("application/x-www-form-urlencoded"),
197            );
198        }
199        self.body = HttpRequestBody::Form(Bytes::from(body));
200        self
201    }
202
203    /// Sets multipart body bytes and optional auto content-type by boundary.
204    ///
205    /// # Parameters
206    /// - `body`: Multipart payload bytes.
207    /// - `boundary`: Multipart boundary used in payload framing.
208    ///
209    /// # Returns
210    /// `Ok(self)` for chaining.
211    ///
212    /// # Errors
213    /// Returns [`HttpError`] when `boundary` is empty or content-type cannot be built.
214    pub fn multipart_body(mut self, body: impl Into<Bytes>, boundary: &str) -> HttpResult<Self> {
215        if boundary.trim().is_empty() {
216            return Err(HttpError::other(
217                "Multipart boundary cannot be empty for multipart_body",
218            ));
219        }
220        if !self.headers.contains_key(CONTENT_TYPE) {
221            let value = HeaderValue::from_str(&format!("multipart/form-data; boundary={boundary}"))
222                .map_err(|error| {
223                    HttpError::other(format!(
224                        "Invalid multipart Content-Type header value: {error}"
225                    ))
226                })?;
227            self.headers.insert(CONTENT_TYPE, value);
228        }
229        self.body = HttpRequestBody::Multipart(body.into());
230        Ok(self)
231    }
232
233    /// Serializes records as NDJSON (`one JSON object per line`).
234    ///
235    /// # Parameters
236    /// - `records`: Serializable records to encode as NDJSON lines.
237    ///
238    /// # Returns
239    /// `Ok(self)` for chaining.
240    ///
241    /// # Errors
242    /// Returns [`HttpError`] when any record fails JSON serialization.
243    pub fn ndjson_body<T>(mut self, records: &[T]) -> HttpResult<Self>
244    where
245        T: Serialize,
246    {
247        let mut payload = String::new();
248        for record in records {
249            let line = serde_json::to_string(record).map_err(|error| {
250                HttpError::decode(format!("Failed to encode NDJSON record: {error}"))
251            })?;
252            payload.push_str(&line);
253            payload.push('\n');
254        }
255        if !self.headers.contains_key(CONTENT_TYPE) {
256            self.headers.insert(
257                CONTENT_TYPE,
258                HeaderValue::from_static("application/x-ndjson"),
259            );
260        }
261        self.body = HttpRequestBody::Ndjson(Bytes::from(payload));
262        Ok(self)
263    }
264
265    /// Overrides the client-wide request timeout for this request only.
266    ///
267    /// # Parameters
268    /// - `timeout`: Maximum time for the whole request (reqwest `timeout`).
269    ///
270    /// # Returns
271    /// `self` for chaining.
272    pub fn timeout(mut self, timeout: Duration) -> Self {
273        self.request_timeout = Some(timeout);
274        self
275    }
276
277    /// Binds a [`CancellationToken`] to this request.
278    ///
279    /// # Parameters
280    /// - `token`: Cancellation token checked before send and during request/stream I/O.
281    ///
282    /// # Returns
283    /// `self` for chaining.
284    pub fn cancellation_token(mut self, token: CancellationToken) -> Self {
285        self.cancellation_token = Some(token);
286        self
287    }
288
289    /// Forces retry enabled for this request even if client-level retry is disabled.
290    ///
291    /// # Returns
292    /// `self` for chaining.
293    pub fn force_retry(mut self) -> Self {
294        self.retry_override = self.retry_override.force_enable();
295        self
296    }
297
298    /// Disables retry for this request even if client-level retry is enabled.
299    ///
300    /// # Returns
301    /// `self` for chaining.
302    pub fn disable_retry(mut self) -> Self {
303        self.retry_override = self.retry_override.force_disable();
304        self
305    }
306
307    /// Overrides retryable-method policy for this request.
308    ///
309    /// # Parameters
310    /// - `policy`: Method policy to apply on this request only.
311    ///
312    /// # Returns
313    /// `self` for chaining.
314    pub fn retry_method_policy(mut self, policy: HttpRetryMethodPolicy) -> Self {
315        self.retry_override = self.retry_override.with_method_policy(policy);
316        self
317    }
318
319    /// Enables or disables honoring `Retry-After` for this request.
320    ///
321    /// # Parameters
322    /// - `enabled`: `true` to honor `Retry-After` on `429 Too Many Requests`.
323    ///
324    /// # Returns
325    /// `self` for chaining.
326    pub fn honor_retry_after(mut self, enabled: bool) -> Self {
327        self.retry_override = self.retry_override.with_honor_retry_after(enabled);
328        self
329    }
330
331    /// Consumes the builder into a frozen [`HttpRequest`].
332    ///
333    /// # Returns
334    /// Built [`HttpRequest`].
335    pub fn build(self) -> HttpRequest {
336        HttpRequest {
337            method: self.method,
338            path: self.path,
339            query: self.query,
340            headers: self.headers,
341            body: self.body,
342            request_timeout: self.request_timeout,
343            cancellation_token: self.cancellation_token,
344            retry_override: self.retry_override,
345        }
346    }
347}