diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e64f5c04..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 @@ -106,16 +106,27 @@ 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 + - 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 + args: -p gloo-net --all-features diff --git a/Cargo.toml b/Cargo.toml index 9c268ba0..0dd94723 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ features = ["futures"] members = [ "crates/timers", "crates/events", + "crates/net", "crates/file", "crates/dialogs", "crates/storage", diff --git a/crates/net/Cargo.toml b/crates/net/Cargo.toml index 1486baa8..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" @@ -62,6 +63,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/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/http/headers.rs b/crates/net/src/http/headers.rs new file mode 100644 index 00000000..c7fbac5b --- /dev/null +++ b/crates/net/src/http/headers.rs @@ -0,0 +1,120 @@ +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 { + raw: web_sys::Headers, +} + +impl Default for Headers { + fn default() -> Self { + Self::new() + } +} + +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(), + } + } + + /// 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) { + // 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/net/src/http.rs b/crates/net/src/http/mod.rs similarity index 62% rename from crates/net/src/http.rs rename to crates/net/src/http/mod.rs index a32dc5d3..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, Headers, ObserverCallback, ReadableStream, ReferrerPolicy, RequestCache, - RequestCredentials, RequestMode, RequestRedirect, + AbortSignal, FormData, ObserverCallback, ReadableStream, ReferrerPolicy, RequestCache, + RequestCredentials, RequestMode, RequestRedirect, ResponseType, }; #[allow( @@ -66,60 +68,72 @@ impl fmt::Display for Method { } } -/// A request. +/// 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, } impl Request { - /// Creates a new request with a url. + /// 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(url: &str) -> Self { Self { options: web_sys::RequestInit::new(), - headers: web_sys::Headers::new().expect("headers"), + headers: Headers::new(), url: url.into(), } } - /// Sets the body. + /// Set the body for this request. pub fn body(mut self, body: impl Into) -> Self { self.options.body(Some(&body.into())); self } - /// Sets the request cache. + /// A string indicating how the request will interact with the browser’s HTTP cache. pub fn cache(mut self, cache: RequestCache) -> Self { self.options.cache(cache); self } - /// Sets the request credentials. + /// Controls what browsers do with credentials (cookies, HTTP authentication entries, and TLS + /// client certificates). pub fn credentials(mut self, credentials: RequestCredentials) -> Self { self.options.credentials(credentials); 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 } - /// Sets the request integrity. + /// The subresource integrity value of the request (e.g., + /// `sha256-BpfBw7ivV8q2jLiT13fxDYAe2tJllusRSZ273h2nFSE=`). pub fn integrity(mut self, integrity: &str) -> Self { self.options.integrity(integrity); self } - /// Sets the request method. + /// 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: Method) -> Self { self.options.method(&method.to_string()); self } - /// Sets the request mode. + /// The mode you want to use for the request. pub fn mode(mut self, mode: RequestMode) -> Self { self.options.mode(mode); self @@ -131,19 +145,29 @@ impl Request { self } - /// Sets the request redirect. + /// 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) -> Self { self.options.redirect(redirect); self } - /// Sets the request referrer. + /// 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) -> Self { self.options.referrer(referrer); self } - /// Sets the request referrer policy. + /// Specifies the + /// [referrer policy](https://w3c.github.io/webappsec-referrer-policy/#referrer-policies) to + /// use for the request. pub fn referrer_policy(mut self, referrer_policy: ReferrerPolicy) -> Self { self.options.referrer_policy(referrer_policy); self @@ -157,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 { @@ -210,37 +234,71 @@ pub struct Response { } impl Response { - /// Gets the url. + /// Build a [Response] from [web_sys::Response]. + pub fn from_raw(raw: web_sys::Response) -> Self { + Self { response: 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.response.type_() + } + + /// The URL of the response. + /// + /// The returned value will be the final URL obtained after any redirects. pub fn url(&self) -> String { self.response.url() } - /// Whether the request was redirected. + /// Whether or not this response is the result of a request you made which was redirected. pub fn redirected(&self) -> bool { self.response.redirected() } - /// Gets the status. + /// the [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status) of the + /// response. pub fn status(&self) -> u16 { self.response.status() } - /// Whether the response was `ok`. + /// 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) -> bool { self.response.ok() } - /// Gets the status text. + /// 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.response.status_text() } /// Gets the headers. pub fn headers(&self) -> Headers { - self.response.headers() + Headers::from_raw(self.response.headers()) } - /// Whether the body was used. + /// 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.response.body_used() } @@ -255,14 +313,14 @@ impl Response { &self.response } - /// Gets the form data. + /// Reads the response to completion, returning it as `FormData`. pub async fn form_data(&self) -> Result { let promise = self.response.form_data().map_err(js_to_error)?; let val = JsFuture::from(promise).await.map_err(js_to_error)?; Ok(FormData::from(val)) } - /// Gets and parses the json. + /// Reads the response to completion, parsing it as JSON. #[cfg(feature = "json")] #[cfg_attr(docsrs, doc(cfg(feature = "json")))] pub async fn json(&self) -> Result { @@ -272,7 +330,7 @@ impl Response { Ok(json.into_serde()?) } - /// Gets the response text. + /// Reads the response as a String. pub async fn text(&self) -> Result { let promise = self.response.text().unwrap(); let val = JsFuture::from(promise).await.map_err(js_to_error)?; 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::*;