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