From 932defd87941e6875cc813c83c4ee18e61ca3193 Mon Sep 17 00:00:00 2001 From: Sean McArthur Date: Tue, 24 Sep 2019 14:50:30 -0700 Subject: [PATCH] Introduce unstable, incomplete WASM support --- .travis.yml | 8 +++ Cargo.toml | 63 +++++++++++------ src/error.rs | 9 ++- src/into_url.rs | 16 +++-- src/lib.rs | 114 +++++++++++++++++------------- src/wasm/body.rs | 3 + src/wasm/client.rs | 162 +++++++++++++++++++++++++++++++++++++++++++ src/wasm/mod.rs | 29 ++++++++ src/wasm/request.rs | 144 ++++++++++++++++++++++++++++++++++++++ src/wasm/response.rs | 73 +++++++++++++++++++ 10 files changed, 543 insertions(+), 78 deletions(-) create mode 100644 src/wasm/body.rs create mode 100644 src/wasm/client.rs create mode 100644 src/wasm/mod.rs create mode 100644 src/wasm/request.rs create mode 100644 src/wasm/response.rs diff --git a/.travis.yml b/.travis.yml index b3d59de..d0595e1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -47,10 +47,18 @@ matrix: #- rust: nightly # env: FEATURES="--features socks" + # trust-dns #- rust: stable #- rust: nightly # env: FEATURES="--features trust-dns" + # wasm + - name: "WASM" + env: TARGET=wasm32-unknown-unknown + rust: nightly + install: rustup target add "$TARGET" + script: cargo check --target "$TARGET" + # android #- rust: stable - rust: nightly diff --git a/Cargo.toml b/Cargo.toml index 3dfa788..985ba4e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,13 +17,34 @@ publish = false [package.metadata.docs.rs] all-features = true +[features] +default = ["default-tls"] + +tls = [] + +default-tls = ["hyper-tls", "native-tls", "tls", "tokio-tls"] +default-tls-vendored = ["default-tls", "native-tls/vendored"] + +#rustls-tls = ["hyper-rustls", "tokio-rustls", "webpki-roots", "rustls", "tls"] + +blocking = ["futures-channel-preview", "futures-util-preview/io"] + +cookies = ["cookie_crate", "cookie_store"] + +gzip = ["async-compression"] + +#trust-dns = ["trust-dns-resolver"] + [dependencies] +http = "0.1.15" +url = "2.1" + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] base64 = "0.10" bytes = "0.4" encoding_rs = "0.8" futures-core-preview = { version = "=0.3.0-alpha.18" } futures-util-preview = { version = "=0.3.0-alpha.18" } -http = "0.1.15" http-body = "=0.2.0-alpha.1" hyper = "=0.13.0-alpha.2" log = "0.4" @@ -32,13 +53,11 @@ mime_guess = "2.0" percent-encoding = "2.1" tokio = { version = "=0.2.0-alpha.5", default-features = false, features = ["rt-full", "tcp"] } tokio-executor = "=0.2.0-alpha.5" -url = "2.1" uuid = { version = "0.7", features = ["v4"] } time = "0.1.42" # TODO: candidates for optional features - serde = "1.0" serde_json = "1.0" serde_urlencoded = "0.6.1" @@ -72,7 +91,7 @@ async-compression = { version = "0.1.0-alpha.4", default-features = false, featu ## trust-dns #trust-dns-resolver = { version = "0.11", optional = true } -[dev-dependencies] +[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] env_logger = "0.6" hyper = { version = "=0.13.0-alpha.2", features = ["unstable-stream"] } serde = { version = "1.0", features = ["derive"] } @@ -80,28 +99,26 @@ libflate = "0.1" doc-comment = "0.3" tokio-fs = { version = "=0.2.0-alpha.5" } -[features] -default = ["default-tls"] - -tls = [] - -default-tls = ["hyper-tls", "native-tls", "tls", "tokio-tls"] -default-tls-vendored = ["default-tls", "native-tls/vendored"] - -# re-enable CI also -#rustls-tls = ["hyper-rustls", "tokio-rustls", "webpki-roots", "rustls", "tls"] - -blocking = ["futures-channel-preview", "futures-util-preview/io"] - -cookies = ["cookie_crate", "cookie_store"] - -gzip = ["async-compression"] - -#trust-dns = ["trust-dns-resolver"] - [target.'cfg(windows)'.dependencies] winreg = "0.6" +# wasm + +[target.'cfg(target_arch = "wasm32")'.dependencies] +js-sys = "0.3.25" +wasm-bindgen = "0.2.48" +wasm-bindgen-futures = { version = "", features = ["futures_0_3"] } + +[target.'cfg(target_arch = "wasm32")'.dependencies.web-sys] +version = "0.3.25" +features = [ + "Headers", + "Request", + "RequestInit", + "Response", + "Window", +] + [[example]] name = "blocking" path = "examples/blocking.rs" diff --git a/src/error.rs b/src/error.rs index 2c15528..583fbd5 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,3 +1,4 @@ +#![cfg_attr(target_arch = "wasm32", allow(unused))] use std::error::Error as StdError; use std::fmt; use std::io; @@ -12,7 +13,7 @@ pub struct Error { inner: Box, } -type BoxError = Box; +pub(crate) type BoxError = Box; struct Inner { kind: Kind, @@ -213,6 +214,12 @@ pub(crate) fn url_bad_scheme(url: Url) -> Error { Error::new(Kind::Builder, Some("URL scheme is not allowed")).with_url(url) } +if_wasm! { + pub(crate) fn wasm(js_val: wasm_bindgen::JsValue) -> BoxError { + format!("{:?}", js_val).into() + } +} + // io::Error helpers #[allow(unused)] diff --git a/src/into_url.rs b/src/into_url.rs index 3d57d32..4e56f02 100644 --- a/src/into_url.rs +++ b/src/into_url.rs @@ -37,14 +37,16 @@ impl<'a> PolyfillTryInto for &'a String { } } -pub(crate) fn expect_uri(url: &Url) -> hyper::Uri { - url.as_str() - .parse() - .expect("a parsed Url should always be a valid Uri") -} +if_hyper! { + pub(crate) fn expect_uri(url: &Url) -> http::Uri { + url.as_str() + .parse() + .expect("a parsed Url should always be a valid Uri") + } -pub(crate) fn try_uri(url: &Url) -> Option { - url.as_str().parse().ok() + pub(crate) fn try_uri(url: &Url) -> Option { + url.as_str().parse().ok() + } } #[cfg(test)] diff --git a/src/lib.rs b/src/lib.rs index 4aaf0c8..bdc87c7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -175,60 +175,32 @@ ////! - **trust-dns**: Enables a trust-dns async resolver instead of default ////! threadpool using `getaddrinfo`. -#[cfg(test)] -#[macro_use] -extern crate doc_comment; +macro_rules! if_wasm { + ($($item:item)*) => {$( + #[cfg(target_arch = "wasm32")] + $item + )*} +} -#[cfg(test)] -doctest!("../README.md"); +macro_rules! if_hyper { + ($($item:item)*) => {$( + #[cfg(not(target_arch = "wasm32"))] + $item + )*} +} -pub use hyper::header; -pub use hyper::Method; -pub use hyper::{StatusCode, Version}; -pub use url::ParseError as UrlError; +pub use http::header; +pub use http::Method; +pub use http::{StatusCode, Version}; pub use url::Url; -pub use self::async_impl::{ - multipart, Body, Client, ClientBuilder, Request, RequestBuilder, Response, -}; -//pub use self::body::Body; -//pub use self::client::{Client, ClientBuilder}; -pub use self::error::{Error, Result}; -pub use self::into_url::IntoUrl; -pub use self::proxy::Proxy; -pub use self::redirect::{RedirectAction, RedirectAttempt, RedirectPolicy}; -//pub use self::request::{Request, RequestBuilder}; -//pub use self::response::Response; -#[cfg(feature = "tls")] -pub use self::tls::{Certificate, Identity}; - -// this module must be first because of the `try_` macro +// universal mods #[macro_use] mod error; - -mod async_impl; -#[cfg(feature = "blocking")] -pub mod blocking; -mod connect; -#[cfg(feature = "cookies")] -pub mod cookie; -//#[cfg(feature = "trust-dns")] -//mod dns; mod into_url; -mod proxy; -mod redirect; -#[cfg(feature = "tls")] -mod tls; -//pub mod multipart; - -#[doc(hidden)] -#[deprecated(note = "types moved to top of crate")] -pub mod r#async { - pub use crate::async_impl::{ - multipart, Body, Client, ClientBuilder, Request, RequestBuilder, Response, - }; -} +pub use self::error::{Error, Result}; +pub use self::into_url::IntoUrl; /// Shortcut method to quickly make a `GET` request. /// @@ -274,8 +246,56 @@ fn _assert_impls() { assert_send::(); assert_send::(); - assert_send::(); + #[cfg(not(target_arch = "wasm32"))] + { + assert_send::(); + } assert_send::(); assert_sync::(); } + +if_hyper! { + #[cfg(test)] + #[macro_use] + extern crate doc_comment; + + #[cfg(test)] + doctest!("../README.md"); + + pub use self::async_impl::{ + multipart, Body, Client, ClientBuilder, Request, RequestBuilder, Response, + }; + pub use self::proxy::Proxy; + pub use self::redirect::{RedirectAction, RedirectAttempt, RedirectPolicy}; + #[cfg(feature = "tls")] + pub use self::tls::{Certificate, Identity}; + + + mod async_impl; + #[cfg(feature = "blocking")] + pub mod blocking; + mod connect; + #[cfg(feature = "cookies")] + pub mod cookie; + //#[cfg(feature = "trust-dns")] + //mod dns; + mod proxy; + mod redirect; + #[cfg(feature = "tls")] + mod tls; + + #[doc(hidden)] + #[deprecated(note = "types moved to top of crate")] + pub mod r#async { + pub use crate::async_impl::{ + multipart, Body, Client, ClientBuilder, Request, RequestBuilder, Response, + }; + } +} + +if_wasm! { + mod wasm; + + pub use self::wasm::{Body, Client, ClientBuilder, Request, RequestBuilder, Response}; +} diff --git a/src/wasm/body.rs b/src/wasm/body.rs new file mode 100644 index 0000000..e6a0b51 --- /dev/null +++ b/src/wasm/body.rs @@ -0,0 +1,3 @@ +/// dox +#[derive(Debug)] +pub struct Body(()); diff --git a/src/wasm/client.rs b/src/wasm/client.rs new file mode 100644 index 0000000..95b4246 --- /dev/null +++ b/src/wasm/client.rs @@ -0,0 +1,162 @@ +use std::future::Future; +use http::Method; +use wasm_bindgen::UnwrapThrowExt as _; + +use crate::IntoUrl; +use super::{Request, RequestBuilder, Response}; + +/// dox +#[derive(Clone, Debug)] +pub struct Client(()); + +/// dox +#[derive(Debug)] +pub struct ClientBuilder(()); + +impl Client { + /// dox + pub fn new() -> Self { + Client::builder().build().unwrap_throw() + } + + /// dox + pub fn builder() -> ClientBuilder { + ClientBuilder::new() + } + + /// Convenience method to make a `GET` request to a URL. + /// + /// # Errors + /// + /// This method fails whenever supplied `Url` cannot be parsed. + pub fn get(&self, url: U) -> RequestBuilder { + self.request(Method::GET, url) + } + + /// Convenience method to make a `POST` request to a URL. + /// + /// # Errors + /// + /// This method fails whenever supplied `Url` cannot be parsed. + pub fn post(&self, url: U) -> RequestBuilder { + self.request(Method::POST, url) + } + + /// Convenience method to make a `PUT` request to a URL. + /// + /// # Errors + /// + /// This method fails whenever supplied `Url` cannot be parsed. + pub fn put(&self, url: U) -> RequestBuilder { + self.request(Method::PUT, url) + } + + /// Convenience method to make a `PATCH` request to a URL. + /// + /// # Errors + /// + /// This method fails whenever supplied `Url` cannot be parsed. + pub fn patch(&self, url: U) -> RequestBuilder { + self.request(Method::PATCH, url) + } + + /// Convenience method to make a `DELETE` request to a URL. + /// + /// # Errors + /// + /// This method fails whenever supplied `Url` cannot be parsed. + pub fn delete(&self, url: U) -> RequestBuilder { + self.request(Method::DELETE, url) + } + + /// Convenience method to make a `HEAD` request to a URL. + /// + /// # Errors + /// + /// This method fails whenever supplied `Url` cannot be parsed. + pub fn head(&self, url: U) -> RequestBuilder { + self.request(Method::HEAD, url) + } + + /// Start building a `Request` with the `Method` and `Url`. + /// + /// Returns a `RequestBuilder`, which will allow setting headers and + /// request body before sending. + /// + /// # Errors + /// + /// This method fails whenever supplied `Url` cannot be parsed. + pub fn request(&self, method: Method, url: U) -> RequestBuilder { + let req = url.into_url().map(move |url| Request::new(method, url)); + RequestBuilder::new(self.clone(), req) + } + + pub(super) fn execute_request(&self, req: Request) -> impl Future> { + fetch(req) + } +} + +async fn fetch(req: Request) -> crate::Result { + // Build the js Request + let mut init = web_sys::RequestInit::new(); + init.method(req.method().as_str()); + + let js_headers = web_sys::Headers::new() + .map_err(crate::error::wasm) + .map_err(crate::error::builder)?; + + for (name, value) in req.headers() { + js_headers + .append(name.as_str(), value.to_str().map_err(crate::error::builder)?) + .map_err(crate::error::wasm) + .map_err(crate::error::builder)?; + } + init.headers(&js_headers.into()); + + let js_req = web_sys::Request::new_with_str_and_init(req.url().as_str(), &init) + .map_err(crate::error::wasm) + .map_err(crate::error::builder)?; + + // Await the fetch() promise + let p = web_sys::window() + .expect("window should exist") + .fetch_with_request(&js_req); + let js_resp = super::promise::(p) + .await + .map_err(crate::error::request)?; + + // Convert from the js Response + let mut resp = http::Response::builder(); + resp.status(js_resp.status()); + + // TODO: translate js_resp.headers() + /* + let js_headers = js_resp.headers(); + let js_iter = js_sys::try_iter(&js_headers) + .expect_throw("headers try_iter") + .expect_throw("headers have an iterator"); + + for item in js_iter { + let item = item.expect_throw("headers iterator doesn't throw"); + } + */ + + resp.body(js_resp) + .map(Response::new) + .map_err(crate::error::request) +} + +// ===== impl ClientBuilder ===== + +impl ClientBuilder { + /// dox + pub fn new() -> Self { + ClientBuilder(()) + } + + /// dox + pub fn build(self) -> Result { + Ok(Client(())) + } + +} diff --git a/src/wasm/mod.rs b/src/wasm/mod.rs new file mode 100644 index 0000000..bb909a0 --- /dev/null +++ b/src/wasm/mod.rs @@ -0,0 +1,29 @@ +use wasm_bindgen::JsCast; + +mod body; +mod client; +mod request; +mod response; + +pub use self::body::Body; +pub use self::client::{Client, ClientBuilder}; +pub use self::request::{Request, RequestBuilder}; +pub use self::response::Response; + + +async fn promise(promise: js_sys::Promise) -> Result +where + T: JsCast, +{ + use wasm_bindgen_futures::futures_0_3::JsFuture; + + let js_val = JsFuture::from(promise) + .await + .map_err(crate::error::wasm)?; + + js_val + .dyn_into::() + .map_err(|_js_val| { + "promise resolved to unexpected type".into() + }) +} diff --git a/src/wasm/request.rs b/src/wasm/request.rs new file mode 100644 index 0000000..271ff42 --- /dev/null +++ b/src/wasm/request.rs @@ -0,0 +1,144 @@ +use std::fmt; + +use http::{Method, HeaderMap}; +use url::Url; + +use super::{Body, Client, Response}; + +/// A request which can be executed with `Client::execute()`. +pub struct Request { + method: Method, + url: Url, + headers: HeaderMap, + body: Option, +} + +/// A builder to construct the properties of a `Request`. +pub struct RequestBuilder { + client: Client, + request: crate::Result, +} + +impl Request { + pub(super) fn new(method: Method, url: Url) -> Self { + Request { + method, + url, + headers: HeaderMap::new(), + body: None, + } + } + + /// Get the method. + #[inline] + pub fn method(&self) -> &Method { + &self.method + } + + /// Get a mutable reference to the method. + #[inline] + pub fn method_mut(&mut self) -> &mut Method { + &mut self.method + } + + /// Get the url. + #[inline] + pub fn url(&self) -> &Url { + &self.url + } + + /// Get a mutable reference to the url. + #[inline] + pub fn url_mut(&mut self) -> &mut Url { + &mut self.url + } + + /// Get the headers. + #[inline] + pub fn headers(&self) -> &HeaderMap { + &self.headers + } + + /// Get a mutable reference to the headers. + #[inline] + pub fn headers_mut(&mut self) -> &mut HeaderMap { + &mut self.headers + } + + /// Get the body. + #[inline] + pub fn body(&self) -> Option<&Body> { + self.body.as_ref() + } + + /// Get a mutable reference to the body. + #[inline] + pub fn body_mut(&mut self) -> &mut Option { + &mut self.body + } +} + +impl RequestBuilder { + pub(super) fn new(client: Client, request: crate::Result) -> RequestBuilder { + RequestBuilder { client, request } + } + + + /// Set the request body. + pub fn body>(mut self, body: T) -> RequestBuilder { + if let Ok(ref mut req) = self.request { + req.body = Some(body.into()); + } + self + } + + /// Constructs the Request and sends it to the target URL, returning a + /// future Response. + /// + /// # Errors + /// + /// This method fails if there was an error while sending request. + /// + /// # Example + /// + /// ```no_run + /// # use reqwest::Error; + /// # + /// # async fn run() -> Result<(), Error> { + /// let response = reqwest::Client::new() + /// .get("https://hyper.rs") + /// .send() + /// .await?; + /// # Ok(()) + /// # } + /// ``` + pub async fn send(self) -> crate::Result { + let req = self.request?; + self.client.execute_request(req).await + } +} + +impl fmt::Debug for Request { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt_request_fields(&mut f.debug_struct("Request"), self).finish() + } +} + +impl fmt::Debug for RequestBuilder { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let mut builder = f.debug_struct("RequestBuilder"); + match self.request { + Ok(ref req) => fmt_request_fields(&mut builder, req).finish(), + Err(ref err) => builder.field("error", err).finish(), + } + } +} + +fn fmt_request_fields<'a, 'b>( + f: &'a mut fmt::DebugStruct<'a, 'b>, + req: &Request, +) -> &'a mut fmt::DebugStruct<'a, 'b> { + f.field("method", &req.method) + .field("url", &req.url) + .field("headers", &req.headers) +} diff --git a/src/wasm/response.rs b/src/wasm/response.rs new file mode 100644 index 0000000..cdf2600 --- /dev/null +++ b/src/wasm/response.rs @@ -0,0 +1,73 @@ +use std::fmt; + +use http::{HeaderMap, StatusCode}; + +/// A Response to a submitted `Request`. +pub struct Response { + http: http::Response, +} + +impl Response { + pub(super) fn new( + res: http::Response, + //url: Url, + ) -> Response { + Response { + http: res, + } + } + + /// Get the `StatusCode` of this `Response`. + #[inline] + pub fn status(&self) -> StatusCode { + self.http.status() + } + + /// Get the `Headers` of this `Response`. + #[inline] + pub fn headers(&self) -> &HeaderMap { + self.http.headers() + } + + /// Get a mutable reference to the `Headers` of this `Response`. + #[inline] + pub fn headers_mut(&mut self) -> &mut HeaderMap { + self.http.headers_mut() + } + + /* It might not be possible to detect this in JS? + /// Get the HTTP `Version` of this `Response`. + #[inline] + pub fn version(&self) -> Version { + self.http.version() + } + */ + + // pub async fn json() + + + /// Get the response text. + pub async fn text(self) -> crate::Result { + let p = self.http.body().text() + .map_err(crate::error::wasm) + .map_err(crate::error::decode)?; + let js_val = super::promise::(p) + .await + .map_err(crate::error::decode)?; + if let Some(s) = js_val.as_string() { + Ok(s) + } else { + Err(crate::error::decode("response.text isn't string")) + } + } +} + +impl fmt::Debug for Response { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("Response") + //.field("url", self.url()) + .field("status", &self.status()) + .field("headers", self.headers()) + .finish() + } +}