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}