1 use http::{HeaderMap, Method};
2 use js_sys::{Promise, JSON};
3 use std::{fmt, future::Future, sync::Arc};
4 use url::Url;
5 use wasm_bindgen::prelude::{wasm_bindgen, UnwrapThrowExt as _};
6 
7 use super::{Request, RequestBuilder, Response};
8 use crate::IntoUrl;
9 
10 #[wasm_bindgen]
11 extern "C" {
12     #[wasm_bindgen(js_name = fetch)]
fetch_with_request(input: &web_sys::Request) -> Promise13     fn fetch_with_request(input: &web_sys::Request) -> Promise;
14 }
15 
js_fetch(req: &web_sys::Request) -> Promise16 fn js_fetch(req: &web_sys::Request) -> Promise {
17     use wasm_bindgen::{JsCast, JsValue};
18     let global = js_sys::global();
19 
20     if let Ok(true) = js_sys::Reflect::has(&global, &JsValue::from_str("ServiceWorkerGlobalScope"))
21     {
22         global
23             .unchecked_into::<web_sys::ServiceWorkerGlobalScope>()
24             .fetch_with_request(req)
25     } else {
26         // browser
27         fetch_with_request(req)
28     }
29 }
30 
31 /// dox
32 #[derive(Clone)]
33 pub struct Client {
34     config: Arc<Config>,
35 }
36 
37 /// dox
38 pub struct ClientBuilder {
39     config: Config,
40 }
41 
42 impl Client {
43     /// dox
new() -> Self44     pub fn new() -> Self {
45         Client::builder().build().unwrap_throw()
46     }
47 
48     /// dox
builder() -> ClientBuilder49     pub fn builder() -> ClientBuilder {
50         ClientBuilder::new()
51     }
52 
53     /// Convenience method to make a `GET` request to a URL.
54     ///
55     /// # Errors
56     ///
57     /// This method fails whenever supplied `Url` cannot be parsed.
get<U: IntoUrl>(&self, url: U) -> RequestBuilder58     pub fn get<U: IntoUrl>(&self, url: U) -> RequestBuilder {
59         self.request(Method::GET, url)
60     }
61 
62     /// Convenience method to make a `POST` request to a URL.
63     ///
64     /// # Errors
65     ///
66     /// This method fails whenever supplied `Url` cannot be parsed.
post<U: IntoUrl>(&self, url: U) -> RequestBuilder67     pub fn post<U: IntoUrl>(&self, url: U) -> RequestBuilder {
68         self.request(Method::POST, url)
69     }
70 
71     /// Convenience method to make a `PUT` request to a URL.
72     ///
73     /// # Errors
74     ///
75     /// This method fails whenever supplied `Url` cannot be parsed.
put<U: IntoUrl>(&self, url: U) -> RequestBuilder76     pub fn put<U: IntoUrl>(&self, url: U) -> RequestBuilder {
77         self.request(Method::PUT, url)
78     }
79 
80     /// Convenience method to make a `PATCH` request to a URL.
81     ///
82     /// # Errors
83     ///
84     /// This method fails whenever supplied `Url` cannot be parsed.
patch<U: IntoUrl>(&self, url: U) -> RequestBuilder85     pub fn patch<U: IntoUrl>(&self, url: U) -> RequestBuilder {
86         self.request(Method::PATCH, url)
87     }
88 
89     /// Convenience method to make a `DELETE` request to a URL.
90     ///
91     /// # Errors
92     ///
93     /// This method fails whenever supplied `Url` cannot be parsed.
delete<U: IntoUrl>(&self, url: U) -> RequestBuilder94     pub fn delete<U: IntoUrl>(&self, url: U) -> RequestBuilder {
95         self.request(Method::DELETE, url)
96     }
97 
98     /// Convenience method to make a `HEAD` request to a URL.
99     ///
100     /// # Errors
101     ///
102     /// This method fails whenever supplied `Url` cannot be parsed.
head<U: IntoUrl>(&self, url: U) -> RequestBuilder103     pub fn head<U: IntoUrl>(&self, url: U) -> RequestBuilder {
104         self.request(Method::HEAD, url)
105     }
106 
107     /// Start building a `Request` with the `Method` and `Url`.
108     ///
109     /// Returns a `RequestBuilder`, which will allow setting headers and
110     /// request body before sending.
111     ///
112     /// # Errors
113     ///
114     /// This method fails whenever supplied `Url` cannot be parsed.
request<U: IntoUrl>(&self, method: Method, url: U) -> RequestBuilder115     pub fn request<U: IntoUrl>(&self, method: Method, url: U) -> RequestBuilder {
116         let req = url.into_url().map(move |url| Request::new(method, url));
117         RequestBuilder::new(self.clone(), req)
118     }
119 
120     /// Executes a `Request`.
121     ///
122     /// A `Request` can be built manually with `Request::new()` or obtained
123     /// from a RequestBuilder with `RequestBuilder::build()`.
124     ///
125     /// You should prefer to use the `RequestBuilder` and
126     /// `RequestBuilder::send()`.
127     ///
128     /// # Errors
129     ///
130     /// This method fails if there was an error while sending request,
131     /// redirect loop was detected or redirect limit was exhausted.
execute( &self, request: Request, ) -> impl Future<Output = Result<Response, crate::Error>>132     pub fn execute(
133         &self,
134         request: Request,
135     ) -> impl Future<Output = Result<Response, crate::Error>> {
136         self.execute_request(request)
137     }
138 
139     // merge request headers with Client default_headers, prior to external http fetch
merge_headers(&self, req: &mut Request)140     fn merge_headers(&self, req: &mut Request) {
141         use http::header::Entry;
142         let headers: &mut HeaderMap = req.headers_mut();
143         // insert default headers in the request headers
144         // without overwriting already appended headers.
145         for (key, value) in self.config.headers.iter() {
146             if let Entry::Vacant(entry) = headers.entry(key) {
147                 entry.insert(value.clone());
148             }
149         }
150     }
151 
execute_request( &self, mut req: Request, ) -> impl Future<Output = crate::Result<Response>>152     pub(super) fn execute_request(
153         &self,
154         mut req: Request,
155     ) -> impl Future<Output = crate::Result<Response>> {
156         self.merge_headers(&mut req);
157         fetch(req)
158     }
159 }
160 
161 impl Default for Client {
default() -> Self162     fn default() -> Self {
163         Self::new()
164     }
165 }
166 
167 impl fmt::Debug for Client {
fmt(&self, f: &mut fmt::Formatter) -> fmt::Result168     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
169         let mut builder = f.debug_struct("Client");
170         self.config.fmt_fields(&mut builder);
171         builder.finish()
172     }
173 }
174 
175 impl fmt::Debug for ClientBuilder {
fmt(&self, f: &mut fmt::Formatter) -> fmt::Result176     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
177         let mut builder = f.debug_struct("ClientBuilder");
178         self.config.fmt_fields(&mut builder);
179         builder.finish()
180     }
181 }
182 
fetch(req: Request) -> crate::Result<Response>183 async fn fetch(req: Request) -> crate::Result<Response> {
184     // Build the js Request
185     let mut init = web_sys::RequestInit::new();
186     init.method(req.method().as_str());
187 
188     // convert HeaderMap to Headers
189     let js_headers = web_sys::Headers::new()
190         .map_err(crate::error::wasm)
191         .map_err(crate::error::builder)?;
192 
193     for (name, value) in req.headers() {
194         js_headers
195             .append(
196                 name.as_str(),
197                 value.to_str().map_err(crate::error::builder)?,
198             )
199             .map_err(crate::error::wasm)
200             .map_err(crate::error::builder)?;
201     }
202     init.headers(&js_headers.into());
203 
204     // When req.cors is true, do nothing because the default mode is 'cors'
205     if !req.cors {
206         init.mode(web_sys::RequestMode::NoCors);
207     }
208 
209     if let Some(creds) = req.credentials {
210         init.credentials(creds);
211     }
212 
213     if let Some(body) = req.body() {
214         if !body.is_empty() {
215             init.body(Some(body.to_js_value()?.as_ref()));
216         }
217     }
218 
219     let js_req = web_sys::Request::new_with_str_and_init(req.url().as_str(), &init)
220         .map_err(crate::error::wasm)
221         .map_err(crate::error::builder)?;
222 
223     // Await the fetch() promise
224     let p = js_fetch(&js_req);
225     let js_resp = super::promise::<web_sys::Response>(p)
226         .await
227         .map_err(crate::error::request)?;
228 
229     // Convert from the js Response
230     let mut resp = http::Response::builder().status(js_resp.status());
231 
232     let url = Url::parse(&js_resp.url()).expect_throw("url parse");
233 
234     let js_headers = js_resp.headers();
235     let js_iter = js_sys::try_iter(&js_headers)
236         .expect_throw("headers try_iter")
237         .expect_throw("headers have an iterator");
238 
239     for item in js_iter {
240         let item = item.expect_throw("headers iterator doesn't throw");
241         let serialized_headers: String = JSON::stringify(&item)
242             .expect_throw("serialized headers")
243             .into();
244         let [name, value]: [String; 2] = serde_json::from_str(&serialized_headers)
245             .expect_throw("deserializable serialized headers");
246         resp = resp.header(&name, &value);
247     }
248 
249     resp.body(js_resp)
250         .map(|resp| Response::new(resp, url))
251         .map_err(crate::error::request)
252 }
253 
254 // ===== impl ClientBuilder =====
255 
256 impl ClientBuilder {
257     /// dox
new() -> Self258     pub fn new() -> Self {
259         ClientBuilder {
260             config: Config::default(),
261         }
262     }
263 
264     /// Returns a 'Client' that uses this ClientBuilder configuration
build(mut self) -> Result<Client, crate::Error>265     pub fn build(mut self) -> Result<Client, crate::Error> {
266         let config = std::mem::take(&mut self.config);
267         Ok(Client {
268             config: Arc::new(config),
269         })
270     }
271 
272     /// Sets the default headers for every request
default_headers(mut self, headers: HeaderMap) -> ClientBuilder273     pub fn default_headers(mut self, headers: HeaderMap) -> ClientBuilder {
274         for (key, value) in headers.iter() {
275             self.config.headers.insert(key, value.clone());
276         }
277         self
278     }
279 }
280 
281 impl Default for ClientBuilder {
default() -> Self282     fn default() -> Self {
283         Self::new()
284     }
285 }
286 
287 #[derive(Clone, Debug)]
288 struct Config {
289     headers: HeaderMap,
290 }
291 
292 impl Default for Config {
default() -> Config293     fn default() -> Config {
294         Config {
295             headers: HeaderMap::new(),
296         }
297     }
298 }
299 
300 impl Config {
fmt_fields(&self, f: &mut fmt::DebugStruct<'_, '_>)301     fn fmt_fields(&self, f: &mut fmt::DebugStruct<'_, '_>) {
302         f.field("default_headers", &self.headers);
303     }
304 }
305 
306 #[cfg(test)]
307 mod tests {
308     use wasm_bindgen_test::*;
309 
310     wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
311 
312     #[wasm_bindgen_test]
default_headers()313     async fn default_headers() {
314         use crate::header::{HeaderMap, HeaderValue, CONTENT_TYPE};
315 
316         let mut headers = HeaderMap::new();
317         headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
318         headers.insert("x-custom", HeaderValue::from_static("flibbertigibbet"));
319         let client = crate::Client::builder()
320             .default_headers(headers)
321             .build()
322             .expect("client");
323         let mut req = client
324             .get("https://www.example.com")
325             .build()
326             .expect("request");
327         // merge headers as if client were about to issue fetch
328         client.merge_headers(&mut req);
329 
330         let test_headers = req.headers();
331         assert!(test_headers.get(CONTENT_TYPE).is_some(), "content-type");
332         assert!(test_headers.get("x-custom").is_some(), "custom header");
333         assert!(test_headers.get("accept").is_none(), "no accept header");
334     }
335 
336     #[wasm_bindgen_test]
default_headers_clone()337     async fn default_headers_clone() {
338         use crate::header::{HeaderMap, HeaderValue, CONTENT_TYPE};
339 
340         let mut headers = HeaderMap::new();
341         headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
342         headers.insert("x-custom", HeaderValue::from_static("flibbertigibbet"));
343         let client = crate::Client::builder()
344             .default_headers(headers)
345             .build()
346             .expect("client");
347 
348         let mut req = client
349             .get("https://www.example.com")
350             .header(CONTENT_TYPE, "text/plain")
351             .build()
352             .expect("request");
353         client.merge_headers(&mut req);
354         let headers1 = req.headers();
355 
356         // confirm that request headers override defaults
357         assert_eq!(
358             headers1.get(CONTENT_TYPE).unwrap(),
359             "text/plain",
360             "request headers override defaults"
361         );
362 
363         // confirm that request headers don't change client defaults
364         let mut req2 = client
365             .get("https://www.example.com/x")
366             .build()
367             .expect("req 2");
368         client.merge_headers(&mut req2);
369         let headers2 = req2.headers();
370         assert_eq!(
371             headers2.get(CONTENT_TYPE).unwrap(),
372             "application/json",
373             "request headers don't change client defaults"
374         );
375     }
376 }
377