Skip to main content

leyline/core/request/
mod.rs

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