From 3c660b2a64cbb0e02cd22a9d58b14a13f7cb31ff Mon Sep 17 00:00:00 2001 From: Richard Dodd Date: Tue, 8 Feb 2022 18:23:05 +0000 Subject: [PATCH 1/7] WIP --- Cargo.toml | 2 + crates/fetch/Cargo.toml | 30 +++++++++ crates/fetch/README.md | 23 +++++++ crates/fetch/src/callback.rs | 25 +++++++ crates/fetch/src/lib.rs | 123 +++++++++++++++++++++++++++++++++++ 5 files changed, 203 insertions(+) create mode 100644 crates/fetch/Cargo.toml create mode 100644 crates/fetch/README.md create mode 100644 crates/fetch/src/callback.rs create mode 100644 crates/fetch/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 8f0f4e8a..814e14d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ categories = ["api-bindings", "wasm"] [dependencies] gloo-timers = { version = "0.2.0", path = "crates/timers" } gloo-events = { version = "0.1.0", path = "crates/events" } +gloo-fetch = { version = "0.1.0", path = "crates/fetch" } gloo-file = { version = "0.2.0", path = "crates/file" } gloo-dialogs = { version = "0.1.0", path = "crates/dialogs" } gloo-storage = { version = "0.2.0", path = "crates/storage" } @@ -34,6 +35,7 @@ features = ["futures"] members = [ "crates/timers", "crates/events", + "crates/fetch", "crates/file", "crates/dialogs", "crates/storage", diff --git a/crates/fetch/Cargo.toml b/crates/fetch/Cargo.toml new file mode 100644 index 00000000..c573fa27 --- /dev/null +++ b/crates/fetch/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "gloo-fetch" +description = "" +version = "0.1.0" +authors = ["Rust and WebAssembly Working Group"] +edition = "2018" +license = "MIT/Apache-2.0" +readme = "README.md" +repository = "https://github.com/rustwasm/gloo/tree/master/crates/events" +homepage = "https://github.com/rustwasm/gloo" +categories = ["api-bindings", "asynchronous", "web-programming", "wasm"] + +[dependencies] +http = "0.2.6" +wasm-bindgen = "0.2.55" +wasm-bindgen-test = "0.3.5" + +[dependencies.web-sys] +version = "0.3.32" +features = [ + "Headers", + "Request", + "Window", + "RequestInit", + "RequestMode", + "RequestCache", + "RequestCredentials", + "RequestRedirect", + "ReferrerPolicy", +] diff --git a/crates/fetch/README.md b/crates/fetch/README.md new file mode 100644 index 00000000..41cea35a --- /dev/null +++ b/crates/fetch/README.md @@ -0,0 +1,23 @@ +
+ +

gloo-fetch

+ +

+ Build Status + Crates.io version + Download + docs.rs docs +

+ +

+ API Docs + | + Contributing + | + Chat +

+ + Built with 🦀🕸 by The Rust and WebAssembly Working Group +
+ + diff --git a/crates/fetch/src/callback.rs b/crates/fetch/src/callback.rs new file mode 100644 index 00000000..872a1456 --- /dev/null +++ b/crates/fetch/src/callback.rs @@ -0,0 +1,25 @@ +use wasm_bindgen::prelude::*; +use wasm_bindgen_test::wasm_bindgen_test; + +/// Create a HeaderMap out of a web `Headers` object. +fn convert_headers(headers: web_sys::Headers) -> http::HeaderMap { + use http::{ + header::{HeaderName, HeaderValue}, + HeaderMap, + }; + let mut map = HeaderMap::new(); + for (key, value) in headers.iter() { + let key = HeaderName::from_bytes(key.as_bytes()).unwrap_throw(); + let value = HeaderValue::from_bytes(value.as_bytes()).unwrap_throw(); + map.insert(key, value); + } + map +} + +#[wasm_bindgen_test] +fn test_convert_headers() { + let headers = web_sys::Headers::new().unwrap_throw(); + headers.append("Content-Type", "text/plain").unwrap_throw(); + let header_map = convert_headers(headers); + //panic!("{:?}", header_map); +} diff --git a/crates/fetch/src/lib.rs b/crates/fetch/src/lib.rs new file mode 100644 index 00000000..aa382c6a --- /dev/null +++ b/crates/fetch/src/lib.rs @@ -0,0 +1,123 @@ +pub mod callback; + +use std::borrow::Cow; +use wasm_bindgen::JsValue; +use web_sys::RequestInit; + +pub use http; +pub use web_sys::{RequestCache, RequestMode}; + +pub struct Request<'a> { + url: Cow<'a, str>, + init: RequestInit, + headers: http::HeaderMap, +} + +impl<'a> Request<'a> { + /// `url` can be a `String`, a `&str`, or a `Cow<'a, str>`. + pub fn new(url: impl Into>) -> Self { + Self { + url: url.into(), + init: RequestInit::new(), + headers: http::HeaderMap::new(), + } + } + + pub fn get(url: impl Into>) -> Self { + let mut req = Self::new(url); + // GET is the default. + //req.method(http::Method::GET); + req + } + + pub fn post(url: impl Into>, body: impl RequestBody) -> Self { + let mut req = Self::new(url); + req.method(http::Method::POST); + req.init.body(Some(&body.as_js_value())); + req + } + + /// The request method, e.g., GET, POST. + /// + /// Note that the Origin header is not set on Fetch requests with a method of HEAD or GET. + pub fn method(&mut self, method: http::Method) -> &mut Self { + self.init.method(method.as_str()); + self + } + + /// Set the content type for the request (e.g. `application/json` for json, `text/html` for + /// Html) + /// + /// # Panics + /// + /// Panics if the content type contains any invalid bytes (`<32` apart from tab, and `127`). + pub fn content_type(&mut self, content_type: impl AsRef<[u8]>) -> &mut Self { + self.insert_header( + "Content-Type", + http::HeaderValue::from_bytes(content_type.as_ref()).expect("invalid content type"), + ); + self + } + + /// Add a header to the request, replacing an existing header with the same name. + /// + /// Note that + /// [some names are forbidden](https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_header_name). + pub fn insert_header( + &mut self, + name: impl http::header::IntoHeaderName, + value: impl Into, + ) -> &mut Self { + self.headers.insert(name, value.into()); + self + } + + /// Add a header to the request, adding a duplicate if an existing header has the same name. + /// + /// Note that + /// [some names are forbidden](https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_header_name). + pub fn append_header( + &mut self, + name: impl http::header::IntoHeaderName, + value: impl Into, + ) -> &mut Self { + self.headers.append(name, value.into()); + self + } + + pub fn headers(&self) -> &http::HeaderMap { + &self.headers + } + + pub fn headers_mut(&mut self) -> &mut http::HeaderMap { + &mut self.headers + } + + /// The subresource integrity value of the request (e.g., + /// `sha256-BpfBw7ivV8q2jLiT13fxDYAe2tJllusRSZ273h2nFSE=`). + pub fn integrity(&mut self, integrity: &'_ str) -> &mut Self { + self.init.integrity(integrity); + self + } + + /// The mode you want to use for the request. + pub fn request_mode(&mut self, mode: RequestMode) -> &mut Self { + self.init.mode(mode); + self + } +} + +trait Sealed {} + +pub trait RequestBody: Sealed { + fn as_js_value(&self) -> JsValue; +} + +impl Sealed for String {} +impl RequestBody for String { + fn as_js_value(&self) -> JsValue { + JsValue::from_str(&self) + } +} + +// TODO From 7d98588ef9fa1944d178586114c17d7e17bbfaa9 Mon Sep 17 00:00:00 2001 From: Richard Dodd Date: Wed, 9 Feb 2022 17:55:37 +0000 Subject: [PATCH 2/7] Implement wrapper around fetch API. --- crates/fetch/Cargo.toml | 17 +- crates/fetch/src/callback.rs | 25 --- crates/fetch/src/headers.rs | 108 +++++++++++ crates/fetch/src/lib.rs | 340 +++++++++++++++++++++++++++-------- src/lib.rs | 1 + 5 files changed, 387 insertions(+), 104 deletions(-) delete mode 100644 crates/fetch/src/callback.rs create mode 100644 crates/fetch/src/headers.rs diff --git a/crates/fetch/Cargo.toml b/crates/fetch/Cargo.toml index c573fa27..f1bf2cf2 100644 --- a/crates/fetch/Cargo.toml +++ b/crates/fetch/Cargo.toml @@ -10,21 +10,30 @@ repository = "https://github.com/rustwasm/gloo/tree/master/crates/events" homepage = "https://github.com/rustwasm/gloo" categories = ["api-bindings", "asynchronous", "web-programming", "wasm"] +[features] +default = ["futures"] +futures = ["wasm-bindgen-futures"] + [dependencies] -http = "0.2.6" +http = { version = "0.2.6", optional = true } +js-sys = "0.3" wasm-bindgen = "0.2.55" -wasm-bindgen-test = "0.3.5" +wasm-bindgen-futures = { version = "0.4", optional = true } [dependencies.web-sys] version = "0.3.32" features = [ + "FormData", "Headers", + "ReadableStream", + "ReferrerPolicy", "Request", - "Window", "RequestInit", "RequestMode", "RequestCache", "RequestCredentials", "RequestRedirect", - "ReferrerPolicy", + "Response", + "ResponseType", + "Window", ] diff --git a/crates/fetch/src/callback.rs b/crates/fetch/src/callback.rs deleted file mode 100644 index 872a1456..00000000 --- a/crates/fetch/src/callback.rs +++ /dev/null @@ -1,25 +0,0 @@ -use wasm_bindgen::prelude::*; -use wasm_bindgen_test::wasm_bindgen_test; - -/// Create a HeaderMap out of a web `Headers` object. -fn convert_headers(headers: web_sys::Headers) -> http::HeaderMap { - use http::{ - header::{HeaderName, HeaderValue}, - HeaderMap, - }; - let mut map = HeaderMap::new(); - for (key, value) in headers.iter() { - let key = HeaderName::from_bytes(key.as_bytes()).unwrap_throw(); - let value = HeaderValue::from_bytes(value.as_bytes()).unwrap_throw(); - map.insert(key, value); - } - map -} - -#[wasm_bindgen_test] -fn test_convert_headers() { - let headers = web_sys::Headers::new().unwrap_throw(); - headers.append("Content-Type", "text/plain").unwrap_throw(); - let header_map = convert_headers(headers); - //panic!("{:?}", header_map); -} diff --git a/crates/fetch/src/headers.rs b/crates/fetch/src/headers.rs new file mode 100644 index 00000000..69fba205 --- /dev/null +++ b/crates/fetch/src/headers.rs @@ -0,0 +1,108 @@ +use js_sys::{Array, Map}; +use std::fmt; +use wasm_bindgen::{JsCast, JsValue, UnwrapThrowExt}; + +// I experimented with using `js_sys::Object` for the headers, since this object is marked +// experimental in MDN. However it's in the fetch spec, and it's necessary for appending headers. +/// A wrapper around `web_sys::Headers`. +pub struct Headers { + pub(crate) raw: web_sys::Headers, +} + +impl Headers { + /// Create a new empty headers object. + pub fn new() -> Self { + // pretty sure this will never throw. + Self { + raw: web_sys::Headers::new().unwrap_throw(), + } + } + + pub(crate) fn from_raw(raw: web_sys::Headers) -> Self { + Self { raw } + } + + /// This method appends a new value onto an existing header, or adds the header if it does not + /// already exist. + pub fn append(&self, name: &str, value: &str) { + // XXX Can this throw? WEBIDL says yes, my experiments with forbidden headers and MDN say + // no. + self.raw.append(name, value).unwrap_throw() + } + + /// Deletes a header if it is present. + pub fn delete(&self, name: &str) { + self.raw.delete(name).unwrap_throw() + } + + /// Gets a header if it is present. + pub fn get(&self, name: &str) -> Option { + self.raw.get(name).unwrap_throw() + } + + /// Whether a header with the given name exists. + pub fn has(&self, name: &str) -> bool { + self.raw.has(name).unwrap_throw() + } + + /// Overwrites a header with the given name. + pub fn set(&self, name: &str, value: &str) { + self.raw.set(name, value).unwrap_throw() + } + + /// Iterate over (header name, header value) pairs. + pub fn entries(&self) -> impl Iterator { + // Here we cheat and cast to a map even though `self` isn't, because the method names match + // and everything works. Is there a better way? Should there be a `MapLike` or + // `MapIterator` type in `js_sys`? + let fake_map: &Map = self.raw.unchecked_ref(); + UncheckedIter(fake_map.entries()).map(|entry| { + let entry: Array = entry.unchecked_into(); + let key = entry.get(0); + let value = entry.get(1); + ( + key.as_string().unwrap_throw(), + value.as_string().unwrap_throw(), + ) + }) + } + + /// Iterate over the names of the headers. + pub fn keys(&self) -> impl Iterator { + let fake_map: &Map = self.raw.unchecked_ref(); + UncheckedIter(fake_map.keys()).map(|key| key.as_string().unwrap_throw()) + } + + /// Iterate over the values of the headers. + pub fn values(&self) -> impl Iterator { + let fake_map: &Map = self.raw.unchecked_ref(); + UncheckedIter(fake_map.values()).map(|v| v.as_string().unwrap_throw()) + } +} + +impl fmt::Debug for Headers { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let mut dbg = f.debug_struct("Headers"); + for (key, value) in self.entries() { + dbg.field(&key, &value); + } + dbg.finish() + } +} + +struct UncheckedIter(js_sys::Iterator); + +impl Iterator for UncheckedIter { + type Item = JsValue; + + fn next(&mut self) -> Option { + // we don't check for errors. Only use this type on things we know conform to the iterator + // interface. + let next = self.0.next().unwrap_throw(); + if next.done() { + None + } else { + Some(next.value()) + } + } +} diff --git a/crates/fetch/src/lib.rs b/crates/fetch/src/lib.rs index aa382c6a..6fa8831c 100644 --- a/crates/fetch/src/lib.rs +++ b/crates/fetch/src/lib.rs @@ -1,123 +1,313 @@ -pub mod callback; +//! A library that wraps the HTTP *fetch* API. +#![warn(missing_docs)] -use std::borrow::Cow; -use wasm_bindgen::JsValue; -use web_sys::RequestInit; +mod headers; -pub use http; -pub use web_sys::{RequestCache, RequestMode}; +use js_sys::{ArrayBuffer, Promise, Uint8Array}; +use std::{ + cell::{RefCell, RefMut}, + ops::Deref, +}; +use wasm_bindgen::{JsCast, JsValue, UnwrapThrowExt}; +use wasm_bindgen_futures::JsFuture; -pub struct Request<'a> { - url: Cow<'a, str>, - init: RequestInit, - headers: http::HeaderMap, +pub use crate::headers::Headers; +#[doc(inline)] +pub use web_sys::{ + ReferrerPolicy, RequestCache, RequestCredentials, RequestMode, RequestRedirect, ResponseType, +}; + +/// A wrapper round `web_sys::Request`: an http request to be used with the `fetch` API. +pub struct Request { + init: RefCell, + headers: RefCell>, } -impl<'a> Request<'a> { - /// `url` can be a `String`, a `&str`, or a `Cow<'a, str>`. - pub fn new(url: impl Into>) -> Self { +impl Request { + /// Creates a new request that will be sent to `url`. + /// + /// Uses `GET` by default. `url` can be a `String`, a `&str`, or a `Cow<'a, str>`. + pub fn new() -> Self { Self { - url: url.into(), - init: RequestInit::new(), - headers: http::HeaderMap::new(), + init: RefCell::new(web_sys::RequestInit::new()), + headers: RefCell::new(None), } } - pub fn get(url: impl Into>) -> Self { - let mut req = Self::new(url); - // GET is the default. - //req.method(http::Method::GET); - req + /// The request method, e.g., GET, POST. + /// + /// Note that the Origin header is not set on Fetch requests with a method of HEAD or GET. + pub fn method(&mut self, method: &str) -> &mut Self { + self.init.borrow_mut().method(method); + self } - pub fn post(url: impl Into>, body: impl RequestBody) -> Self { - let mut req = Self::new(url); - req.method(http::Method::POST); - req.init.body(Some(&body.as_js_value())); - req + /// Get access to the headers object for this request. + /// + /// If you are going to insert new headers, note that + /// [some names are forbidden](https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_header_name) + /// and if they are set then sending the request will error. + pub fn headers(&mut self) -> impl Deref + '_ { + RefMut::map(self.headers.borrow_mut(), |opt| { + opt.get_or_insert_with(|| Headers::new()) + }) } - /// The request method, e.g., GET, POST. - /// - /// Note that the Origin header is not set on Fetch requests with a method of HEAD or GET. - pub fn method(&mut self, method: http::Method) -> &mut Self { - self.init.method(method.as_str()); + /// Set the body for this request. + pub fn body(&mut self, body: impl RequestBody) -> &mut Self { + self.init.borrow_mut().body(body.as_js_value().as_ref()); self } /// Set the content type for the request (e.g. `application/json` for json, `text/html` for - /// Html) + /// Html). + /// + /// If you want a more typed experience, you can use the + /// [`mime`](https://crates.io/crates/mime) crate, and use the `impl Display for mime::Mime` to + /// get a byte string. /// /// # Panics /// /// Panics if the content type contains any invalid bytes (`<32` apart from tab, and `127`). - pub fn content_type(&mut self, content_type: impl AsRef<[u8]>) -> &mut Self { - self.insert_header( - "Content-Type", - http::HeaderValue::from_bytes(content_type.as_ref()).expect("invalid content type"), - ); + #[doc(alias("mime_type", "media_type"))] + pub fn content_type(&mut self, content_type: &str) -> &mut Self { + self.headers().set("Content-Type", content_type); self } - /// Add a header to the request, replacing an existing header with the same name. - /// - /// Note that - /// [some names are forbidden](https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_header_name). - pub fn insert_header( - &mut self, - name: impl http::header::IntoHeaderName, - value: impl Into, - ) -> &mut Self { - self.headers.insert(name, value.into()); + /// A string indicating how the request will interact with the browser’s HTTP cache. + pub fn cache(&mut self, cache: RequestCache) -> &mut Self { + self.init.borrow_mut().cache(cache); self } - /// Add a header to the request, adding a duplicate if an existing header has the same name. - /// - /// Note that - /// [some names are forbidden](https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_header_name). - pub fn append_header( - &mut self, - name: impl http::header::IntoHeaderName, - value: impl Into, - ) -> &mut Self { - self.headers.append(name, value.into()); + /// Controls what browsers do with credentials (cookies, HTTP authentication entries, and TLS + /// client certificates). + pub fn credentials(&mut self, credentials: RequestCredentials) -> &mut Self { + self.init.borrow_mut().credentials(credentials); self } - pub fn headers(&self) -> &http::HeaderMap { - &self.headers - } - - pub fn headers_mut(&mut self) -> &mut http::HeaderMap { - &mut self.headers - } - /// The subresource integrity value of the request (e.g., /// `sha256-BpfBw7ivV8q2jLiT13fxDYAe2tJllusRSZ273h2nFSE=`). pub fn integrity(&mut self, integrity: &'_ str) -> &mut Self { - self.init.integrity(integrity); + self.init.borrow_mut().integrity(integrity); self } /// The mode you want to use for the request. - pub fn request_mode(&mut self, mode: RequestMode) -> &mut Self { - self.init.mode(mode); + pub fn mode(&mut self, mode: RequestMode) -> &mut Self { + self.init.borrow_mut().mode(mode); + self + } + + /// How to handle a redirect response: + /// + /// - *follow*: Automatically follow redirects. Unless otherwise stated the redirect mode is + /// set to follow + /// - *error*: Abort with an error if a redirect occurs. + /// - *manual*: Caller intends to process the response in another context. See [WHATWG fetch + /// standard](https://fetch.spec.whatwg.org/#requests) for more information. + pub fn redirect(&mut self, redirect: RequestRedirect) -> &mut Self { + self.init.borrow_mut().redirect(redirect); self } + + /// The referrer of the request. + /// + /// This can be a same-origin URL, `about:client`, or an empty string. + pub fn referrer(&mut self, referrer: &'_ str) -> &mut Self { + self.init.borrow_mut().referrer(referrer); + self + } + + /// Specifies the + /// [referrer policy](https://w3c.github.io/webappsec-referrer-policy/#referrer-policies) to + /// use for the request. + pub fn referrer_policy(&mut self, policy: ReferrerPolicy) -> &mut Self { + self.init.borrow_mut().referrer_policy(policy); + self + } + + fn apply_headers(&self) { + if let Some(headers) = self.headers.borrow_mut().take() { + self.init.borrow_mut().headers(&headers.raw); + } + } + + /// Send the request, returning a future that will resolve to the response once all headers + /// have been received. + pub async fn send(&self, url: impl AsRef) -> Result { + let response = JsFuture::from(self.send_raw(url)).await?; + Ok(Response::from_raw(response)) + } + + /// A wrapper round `fetch` to de-duplicate some boilerplate + fn send_raw(&self, url: impl AsRef) -> Promise { + self.apply_headers(); + web_sys::window() + .expect("no window") + .fetch_with_str_and_init(url.as_ref(), &*self.init.borrow()) + } } -trait Sealed {} +mod private { + pub trait Sealed {} +} -pub trait RequestBody: Sealed { - fn as_js_value(&self) -> JsValue; +/// A trait for types that can be passed as the `body` to a `Request`. +/// +/// This trait is sealed because we know all the types that are allowed and want to prevent +/// implementation for other types. +pub trait RequestBody: private::Sealed { + /// `web_sys::Request::body` takes an untyped `JsValue`. + /// + /// This is an implementation detail - you shouldn't need to look at this trait at all. + fn as_js_value(&self) -> Option; } -impl Sealed for String {} +impl private::Sealed for String {} impl RequestBody for String { - fn as_js_value(&self) -> JsValue { - JsValue::from_str(&self) + fn as_js_value(&self) -> Option { + Some(JsValue::from_str(&self)) } } -// TODO +impl<'a> private::Sealed for &'a str {} +impl<'a> RequestBody for &'a str { + fn as_js_value(&self) -> Option { + Some(JsValue::from_str(self)) + } +} + +// TODO Blob, BufferSource, FormData, URLSearchParams, (USVString - done), ReadableStream + +/// The response to a `fetch` request once all headers have been successfully received. +/// +/// Note that the full response might not be received that this point, which is why methods that +/// access the response body are asynchronous. +pub struct Response { + raw: web_sys::Response, +} + +impl Response { + /// Downcast a js value to an instance of web_sys::Response. + /// + /// # Correctness + /// + /// Will result in incorrect code if `raw` is not a `web_sys::Response`. + fn from_raw(raw: JsValue) -> Self { + let raw: web_sys::Response = raw.unchecked_into(); + Self { raw } + } + + /// The type read-only property of the Response interface contains the type of the response. + /// + /// It can be one of the following: + /// + /// - basic: Normal, same origin response, with all headers exposed except “Set-Cookie” and + /// “Set-Cookie2″. + /// - cors: Response was received from a valid cross-origin request. Certain headers and the + /// body may be accessed. + /// - error: Network error. No useful information describing the error is available. The + /// Response’s status is 0, headers are empty and immutable. This is the type for a Response + /// obtained from Response.error(). + /// - opaque: Response for “no-cors” request to cross-origin resource. Severely restricted. + /// - opaqueredirect: The fetch request was made with redirect: "manual". The Response's + /// status is 0, headers are empty, body is null and trailer is empty. + pub fn type_(&self) -> ResponseType { + self.raw.type_() + } + + /// The URL of the response. + /// + /// The returned value will be the final URL obtained after any redirects. + pub fn url(&self) -> String { + self.raw.url() + } + + /// Whether or not this response is the result of a request you made which was redirected. + pub fn redirected(&self) -> bool { + self.raw.redirected() + } + + /// the [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status) of the + /// response. + pub fn status(&self) -> u16 { + self.raw.status() + } + + /// Whether the [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status) + /// was a success code (in the range `200 - 299`). + pub fn ok(&self) -> u16 { + self.raw.status() + } + + /// The status message corresponding to the + /// [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status) from + /// `Response::status`. + /// + /// For example, this would be 'OK' for a status code 200, 'Continue' for 100, or 'Not Found' + /// for 404. + pub fn status_text(&self) -> String { + self.raw.status_text() + } + + /// Provides access to the headers contained in the response. + /// + /// Some headers may be inaccessible, depending on CORS and other things. + pub fn headers(&self) -> Headers { + Headers::from_raw(self.raw.headers()) + } + + /// Has the response body been consumed? + /// + /// If true, then any future attempts to consume the body will error. + pub fn body_used(&self) -> bool { + self.raw.body_used() + } + + // TODO unsure how to handle streams, and personally don't need this functionality + + /// Reads the response to completion, returning it as an `ArrayBuffer`. + pub async fn array_buffer(&self) -> Result { + JsFuture::from(self.raw.array_buffer().unwrap_throw()) + .await + .map(JsCast::unchecked_into) + } + + /// Reads the response to completion, returning it as a `Blob`. + pub async fn blob(&self) -> Result { + JsFuture::from(self.raw.array_buffer().unwrap_throw()) + .await + .map(JsCast::unchecked_into) + } + + /// Reads the response to completion, returning it as `FormData`. + pub async fn form_data(&self) -> Result { + JsFuture::from(self.raw.array_buffer().unwrap_throw()) + .await + .map(JsCast::unchecked_into) + } + + /// Reads the response to completion, parsing it as JSON. + /// + /// An alternative here is to get the data as bytes (`body_as_vec`) or a string (`text`), and + /// then parse the json in Rust, using `serde` or something else. + pub async fn json(&self) -> Result { + JsFuture::from(self.raw.array_buffer().unwrap_throw()).await + } + + /// Reads the response as a String. + pub async fn text(&self) -> Result { + JsFuture::from(self.raw.text().unwrap_throw()) + .await + .map(|ok| ok.as_string().unwrap_throw()) + } + + /// Reads the response into a `Vec`. + pub async fn body_as_vec(&self) -> Result, JsValue> { + let buf = self.array_buffer().await?; + Ok(Uint8Array::new(&buf).to_vec()) + } +} diff --git a/src/lib.rs b/src/lib.rs index 655f5c61..5551087e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,7 @@ pub use gloo_console as console; pub use gloo_dialogs as dialogs; pub use gloo_events as events; +pub use gloo_fetch as fetch; pub use gloo_file as file; pub use gloo_history as history; pub use gloo_render as render; From c22a86be7070b58b228b63330a8ea60b7680ce00 Mon Sep 17 00:00:00 2001 From: Muhammad Hamza Date: Fri, 11 Mar 2022 14:16:19 +0500 Subject: [PATCH 3/7] Change directory structure --- Cargo.toml | 4 ++-- crates/{fetch => net}/Cargo.toml | 0 crates/{fetch => net}/README.md | 0 crates/{fetch => net}/src/headers.rs | 0 crates/{fetch/src/lib.rs => net/src/http.rs} | 0 5 files changed, 2 insertions(+), 2 deletions(-) rename crates/{fetch => net}/Cargo.toml (100%) rename crates/{fetch => net}/README.md (100%) rename crates/{fetch => net}/src/headers.rs (100%) rename crates/{fetch/src/lib.rs => net/src/http.rs} (100%) diff --git a/Cargo.toml b/Cargo.toml index 814e14d0..1df82b32 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ categories = ["api-bindings", "wasm"] [dependencies] gloo-timers = { version = "0.2.0", path = "crates/timers" } gloo-events = { version = "0.1.0", path = "crates/events" } -gloo-fetch = { version = "0.1.0", path = "crates/fetch" } +gloo-fetch = { version = "0.1.0", path = "crates/net" } gloo-file = { version = "0.2.0", path = "crates/file" } gloo-dialogs = { version = "0.1.0", path = "crates/dialogs" } gloo-storage = { version = "0.2.0", path = "crates/storage" } @@ -35,7 +35,7 @@ features = ["futures"] members = [ "crates/timers", "crates/events", - "crates/fetch", + "crates/net", "crates/file", "crates/dialogs", "crates/storage", diff --git a/crates/fetch/Cargo.toml b/crates/net/Cargo.toml similarity index 100% rename from crates/fetch/Cargo.toml rename to crates/net/Cargo.toml diff --git a/crates/fetch/README.md b/crates/net/README.md similarity index 100% rename from crates/fetch/README.md rename to crates/net/README.md diff --git a/crates/fetch/src/headers.rs b/crates/net/src/headers.rs similarity index 100% rename from crates/fetch/src/headers.rs rename to crates/net/src/headers.rs diff --git a/crates/fetch/src/lib.rs b/crates/net/src/http.rs similarity index 100% rename from crates/fetch/src/lib.rs rename to crates/net/src/http.rs From feacd96f1a2df38e9ecec3c6f12c511539a674f8 Mon Sep 17 00:00:00 2001 From: Muhammad Hamza Date: Fri, 11 Mar 2022 14:46:50 +0500 Subject: [PATCH 4/7] Post merge fixes --- Cargo.toml | 1 - crates/net/Cargo.toml | 1 + crates/net/src/{ => http}/headers.rs | 16 +++++++++-- crates/net/src/{http.rs => http/mod.rs} | 36 ++++++++++++------------- src/lib.rs | 1 - 5 files changed, 33 insertions(+), 22 deletions(-) rename crates/net/src/{ => http}/headers.rs (91%) rename crates/net/src/{http.rs => http/mod.rs} (93%) diff --git a/Cargo.toml b/Cargo.toml index ea2bf6f5..0dd94723 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,6 @@ categories = ["api-bindings", "wasm"] [dependencies] gloo-timers = { version = "0.2.0", path = "crates/timers" } gloo-events = { version = "0.1.0", path = "crates/events" } -gloo-fetch = { version = "0.1.0", path = "crates/net" } gloo-file = { version = "0.2.0", path = "crates/file" } gloo-dialogs = { version = "0.1.0", path = "crates/dialogs" } gloo-storage = { version = "0.2.0", path = "crates/storage" } diff --git a/crates/net/Cargo.toml b/crates/net/Cargo.toml index 1486baa8..3c6a063f 100644 --- a/crates/net/Cargo.toml +++ b/crates/net/Cargo.toml @@ -62,6 +62,7 @@ http = [ 'web-sys/RequestInit', 'web-sys/RequestMode', 'web-sys/Response', + 'web-sys/ResponseType', 'web-sys/Window', 'web-sys/RequestCache', 'web-sys/RequestCredentials', diff --git a/crates/net/src/headers.rs b/crates/net/src/http/headers.rs similarity index 91% rename from crates/net/src/headers.rs rename to crates/net/src/http/headers.rs index 69fba205..c7fbac5b 100644 --- a/crates/net/src/headers.rs +++ b/crates/net/src/http/headers.rs @@ -6,7 +6,13 @@ use wasm_bindgen::{JsCast, JsValue, UnwrapThrowExt}; // experimental in MDN. However it's in the fetch spec, and it's necessary for appending headers. /// A wrapper around `web_sys::Headers`. pub struct Headers { - pub(crate) raw: web_sys::Headers, + raw: web_sys::Headers, +} + +impl Default for Headers { + fn default() -> Self { + Self::new() + } } impl Headers { @@ -18,10 +24,16 @@ impl Headers { } } - pub(crate) fn from_raw(raw: web_sys::Headers) -> Self { + /// Build [Headers] from [web_sys::Headers]. + pub fn from_raw(raw: web_sys::Headers) -> Self { Self { raw } } + /// Covert [Headers] to [web_sys::Headers]. + pub fn into_raw(self) -> web_sys::Headers { + self.raw + } + /// This method appends a new value onto an existing header, or adds the header if it does not /// already exist. pub fn append(&self, name: &str, value: &str) { diff --git a/crates/net/src/http.rs b/crates/net/src/http/mod.rs similarity index 93% rename from crates/net/src/http.rs rename to crates/net/src/http/mod.rs index caa10faf..5ebea63f 100644 --- a/crates/net/src/http.rs +++ b/crates/net/src/http/mod.rs @@ -3,7 +3,7 @@ //! # Example //! //! ``` -//! # use reqwasm::http::Request; +//! # use gloo_net::http::Request; //! # async fn no_run() { //! let resp = Request::get("/path") //! .send() @@ -13,21 +13,23 @@ //! # } //! ``` +mod headers; + use crate::{js_to_error, Error}; use js_sys::{ArrayBuffer, Uint8Array}; use std::fmt; use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; use wasm_bindgen_futures::JsFuture; -use web_sys::window; #[cfg(feature = "json")] #[cfg_attr(docsrs, doc(cfg(feature = "json")))] use serde::de::DeserializeOwned; +pub use headers::Headers; pub use web_sys::{ AbortSignal, FormData, ObserverCallback, ReadableStream, ReferrerPolicy, RequestCache, - RequestCredentials, RequestMode, RequestRedirect, + RequestCredentials, RequestMode, RequestRedirect, ResponseType, }; #[allow( @@ -69,7 +71,7 @@ impl fmt::Display for Method { /// A wrapper round `web_sys::Request`: an http request to be used with the `fetch` API. pub struct Request { options: web_sys::RequestInit, - headers: web_sys::Headers, + headers: Headers, url: String, } @@ -80,7 +82,7 @@ impl Request { pub fn new(url: &str) -> Self { Self { options: web_sys::RequestInit::new(), - headers: web_sys::Headers::new().expect("headers"), + headers: Headers::new(), url: url.into(), } } @@ -104,9 +106,15 @@ impl Request { self } + /// Replace _all_ the headers. + pub fn headers(mut self, headers: Headers) -> Self { + self.headers = headers; + self + } + /// Sets a header. pub fn header(self, key: &str, value: &str) -> Self { - self.headers.set(key, value).expect("set header"); + self.headers.set(key, value); self } @@ -173,12 +181,12 @@ impl Request { /// Executes the request. pub async fn send(mut self) -> Result { - self.options.headers(&self.headers); + self.options.headers(&self.headers.into_raw()); let request = web_sys::Request::new_with_str_and_init(&self.url, &self.options) .map_err(js_to_error)?; - let promise = window().unwrap().fetch_with_request(&request); + let promise = gloo_utils::window().fetch_with_request(&request); let response = JsFuture::from(promise).await.map_err(js_to_error)?; match response.dyn_into::() { Ok(response) => Ok(Response { @@ -226,13 +234,8 @@ pub struct Response { } impl Response { - /// Downcast a js value to an instance of web_sys::Response. - /// - /// # Correctness - /// - /// Will result in incorrect code if `raw` is not a `web_sys::Response`. - fn from_raw(raw: JsValue) -> Self { - let raw: web_sys::Response = raw.unchecked_into(); + /// Build a [Response] from [web_sys::Response]. + pub fn from_raw(raw: web_sys::Response) -> Self { Self { response: raw } } @@ -318,9 +321,6 @@ impl Response { } /// Reads the response to completion, parsing it as JSON. - /// - /// An alternative here is to get the data as bytes (`body_as_vec`) or a string (`text`), and - /// then parse the json in Rust, using `serde` or something else. #[cfg(feature = "json")] #[cfg_attr(docsrs, doc(cfg(feature = "json")))] pub async fn json(&self) -> Result { diff --git a/src/lib.rs b/src/lib.rs index 5b0aec9d..57a8382d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,7 +7,6 @@ pub use gloo_console as console; pub use gloo_dialogs as dialogs; pub use gloo_events as events; -pub use gloo_fetch as fetch; pub use gloo_file as file; pub use gloo_history as history; pub use gloo_net as net; From 57e2d18a89a85916ac9fb0815a848aee0819b091 Mon Sep 17 00:00:00 2001 From: Muhammad Hamza Date: Fri, 11 Mar 2022 14:49:47 +0500 Subject: [PATCH 5/7] Fix some incorrect docs/examples --- crates/net/Cargo.toml | 3 ++- crates/net/README.md | 2 +- crates/net/src/websocket/futures.rs | 2 +- crates/net/tests/http.rs | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/net/Cargo.toml b/crates/net/Cargo.toml index 3c6a063f..3c0a67d9 100644 --- a/crates/net/Cargo.toml +++ b/crates/net/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Rust and WebAssembly Working Group", "Muhammad Hamza "] edition = "2018" license = "MIT OR Apache-2.0" -repository = "https://github.com/hamza1311/reqwasm" +repository = "https://github.com/rustwasm/gloo" description = "HTTP requests library for WASM Apps" readme = "README.md" keywords = ["requests", "http", "wasm", "websockets"] @@ -12,6 +12,7 @@ categories = ["wasm", "web-programming::http-client", "api-bindings"] [package.metadata.docs.rs] all-features = true +rustdoc-args = ["--cfg", "docsrs"] [dependencies] wasm-bindgen = "0.2" diff --git a/crates/net/README.md b/crates/net/README.md index f5921590..8874f42f 100644 --- a/crates/net/README.md +++ b/crates/net/README.md @@ -36,7 +36,7 @@ assert_eq!(resp.status(), 200); ### WebSocket ```rust -use reqwasm::websocket::{Message, futures::WebSocket}; +use gloo_net::websocket::{Message, futures::WebSocket}; use wasm_bindgen_futures::spawn_local; use futures::{SinkExt, StreamExt}; diff --git a/crates/net/src/websocket/futures.rs b/crates/net/src/websocket/futures.rs index 5a9a9085..cf63dd68 100644 --- a/crates/net/src/websocket/futures.rs +++ b/crates/net/src/websocket/futures.rs @@ -3,7 +3,7 @@ //! # Example //! //! ```rust -//! use reqwasm::websocket::{Message, futures::WebSocket}; +//! use gloo_net::websocket::{Message, futures::WebSocket}; //! use wasm_bindgen_futures::spawn_local; //! use futures::{SinkExt, StreamExt}; //! diff --git a/crates/net/tests/http.rs b/crates/net/tests/http.rs index a2672423..868898bf 100644 --- a/crates/net/tests/http.rs +++ b/crates/net/tests/http.rs @@ -1,4 +1,4 @@ -use reqwasm::http::*; +use gloo_net::http::*; use serde::{Deserialize, Serialize}; use wasm_bindgen_test::*; From 551248bc341584d96f0953971c059a645b535780 Mon Sep 17 00:00:00 2001 From: Muhammad Hamza Date: Fri, 11 Mar 2022 14:53:48 +0500 Subject: [PATCH 6/7] Actually run CI --- .github/workflows/tests.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e64f5c04..379bc97c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -106,16 +106,19 @@ jobs: restore-keys: | cargo-${{ runner.os }}-test- cargo-${{ runner.os }}- + - name: Run browser tests env: HTTPBIN_URL: "http://localhost:8080" ECHO_SERVER_URL: "ws://localhost:8081" - run: wasm-pack test --chrome --firefox --headless + run: | + cd crates/net + wasm-pack test --chrome --firefox --headless --all-features - - name: Run browser tests + - name: Run native tests env: HTTPBIN_URL: "http://localhost:8080" ECHO_SERVER_URL: "ws://localhost:8081" uses: actions-rs/cargo@v1 with: - command: test + command: test -p gloo-net --all-features From 720f66d51e92840187462f4a631c5a778d06fecb Mon Sep 17 00:00:00 2001 From: Muhammad Hamza Date: Fri, 11 Mar 2022 15:08:31 +0500 Subject: [PATCH 7/7] Actually run CI part 2 --- .github/workflows/tests.yml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 379bc97c..b4469b57 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -90,7 +90,7 @@ jobs: with: toolchain: stable profile: minimal - components: clippy + override: true target: wasm32-unknown-unknown - name: Install wasm-pack @@ -115,10 +115,18 @@ jobs: cd crates/net wasm-pack test --chrome --firefox --headless --all-features + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + profile: minimal + override: true + target: wasm32-unknown-unknown + - name: Run native tests env: HTTPBIN_URL: "http://localhost:8080" ECHO_SERVER_URL: "ws://localhost:8081" uses: actions-rs/cargo@v1 with: - command: test -p gloo-net --all-features + command: test + args: -p gloo-net --all-features