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}