Skip to main content

fetsig/browser/
request.rs

1use std::time::Duration;
2
3use js_sys::Uint8Array;
4use log::warn;
5use smol_str::{SmolStr, ToSmolStr};
6use wasm_bindgen::JsValue;
7use wasm_bindgen_futures::JsFuture;
8use web_sys::{Headers, RequestInit};
9
10use crate::{HEADER_ACCEPT, HEADER_CONTENT_TYPE, HEADER_WANTS_RESPONSE, MediaType};
11
12use super::{
13    common::{Abort, PendingFetch},
14    file::File,
15    js_error,
16};
17
18pub enum Method {
19    Head,
20    Get,
21    Post,
22    Put,
23    Delete,
24    Options,
25}
26
27impl Method {
28    pub fn as_str(&self) -> &'static str {
29        match self {
30            Self::Head => "Head",
31            Self::Get => "Get",
32            Self::Post => "Post",
33            Self::Put => "Put",
34            Self::Delete => "Delete",
35            Self::Options => "Options",
36        }
37    }
38
39    pub fn is_load(&self) -> bool {
40        matches!(self, Self::Head | Self::Get | Self::Options)
41    }
42}
43
44pub struct Request<'a> {
45    logging: bool,
46    method: Method,
47    is_load: bool,
48    url: &'a str,
49    headers: Option<Vec<(&'static str, SmolStr)>>,
50    media_type: Option<MediaType>,
51    body: Option<Body>,
52    wants_response: bool,
53    timeout: Option<Duration>,
54}
55
56enum Body {
57    Bytes(Vec<u8>),
58    File(File),
59}
60
61impl<'a> Request<'a> {
62    pub fn new(url: &'a str) -> Self {
63        Self {
64            logging: true,
65            method: Method::Get,
66            is_load: true,
67            url,
68            headers: None,
69            media_type: None,
70            body: None,
71            wants_response: false,
72            timeout: Some(Duration::from_secs(5)),
73        }
74    }
75
76    #[must_use]
77    pub fn with_logging(mut self, logging: bool) -> Self {
78        self.logging = logging;
79        self
80    }
81
82    #[must_use]
83    pub fn with_method(mut self, method: Method) -> Self {
84        self.method = method;
85        self
86    }
87
88    #[must_use]
89    pub fn with_header(mut self, name: &'static str, value: impl ToSmolStr) -> Self {
90        let mut headers = self.headers.take().unwrap_or_default();
91        headers.retain(|(header, _)| *header != name);
92        headers.push((name, value.to_smolstr()));
93        self.headers = Some(headers);
94        self
95    }
96
97    #[must_use]
98    pub fn with_headers(mut self, headers: Option<Vec<(&'static str, SmolStr)>>) -> Self {
99        if let Some(new_headers) = headers {
100            let mut headers = self.headers.take().unwrap_or_default();
101            for new_header in new_headers {
102                headers.retain(|(header, _)| *header != new_header.0);
103                headers.push((new_header.0, new_header.1));
104            }
105            self.headers = Some(headers);
106        }
107        self
108    }
109
110    #[must_use]
111    pub fn with_media_type(mut self, media_type: MediaType) -> Self {
112        self.media_type = Some(media_type);
113        self.with_header(HEADER_CONTENT_TYPE, media_type)
114    }
115
116    #[must_use]
117    pub fn with_body(mut self, body: Vec<u8>) -> Self {
118        self.body = Some(Body::Bytes(body));
119        self
120    }
121
122    #[must_use]
123    pub fn with_file(mut self, file: File) -> Self {
124        self.body = Some(Body::File(file));
125        self
126    }
127
128    #[must_use]
129    pub fn with_is_load(mut self, is_load: bool) -> Self {
130        self.is_load = is_load;
131        self
132    }
133
134    #[must_use]
135    pub fn with_timeout(mut self, timeout: Option<Duration>) -> Self {
136        self.timeout = timeout;
137        self
138    }
139
140    #[must_use]
141    pub fn encoding(mut self, media_type: impl Into<MediaType>) -> Self {
142        let media_type = media_type.into();
143        let media_type = match media_type {
144            #[cfg(feature = "json")]
145            MediaType::Json => MediaType::Json,
146            #[cfg(feature = "postcard")]
147            MediaType::Postcard => MediaType::Postcard,
148            _ => {
149                warn!(
150                    "Unsupported media type '{media_type}' used, degrading to 'application/json'",
151                );
152                MediaType::Json
153            }
154        };
155        self.wants_response = false;
156        self.with_media_type(media_type)
157            .with_header(HEADER_ACCEPT, media_type)
158    }
159
160    #[must_use]
161    pub fn encoding_with_response(mut self, media_type: impl Into<MediaType>) -> Self {
162        let media_type = media_type.into();
163        let media_type = match media_type {
164            #[cfg(feature = "json")]
165            MediaType::Json => MediaType::Json,
166            #[cfg(feature = "postcard")]
167            MediaType::Postcard => MediaType::Postcard,
168            _ => {
169                warn!(
170                    "Unsupported media type '{media_type}' used, degrading to 'application/json'",
171                );
172                MediaType::Json
173            }
174        };
175        self.wants_response = true;
176        self.with_media_type(media_type)
177            .with_header(HEADER_ACCEPT, media_type)
178            .with_header(HEADER_WANTS_RESPONSE, "1")
179    }
180
181    #[cfg(feature = "json")]
182    #[inline]
183    #[must_use]
184    pub fn json(self) -> Self {
185        self.encoding(MediaType::Json)
186    }
187
188    #[cfg(feature = "json")]
189    #[inline]
190    #[must_use]
191    pub fn json_with_response(self) -> Self {
192        self.encoding_with_response(MediaType::Json)
193    }
194
195    #[cfg(feature = "postcard")]
196    #[inline]
197    #[must_use]
198    pub fn postcard(self) -> Self {
199        self.encoding(MediaType::Postcard)
200    }
201
202    #[cfg(feature = "postcard")]
203    #[inline]
204    #[must_use]
205    pub fn postcard_with_response(self) -> Self {
206        self.encoding_with_response(MediaType::Postcard)
207    }
208
209    #[must_use]
210    pub fn create(self) -> Self {
211        self.with_method(Method::Post)
212    }
213
214    #[must_use]
215    pub fn retrieve(self) -> Self {
216        self.with_method(Method::Get)
217    }
218
219    #[must_use]
220    pub fn update(self) -> Self {
221        self.with_method(Method::Put)
222    }
223
224    #[must_use]
225    pub fn delete(self) -> Self {
226        self.with_method(Method::Delete)
227    }
228
229    #[must_use]
230    pub fn execute(self) -> Self {
231        self.with_method(Method::Post)
232    }
233
234    pub fn logging(&self) -> bool {
235        self.logging
236    }
237
238    pub fn method(&self) -> &Method {
239        &self.method
240    }
241
242    pub fn is_load(&self) -> bool {
243        self.is_load
244    }
245
246    pub fn url(&self) -> &str {
247        self.url
248    }
249
250    pub fn media_type(&self) -> Option<MediaType> {
251        self.media_type
252    }
253
254    pub fn headers(&self) -> Option<&[(&'static str, SmolStr)]> {
255        self.headers.as_deref()
256    }
257
258    pub fn wants_response(&self) -> bool {
259        self.wants_response
260    }
261
262    pub(crate) fn start(&self) -> Result<PendingFetch, SmolStr> {
263        let request_init = RequestInit::new();
264        request_init.set_method(match &self.method {
265            Method::Head => "HEAD",
266            Method::Get => "GET",
267            Method::Post => "POST",
268            Method::Put => "PUT",
269            Method::Delete => "DELETE",
270            Method::Options => "OPTIONS",
271        });
272
273        let headers: Headers = self.try_into()?;
274        request_init.set_headers(&headers);
275
276        if let Some(body) = &self.body {
277            let value = match body {
278                Body::Bytes(bytes) => {
279                    let array: Uint8Array = bytes.as_slice().into();
280                    JsValue::from(array)
281                }
282                Body::File(file) => JsValue::from(web_sys::File::from(file.clone())),
283            };
284            request_init.set_body(&value);
285        }
286
287        let abort = Abort::new()?;
288        request_init.set_signal(Some(&abort.signal()));
289
290        let promise = web_sys::window()
291            .expect("window")
292            .fetch_with_str_and_init(self.url(), &request_init);
293        Ok(PendingFetch::new(
294            self.url(),
295            abort,
296            self.timeout,
297            JsFuture::from(promise),
298        ))
299    }
300}
301
302impl TryFrom<&Request<'_>> for Headers {
303    type Error = SmolStr;
304
305    fn try_from(request: &Request) -> Result<Self, Self::Error> {
306        let output = Headers::new().map_err(js_error)?;
307        if let Some(headers) = request.headers() {
308            for (name, value) in headers {
309                output.set(name, value).map_err(js_error)?;
310            }
311        }
312        Ok(output)
313    }
314}