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}