Skip to main content

better_fetch/
request.rs

1use std::collections::HashMap;
2use std::time::Duration;
3
4use bytes::Bytes;
5use http::{HeaderMap, Method};
6use indexmap::IndexMap;
7
8use crate::auth::Auth;
9use crate::backend::HttpBody;
10use crate::cancel::CancellationToken;
11use crate::client::Client;
12use crate::error::Error;
13use crate::response::Response;
14use crate::retry::RetryPolicy;
15use crate::url_build::QueryValue;
16use crate::Result;
17
18#[cfg(feature = "json")]
19use crate::json_parser::JsonParserFn;
20
21/// Fluent builder for a single HTTP request.
22pub struct RequestBuilder<'a> {
23    pub(crate) client: &'a Client,
24    pub(crate) method: Method,
25    pub(crate) path: String,
26    pub(crate) params: HashMap<String, String>,
27    pub(crate) query: IndexMap<String, QueryValue>,
28    pub(crate) headers: HeaderMap,
29    pub(crate) body: HttpBody,
30    #[cfg(feature = "multipart")]
31    pub(crate) multipart: Option<crate::multipart::Form>,
32    pub(crate) timeout: Option<Duration>,
33    pub(crate) retry: Option<RetryPolicy>,
34    pub(crate) auth: Option<Auth>,
35    pub(crate) cancellation: Option<CancellationToken>,
36    pub(crate) throw_on_error: bool,
37    #[cfg(feature = "json")]
38    pub(crate) json_parser: Option<JsonParserFn>,
39    #[cfg(feature = "validate")]
40    pub(crate) validate_response: bool,
41}
42
43impl<'a> RequestBuilder<'a> {
44    pub fn param(mut self, key: impl Into<String>, value: impl ToString) -> Self {
45        self.params.insert(key.into(), value.to_string());
46        self
47    }
48
49    pub fn params(mut self, params: HashMap<String, String>) -> Self {
50        self.params.extend(params);
51        self
52    }
53
54    pub fn params_iter(
55        mut self,
56        params: impl IntoIterator<Item = (impl Into<String>, impl ToString)>,
57    ) -> Self {
58        for (k, v) in params {
59            self.params.insert(k.into(), v.to_string());
60        }
61        self
62    }
63
64    pub fn query(mut self, key: impl Into<String>, value: impl ToString) -> Self {
65        self.query
66            .insert(key.into(), QueryValue::Scalar(value.to_string()));
67        self
68    }
69
70    /// Sets multiple query parameters preserving insertion order.
71    pub fn queries(mut self, query: IndexMap<String, QueryValue>) -> Self {
72        for (k, v) in query {
73            self.query.insert(k, v);
74        }
75        self
76    }
77
78    #[cfg(feature = "json")]
79    pub fn query_json<T: serde::Serialize>(
80        mut self,
81        key: impl Into<String>,
82        value: &T,
83    ) -> Result<Self> {
84        self.query
85            .insert(key.into(), QueryValue::from_serializable(value)?);
86        Ok(self)
87    }
88
89    pub fn header(mut self, key: impl AsRef<str>, value: impl AsRef<str>) -> Result<Self> {
90        let name = http::HeaderName::from_bytes(key.as_ref().as_bytes())
91            .map_err(|e| Error::Other(format!("invalid header name: {e}")))?;
92        let value = http::HeaderValue::from_str(value.as_ref())
93            .map_err(|e| Error::Other(format!("invalid header value: {e}")))?;
94        self.headers.insert(name, value);
95        Ok(self)
96    }
97
98    #[cfg(feature = "json")]
99    pub fn json<T: serde::Serialize>(mut self, body: &T) -> Result<Self> {
100        let bytes = serde_json::to_vec(body).map_err(|e| Error::Other(e.to_string()))?;
101        self.body = HttpBody::Bytes(Bytes::from(bytes));
102        if !self.headers.contains_key(http::header::CONTENT_TYPE) {
103            self.headers.insert(
104                http::header::CONTENT_TYPE,
105                http::HeaderValue::from_static("application/json"),
106            );
107        }
108        Ok(self)
109    }
110
111    pub fn body(mut self, body: impl Into<Bytes>) -> Self {
112        self.body = HttpBody::Bytes(body.into());
113        self
114    }
115
116    /// URL-encoded form body (`application/x-www-form-urlencoded`).
117    pub fn form<I, K, V>(mut self, fields: I) -> Self
118    where
119        I: IntoIterator<Item = (K, V)>,
120        K: AsRef<str>,
121        V: AsRef<str>,
122    {
123        let mut serializer = url::form_urlencoded::Serializer::new(String::new());
124        for (k, v) in fields {
125            serializer.append_pair(k.as_ref(), v.as_ref());
126        }
127        self.body = HttpBody::Bytes(Bytes::from(serializer.finish()));
128        if !self.headers.contains_key(http::header::CONTENT_TYPE) {
129            self.headers.insert(
130                http::header::CONTENT_TYPE,
131                http::HeaderValue::from_static("application/x-www-form-urlencoded"),
132            );
133        }
134        self
135    }
136
137    /// Multipart form body (requires the `multipart` feature).
138    #[cfg(feature = "multipart")]
139    pub fn multipart(mut self, form: crate::multipart::Form) -> Self {
140        self.multipart = Some(form);
141        self.body = HttpBody::Empty;
142        self
143    }
144
145    pub fn timeout(mut self, timeout: Duration) -> Self {
146        self.timeout = Some(timeout);
147        self
148    }
149
150    pub fn retry(mut self, policy: RetryPolicy) -> Self {
151        self.retry = Some(policy);
152        self
153    }
154
155    pub fn auth(mut self, auth: Auth) -> Self {
156        self.auth = Some(auth);
157        self
158    }
159
160    pub fn bearer_token(mut self, token: impl Into<String>) -> Self {
161        self.auth = Some(Auth::bearer(token));
162        self
163    }
164
165    /// Cancels the in-flight request and retry sleeps when this token is triggered.
166    pub fn cancellation_token(mut self, token: CancellationToken) -> Self {
167        self.cancellation = Some(token);
168        self
169    }
170
171    /// When `true`, [`send`](Self::send) returns `Err` on non-2xx HTTP status (like upstream `throw: true`).
172    pub fn throw_on_error(mut self, throw: bool) -> Self {
173        self.throw_on_error = throw;
174        self
175    }
176
177    /// Overrides the client's JSON parser for this request only.
178    ///
179    /// See [`crate::json_parser`] for fast path vs two-step parsing.
180    #[cfg(feature = "json")]
181    pub fn json_parser<F>(mut self, f: F) -> Self
182    where
183        F: Fn(&Bytes) -> std::result::Result<serde_json::Value, String> + Send + Sync + 'static,
184    {
185        self.json_parser = Some(crate::json_parser::json_parser(f));
186        self
187    }
188
189    /// Overrides the client's JSON parser for this request only.
190    #[cfg(feature = "json")]
191    pub fn json_parser_fn(mut self, parser: JsonParserFn) -> Self {
192        self.json_parser = Some(parser);
193        self
194    }
195
196    pub async fn send(self) -> Result<Response> {
197        self.client.execute(self).await
198    }
199
200    #[cfg(feature = "json")]
201    #[must_use = "send the request with `.await` and handle the result"]
202    pub async fn send_json<T: serde::de::DeserializeOwned>(self) -> Result<T> {
203        self.send().await?.json::<T>().await
204    }
205
206    /// When `false`, [`send_json_validated`](Self::send_json_validated) only deserializes (no garde).
207    #[cfg(feature = "validate")]
208    pub fn validate_response(mut self, validate: bool) -> Self {
209        self.validate_response = validate;
210        self
211    }
212
213    /// `send` + [`Response::json_validated`](crate::Response::json_validated).
214    #[cfg(feature = "validate")]
215    pub async fn send_json_validated<T>(self) -> Result<T>
216    where
217        T: serde::de::DeserializeOwned + garde::Validate,
218        T::Context: Default,
219    {
220        if !self.validate_response {
221            return self.send_json().await;
222        }
223        self.send().await?.json_validated().await
224    }
225}