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