fetsig/browser/
request.rs1use 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}