Skip to main content

leyline/core/request/
mod.rs

1#![forbid(unsafe_code)]
2mod compress;
3mod debug;
4mod encode;
5mod send;
6
7pub use compress::ContentEncoding;
8
9use http::{HeaderName, HeaderValue, Method};
10
11use crate::profile::{HeaderAnchor, Preset};
12
13use crate::core::body::Body;
14use crate::core::config::{ProxyConfig, RedirectPolicy, TimeoutConfig};
15use crate::core::digest::DigestAuth;
16use crate::core::error::{Error, Kind};
17use crate::core::headers::HeaderList;
18use crate::core::into_url::IntoUrl;
19#[cfg(feature = "multipart")]
20use crate::core::multipart::Form;
21use crate::core::retry::RetryPolicy;
22use crate::core::session::Session;
23
24pub trait IntoParamPair {
25    fn into_param_pair(self) -> (String, String);
26}
27
28impl<K, V> IntoParamPair for (K, V)
29where
30    K: AsRef<str>,
31    V: AsRef<str>,
32{
33    fn into_param_pair(self) -> (String, String) {
34        (self.0.as_ref().to_string(), self.1.as_ref().to_string())
35    }
36}
37
38impl<K, V> IntoParamPair for &(K, V)
39where
40    K: AsRef<str>,
41    V: AsRef<str>,
42{
43    fn into_param_pair(self) -> (String, String) {
44        (self.0.as_ref().to_string(), self.1.as_ref().to_string())
45    }
46}
47
48#[must_use = "builders are lazy: nothing happens until `.send()` / `.build()`"]
49pub struct RequestBuilder {
50    pub(super) session: Session,
51    pub(super) method: Method,
52    pub(super) url: String,
53    pub(super) preset: Option<Preset>,
54    pub(super) body: Body,
55    pub(super) headers: HeaderList,
56    pub(super) query_params: Vec<(String, String)>,
57    pub(super) timeouts: Option<TimeoutConfig>,
58    pub(super) builder_error: Option<Error>,
59    pub(super) stream_response: bool,
60    pub(super) compress: Option<ContentEncoding>,
61    pub(super) retry_policy: RetryPolicy,
62    pub(super) digest_auth: Option<DigestAuth>,
63    pub(super) proxy: Option<ProxyConfig>,
64    pub(super) header_order: Option<Vec<String>>,
65    pub(super) preset_user: bool,
66    pub(super) redirect: Option<RedirectPolicy>,
67}
68
69fn default_preset(session: &Session, method: &Method) -> Option<Preset> {
70    if !session.impersonates() {
71        return None;
72    }
73    match *method {
74        Method::GET | Method::HEAD => Some(Preset::Navigate),
75        _ => None,
76    }
77}
78
79impl RequestBuilder {
80    pub(crate) fn new(session: &Session, method: Method, url: &str) -> Self {
81        let preset = default_preset(session, &method);
82        Self {
83            session: session.clone(),
84            method,
85            url: url.to_string(),
86            preset,
87            body: Body::default(),
88            headers: HeaderList::new(),
89            query_params: Vec::new(),
90            timeouts: None,
91            builder_error: None,
92            stream_response: false,
93            compress: None,
94            retry_policy: session.default_retry().clone(),
95            digest_auth: None,
96            proxy: None,
97            header_order: None,
98            preset_user: false,
99            redirect: None,
100        }
101    }
102
103    pub(crate) fn from_url(session: &Session, method: Method, url: impl IntoUrl) -> Self {
104        match url.into_url() {
105            Ok(url) => Self::new(session, method, url.as_str()),
106            Err(err) => {
107                let mut builder = Self::new(session, method, "");
108                builder.builder_error = Some(err);
109                builder
110            }
111        }
112    }
113
114    fn fail(&mut self, err: Error) {
115        if self.builder_error.is_none() {
116            self.builder_error = Some(err);
117        }
118    }
119
120    fn infer_from_content_type(&mut self) {
121        if self.preset_user || !self.session.impersonates() {
122            return;
123        }
124        if !matches!(self.method, Method::POST | Method::PUT | Method::PATCH) {
125            return;
126        }
127        let Some(ct) = self.headers.get("content-type") else {
128            return;
129        };
130        let ct = String::from_utf8_lossy(ct.as_bytes()).to_ascii_lowercase();
131        if ct.starts_with("application/json") {
132            self.preset = Some(Preset::Xhr);
133        } else if ct.starts_with("application/x-www-form-urlencoded") {
134            self.preset = Some(Preset::Form);
135        }
136    }
137
138    pub fn timeout(mut self, config: impl Into<TimeoutConfig>) -> Self {
139        self.timeouts = Some(config.into());
140        self
141    }
142
143    pub fn preset(mut self, preset: Preset) -> Self {
144        self.preset = Some(preset);
145        self.preset_user = true;
146        self
147    }
148
149    pub fn header_order(mut self, order: &[&str]) -> Self {
150        self.header_order = Some(order.iter().map(|s| (*s).to_string()).collect());
151        self
152    }
153
154    pub fn body(mut self, body: impl Into<Body>) -> Self {
155        self.body = body.into();
156        self
157    }
158
159    pub fn json(mut self, value: &impl serde::Serialize) -> Self {
160        match serde_json::to_vec(value) {
161            Ok(bytes) => {
162                self.put("content-type", "application/json");
163                self.body = Body::from(bytes);
164            }
165            Err(e) => {
166                self.builder_error = Some(Error::new(Kind::Json).with_source(e));
167            }
168        }
169        self
170    }
171
172    pub fn form<I, P>(mut self, params: I) -> Self
173    where
174        I: IntoIterator<Item = P>,
175        P: IntoParamPair,
176    {
177        let pairs = collect_pairs(params);
178        let encoded = encode::url_encode_pairs(&pairs);
179        self.put("content-type", "application/x-www-form-urlencoded");
180        self.body = Body::from(encoded.into_bytes());
181        self
182    }
183
184    pub fn stream(mut self) -> Self {
185        self.stream_response = true;
186        self
187    }
188
189    pub fn compress(mut self, encoding: ContentEncoding) -> Self {
190        self.compress = Some(encoding);
191        self
192    }
193
194    pub fn query<I, P>(mut self, params: I) -> Self
195    where
196        I: IntoIterator<Item = P>,
197        P: IntoParamPair,
198    {
199        for pair in params {
200            self.query_params.push(pair.into_param_pair());
201        }
202        self
203    }
204
205    pub fn header(
206        mut self,
207        name: impl TryInto<HeaderName>,
208        value: impl TryInto<HeaderValue>,
209    ) -> Self {
210        if let Err(err) = self.headers.append(name, value) {
211            self.fail(err);
212        }
213        self
214    }
215
216    fn put(&mut self, name: impl TryInto<HeaderName>, value: impl TryInto<HeaderValue>) {
217        if let Err(err) = self.headers.set(name, value) {
218            self.fail(err);
219        }
220    }
221
222    pub fn headers<I, P>(mut self, headers: I) -> Self
223    where
224        I: IntoIterator<Item = P>,
225        P: IntoParamPair,
226    {
227        for pair in headers {
228            let (k, v) = pair.into_param_pair();
229            if let Err(err) = self.headers.append(k, v) {
230                self.fail(err);
231                return self;
232            }
233        }
234        self
235    }
236
237    pub fn anchored(
238        mut self,
239        anchor: HeaderAnchor,
240        name: impl TryInto<HeaderName>,
241        value: impl TryInto<HeaderValue>,
242    ) -> Self {
243        if let Err(err) = self.headers.append_anchored(anchor, name, value) {
244            self.fail(err);
245        }
246        self
247    }
248
249    pub fn bearer_auth(mut self, token: &str) -> Self {
250        self.put("authorization", format!("Bearer {token}"));
251        self
252    }
253
254    pub fn basic_auth(mut self, username: &str, password: &str) -> Self {
255        let encoded = crate::util::base64_encode(&format!("{username}:{password}"));
256        self.put("authorization", format!("Basic {encoded}"));
257        self
258    }
259
260    pub fn retry(mut self, policy: RetryPolicy) -> Self {
261        self.retry_policy = policy;
262        self
263    }
264
265    pub fn redirect(mut self, policy: RedirectPolicy) -> Self {
266        self.redirect = Some(policy);
267        self
268    }
269
270    pub fn digest_auth(mut self, auth: DigestAuth) -> Self {
271        self.digest_auth = Some(auth);
272        self
273    }
274
275    #[cfg(feature = "multipart")]
276    pub fn multipart(mut self, form: Form) -> Self {
277        self.put("content-type", form.content_type());
278        self.body = form.into_stream_body();
279        self
280    }
281
282    pub fn proxy(mut self, config: impl Into<ProxyConfig>) -> Self {
283        self.proxy = Some(config.into());
284        self
285    }
286}
287
288#[cfg(test)]
289#[path = "mod_tests.rs"]
290mod tests;
291
292fn collect_pairs<I, P>(params: I) -> Vec<(String, String)>
293where
294    I: IntoIterator<Item = P>,
295    P: IntoParamPair,
296{
297    params
298        .into_iter()
299        .map(IntoParamPair::into_param_pair)
300        .collect()
301}