yew_stdweb/services/fetch/std_web.rs
1//! `stdweb` implementation for the fetch service.
2
3use super::Referrer;
4use crate::callback::Callback;
5use crate::format::{Binary, Format, Text};
6use crate::services::Task;
7use serde::Serialize;
8use std::collections::HashMap;
9use std::fmt;
10use stdweb::serde::Serde;
11use stdweb::unstable::{TryFrom, TryInto};
12use stdweb::web::error::Error;
13use stdweb::web::ArrayBuffer;
14use stdweb::{JsSerialize, Value};
15#[allow(unused_imports)]
16use stdweb::{_js_impl, js};
17use thiserror::Error;
18
19#[doc(no_inline)]
20pub use http::{HeaderMap, Method, Request, Response, StatusCode, Uri};
21
22/// Type to set cache for fetch.
23#[derive(Serialize, Debug)]
24#[serde(rename_all = "kebab-case")]
25pub enum Cache {
26 /// `default` value of cache.
27 #[serde(rename = "default")]
28 DefaultCache,
29 /// `no-store` value of cache.
30 NoStore,
31 /// `reload` value of cache.
32 Reload,
33 /// `no-cache` value of cache.
34 NoCache,
35 /// `force-cache` value of cache
36 ForceCache,
37 /// `only-if-cached` value of cache
38 OnlyIfCached,
39}
40
41/// Type to set credentials for fetch.
42#[derive(Serialize, Debug)]
43#[serde(rename_all = "kebab-case")]
44pub enum Credentials {
45 /// `omit` value of credentials.
46 Omit,
47 /// `include` value of credentials.
48 Include,
49 /// `same-origin` value of credentials.
50 SameOrigin,
51}
52
53/// Type to set mode for fetch.
54#[derive(Serialize, Debug)]
55#[serde(rename_all = "kebab-case")]
56pub enum Mode {
57 /// `same-origin` value of mode.
58 SameOrigin,
59 /// `no-cors` value of mode.
60 NoCors,
61 /// `cors` value of mode.
62 Cors,
63}
64
65/// Type to set redirect behaviour for fetch.
66#[derive(Serialize, Debug)]
67#[serde(rename_all = "kebab-case")]
68pub enum Redirect {
69 /// `follow` value of redirect.
70 Follow,
71 /// `error` value of redirect.
72 Error,
73 /// `manual` value of redirect.
74 Manual,
75}
76
77impl Serialize for Referrer {
78 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
79 where
80 S: serde::Serializer,
81 {
82 match *self {
83 Referrer::SameOriginUrl(ref s) => serializer.serialize_str(s),
84 Referrer::AboutClient => {
85 serializer.serialize_unit_variant("Referrer", 0, "about:client")
86 }
87 Referrer::Empty => serializer.serialize_unit_variant("Referrer", 1, ""),
88 }
89 }
90}
91
92/// Type to set referrer policy for fetch.
93#[derive(Serialize, Debug)]
94#[serde(rename_all = "kebab-case")]
95pub enum ReferrerPolicy {
96 /// `no-referrer` value of referrerPolicy.
97 NoReferrer,
98 /// `no-referrer-when-downgrade` value of referrerPolicy.
99 NoReferrerWhenDowngrade,
100 /// `same-origin` value of referrerPolicy.
101 SameOrigin,
102 /// `origin` value of referrerPolicy.
103 Origin,
104 /// `strict-origin` value of referrerPolicy.
105 StrictOrigin,
106 /// `origin-when-cross-origin` value of referrerPolicy.
107 OriginWhenCrossOrigin,
108 /// `strict-origin-when-cross-origin` value of referrerPolicy.
109 StrictOriginWhenCrossOrigin,
110 /// `unsafe-url` value of referrerPolicy.
111 UnsafeUrl,
112}
113
114/// Init options for `fetch()` function call.
115/// https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch
116#[derive(Serialize, Default, Debug)]
117#[serde(rename_all = "camelCase")]
118pub struct FetchOptions {
119 /// Cache of a fetch request.
120 #[serde(skip_serializing_if = "Option::is_none")]
121 pub cache: Option<Cache>,
122 /// Credentials of a fetch request.
123 #[serde(skip_serializing_if = "Option::is_none")]
124 pub credentials: Option<Credentials>,
125 /// Redirect behaviour of a fetch request.
126 #[serde(skip_serializing_if = "Option::is_none")]
127 pub redirect: Option<Redirect>,
128 /// Request mode of a fetch request.
129 #[serde(skip_serializing_if = "Option::is_none")]
130 pub mode: Option<Mode>,
131 /// Referrer of a fetch request.
132 #[serde(skip_serializing_if = "Option::is_none")]
133 pub referrer: Option<Referrer>,
134 /// Referrer policy of a fetch request.
135 #[serde(skip_serializing_if = "Option::is_none")]
136 pub referrer_policy: Option<ReferrerPolicy>,
137 /// Integrity of a fetch request.
138 #[serde(skip_serializing_if = "Option::is_none")]
139 pub integrity: Option<String>,
140}
141
142/// Represents errors of a fetch service.
143#[derive(Debug, Error)]
144enum FetchError {
145 #[error("failed response")]
146 FailedResponse,
147}
148
149/// A handle to control sent requests. Can be canceled with a `Task::cancel` call.
150#[must_use = "the request will be cancelled when the task is dropped"]
151pub struct FetchTask(Option<Value>);
152
153impl fmt::Debug for FetchTask {
154 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155 f.write_str("FetchTask")
156 }
157}
158
159/// A service to fetch resources.
160#[derive(Default, Debug)]
161pub struct FetchService {}
162
163impl FetchService {
164 /// Sends a request to a remote server given a Request object and a callback
165 /// function to convert a Response object into a loop's message.
166 ///
167 /// You may use a Request builder to build your request declaratively as on the
168 /// following examples:
169 ///
170 /// ```
171 ///# use yew::format::{Nothing, Json};
172 ///# use yew::services::fetch::Request;
173 ///# use serde_json::json;
174 /// let post_request = Request::post("https://my.api/v1/resource")
175 /// .header("Content-Type", "application/json")
176 /// .body(Json(&json!({"foo": "bar"})))
177 /// .expect("Failed to build request.");
178 ///
179 /// let get_request = Request::get("https://my.api/v1/resource")
180 /// .body(Nothing)
181 /// .expect("Failed to build request.");
182 /// ```
183 ///
184 /// The callback function can build a loop message by passing or analyzing the
185 /// response body and metadata.
186 ///
187 /// ```
188 ///# use yew::{Component, ComponentLink, Html};
189 ///# use yew::services::FetchService;
190 ///# use yew::services::fetch::{Response, Request};
191 ///# struct Comp;
192 ///# impl Component for Comp {
193 ///# type Message = Msg;type Properties = ();
194 ///# fn create(props: Self::Properties,link: ComponentLink<Self>) -> Self {unimplemented!()}
195 ///# fn update(&mut self,msg: Self::Message) -> bool {unimplemented!()}
196 ///# fn change(&mut self, _: Self::Properties) -> bool {unimplemented!()}
197 ///# fn view(&self) -> Html {unimplemented!()}
198 ///# }
199 ///# enum Msg {
200 ///# Noop,
201 ///# Error
202 ///# }
203 ///# fn dont_execute() {
204 ///# let link: ComponentLink<Comp> = unimplemented!();
205 ///# let post_request: Request<Result<String, anyhow::Error>> = unimplemented!();
206 /// let task = FetchService::fetch(
207 /// post_request,
208 /// link.callback(|response: Response<Result<String, anyhow::Error>>| {
209 /// if response.status().is_success() {
210 /// Msg::Noop
211 /// } else {
212 /// Msg::Error
213 /// }
214 /// }),
215 /// );
216 ///# }
217 /// ```
218 ///
219 /// For a full example, you can specify that the response must be in the JSON format,
220 /// and be a specific serialized data type. If the mesage isn't Json, or isn't the specified
221 /// data type, then you will get a message indicating failure.
222 ///
223 /// ```
224 ///# use yew::format::{Json, Nothing, Format};
225 ///# use yew::services::FetchService;
226 ///# use http::Request;
227 ///# use yew::services::fetch::Response;
228 ///# use yew::{Component, ComponentLink, Html};
229 ///# use serde_derive::Deserialize;
230 ///# struct Comp;
231 ///# impl Component for Comp {
232 ///# type Message = Msg;type Properties = ();
233 ///# fn create(props: Self::Properties,link: ComponentLink<Self>) -> Self {unimplemented!()}
234 ///# fn update(&mut self,msg: Self::Message) -> bool {unimplemented!()}
235 ///# fn change(&mut self, _: Self::Properties) -> bool {unimplemented!()}
236 ///# fn view(&self) -> Html {unimplemented!()}
237 ///# }
238 ///# enum Msg {
239 ///# FetchResourceComplete(Data),
240 ///# FetchResourceFailed
241 ///# }
242 /// #[derive(Deserialize)]
243 /// struct Data {
244 /// value: String
245 /// }
246 ///
247 ///# fn dont_execute() {
248 ///# let link: ComponentLink<Comp> = unimplemented!();
249 /// let get_request = Request::get("/thing").body(Nothing).unwrap();
250 /// let callback = link.callback(|response: Response<Json<Result<Data, anyhow::Error>>>| {
251 /// if let (meta, Json(Ok(body))) = response.into_parts() {
252 /// if meta.status.is_success() {
253 /// return Msg::FetchResourceComplete(body);
254 /// }
255 /// }
256 /// Msg::FetchResourceFailed
257 /// });
258 ///
259 /// let task = FetchService::fetch(get_request, callback);
260 ///# }
261 /// ```
262 ///
263 pub fn fetch<IN, OUT: 'static>(
264 request: Request<IN>,
265 callback: Callback<Response<OUT>>,
266 ) -> Result<FetchTask, &'static str>
267 where
268 IN: Into<Text>,
269 OUT: From<Text>,
270 {
271 fetch_impl::<IN, OUT, String, String>(false, request, None, callback)
272 }
273
274 /// `fetch` with provided `FetchOptions` object.
275 /// Use it if you need to send cookies with a request:
276 /// ```
277 ///# use yew::format::Nothing;
278 ///# use yew::services::fetch::{self, FetchOptions, Credentials};
279 ///# use yew::{Html, Component, ComponentLink};
280 ///# use yew::services::FetchService;
281 ///# use http::Response;
282 ///# struct Comp;
283 ///# impl Component for Comp {
284 ///# type Message = Msg;
285 ///# type Properties = ();
286 ///# fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {unimplemented!()}
287 ///# fn update(&mut self, msg: Self::Message) -> bool {unimplemented!()}
288 ///# fn change(&mut self, _: Self::Properties) -> bool {unimplemented!()}
289 ///# fn view(&self) -> Html {unimplemented!()}
290 ///# }
291 ///# pub enum Msg { }
292 ///# fn dont_execute() {
293 ///# let link: ComponentLink<Comp> = unimplemented!();
294 ///# let callback = link.callback(|response: Response<Result<String, anyhow::Error>>| -> Msg { unimplemented!() });
295 /// let request = fetch::Request::get("/path/")
296 /// .body(Nothing)
297 /// .unwrap();
298 /// let options = FetchOptions {
299 /// credentials: Some(Credentials::SameOrigin),
300 /// ..FetchOptions::default()
301 /// };
302 /// let task = FetchService::fetch_with_options(request, options, callback);
303 ///# }
304 /// ```
305 pub fn fetch_with_options<IN, OUT: 'static>(
306 request: Request<IN>,
307 options: FetchOptions,
308 callback: Callback<Response<OUT>>,
309 ) -> Result<FetchTask, &'static str>
310 where
311 IN: Into<Text>,
312 OUT: From<Text>,
313 {
314 fetch_impl::<IN, OUT, String, String>(false, request, Some(options), callback)
315 }
316
317 /// Fetch the data in binary format.
318 pub fn fetch_binary<IN, OUT: 'static>(
319 request: Request<IN>,
320 callback: Callback<Response<OUT>>,
321 ) -> Result<FetchTask, &'static str>
322 where
323 IN: Into<Binary>,
324 OUT: From<Binary>,
325 {
326 fetch_impl::<IN, OUT, Vec<u8>, ArrayBuffer>(true, request, None, callback)
327 }
328
329 /// Fetch the data in binary format using the provided request options.
330 pub fn fetch_binary_with_options<IN, OUT: 'static>(
331 request: Request<IN>,
332 options: FetchOptions,
333 callback: Callback<Response<OUT>>,
334 ) -> Result<FetchTask, &'static str>
335 where
336 IN: Into<Binary>,
337 OUT: From<Binary>,
338 {
339 fetch_impl::<IN, OUT, Vec<u8>, ArrayBuffer>(true, request, Some(options), callback)
340 }
341}
342
343fn fetch_impl<IN, OUT: 'static, T, X>(
344 binary: bool,
345 request: Request<IN>,
346 options: Option<FetchOptions>,
347 callback: Callback<Response<OUT>>,
348) -> Result<FetchTask, &'static str>
349where
350 IN: Into<Format<T>>,
351 OUT: From<Format<T>>,
352 T: JsSerialize,
353 X: TryFrom<Value> + Into<T>,
354{
355 // Consume request as parts and body.
356 let (parts, body) = request.into_parts();
357
358 // Map headers into a Js `Header` to make sure it's supported.
359 let header_list = parts
360 .headers
361 .iter()
362 .map(|(k, v)| {
363 Ok((
364 k.as_str(),
365 v.to_str().map_err(|_| "Unparsable request header")?,
366 ))
367 })
368 .collect::<Result<HashMap<_, _>, _>>()?;
369 let header_map = js! {
370 try {
371 return new Headers(@{header_list});
372 } catch(error) {
373 return error;
374 }
375 };
376 if Error::try_from(js!( return @{header_map.as_ref()}; )).is_ok() {
377 return Err("couldn't build headers");
378 }
379
380 // Formats URI.
381 let uri = parts.uri.to_string();
382 let method = parts.method.as_str();
383 let body = body.into().ok();
384
385 // Prepare the response callback.
386 // Notice that the callback signature must match the call from the JavaScript
387 // side. There is no static check at this point.
388 let callback = move |success: bool, status: u16, headers: HashMap<String, String>, data: X| {
389 let mut response_builder = Response::builder();
390
391 if let Ok(status) = StatusCode::from_u16(status) {
392 response_builder = response_builder.status(status);
393 }
394
395 for (key, values) in headers {
396 response_builder = response_builder.header(key.as_str(), values.as_str());
397 }
398
399 // Deserialize and wrap response data into a Text object.
400 let data = if success {
401 Ok(data.into())
402 } else {
403 Err(FetchError::FailedResponse.into())
404 };
405 let out = OUT::from(data);
406 let response = response_builder.body(out).unwrap();
407 callback.emit(response);
408 };
409
410 #[allow(clippy::too_many_arguments)]
411 let handle = js! {
412 var body = @{body};
413 if (@{binary} && body != null) {
414 body = Uint8Array.from(body);
415 }
416 var callback = @{callback};
417 var abortController = AbortController ? new AbortController() : null;
418 var handle = {
419 active: true,
420 callback,
421 abortController,
422 };
423 var init = {
424 method: @{method},
425 body: body,
426 headers: @{header_map},
427 };
428 var opts = @{Serde(options)} || {};
429 for (var attrname in opts) {
430 init[attrname] = opts[attrname];
431 }
432 if (abortController && !("signal" in init)) {
433 init.signal = abortController.signal;
434 }
435 fetch(@{uri}, init).then(function(response) {
436 var promise = (@{binary}) ? response.arrayBuffer() : response.text();
437 var status = response.status;
438 var headers = {};
439 response.headers.forEach(function(value, key) {
440 headers[key] = value;
441 });
442 promise.then(function(data) {
443 if (handle.active == true) {
444 handle.active = false;
445 callback(true, status, headers, data);
446 callback.drop();
447 }
448 }).catch(function(err) {
449 if (handle.active == true) {
450 handle.active = false;
451 callback(false, status, headers, data);
452 callback.drop();
453 }
454 });
455 }).catch(function(e) {
456 if (handle.active == true) {
457 var data = (@{binary}) ? new ArrayBuffer() : "";
458 handle.active = false;
459 callback(false, 408, {}, data);
460 callback.drop();
461 }
462 });
463 return handle;
464 };
465 Ok(FetchTask(Some(handle)))
466}
467
468impl Task for FetchTask {
469 fn is_active(&self) -> bool {
470 if let Some(ref task) = self.0 {
471 let result = js! {
472 var the_task = @{task};
473 return the_task.active &&
474 (!the_task.abortController || !the_task.abortController.signal.aborted);
475 };
476 result.try_into().unwrap_or(false)
477 } else {
478 false
479 }
480 }
481}
482
483impl Drop for FetchTask {
484 fn drop(&mut self) {
485 if self.is_active() {
486 // Fetch API doesn't support request cancelling in all browsers
487 // and we should use this workaround with a flag.
488 // In that case, request not canceled, but callback won't be called.
489 let handle = self
490 .0
491 .take()
492 .expect("tried to cancel request fetching twice");
493 js! { @(no_return)
494 var handle = @{handle};
495 handle.active = false;
496 handle.callback.drop();
497 if (handle.abortController) {
498 handle.abortController.abort();
499 }
500 }
501 }
502 }
503}