diff --git a/CHANGELOG.md b/CHANGELOG.md index 18a5d0c7a..9366962f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## Unreleased +- (BREAKING) The codegen logic has undergone a near-complete rewrite. This means + less nested data types in several cases. The generated code + will break for some queries. +- The custom browser HTTP client in the `web` feature was replaced with + `reqwest`. + ## 0.9.0 - 2020-03-13 ## Added diff --git a/examples/web/Cargo.toml b/examples/web/Cargo.toml index 42499c6f0..26462efbb 100644 --- a/examples/web/Cargo.toml +++ b/examples/web/Cargo.toml @@ -1,36 +1,37 @@ [package] -name = "web" -version = "0.1.0" authors = ["Tom Houlé "] edition = "2018" +name = "web" +version = "0.1.0" # https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#profile-overrides #[profile.release] #lto = "thin" -[dev-dependencies] -graphql_client = { path = "../../graphql_client" } -graphql_client_web = { path = "../../graphql_client_web" } +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +graphql_client = {path = "../../graphql_client", features = ["web"]} +js-sys = "0.3.44" +lazy_static = "1.4" +serde = {version = "1.0.114", features = ["derive"]} wasm-bindgen = "^0.2" -serde = { version = "1.0.67", features = ["derive"] } -lazy_static = "1.0.1" -js-sys = "0.3.6" -futures = "0.1.25" -wasm-bindgen-futures = "0.3.6" +wasm-bindgen-futures = "0.4.17" -[dev-dependencies.web-sys] -version = "0.3.6" +[dependencies.web-sys] features = [ - "console", - "Document", - "Element", - "EventTarget", - "Node", - "HtmlBodyElement", - "HtmlDocument", - "HtmlElement", + "console", + "Document", + "Element", + "EventTarget", + "Node", + "HtmlBodyElement", + "HtmlDocument", + "HtmlElement", ] +version = "0.3.6" -[[example]] -name = "web" -crate-type = ["cdylib"] +# [[example]] +# name = "web" +# crate-type = ["cdylib"] diff --git a/examples/web/examples/web.rs b/examples/web/src/lib.rs similarity index 76% rename from examples/web/examples/web.rs rename to examples/web/src/lib.rs index 3937e5400..cc7168cc8 100644 --- a/examples/web/examples/web.rs +++ b/examples/web/src/lib.rs @@ -1,16 +1,12 @@ -use futures::Future; -use graphql_client::GraphQLQuery; +use graphql_client::{web, GraphQLQuery, Response}; use lazy_static::*; -use std::cell::RefCell; -use std::sync::Mutex; -use wasm_bindgen::prelude::*; -use wasm_bindgen::JsCast; -use wasm_bindgen_futures::future_to_promise; +use std::{cell::RefCell, sync::Mutex}; +use wasm_bindgen::{prelude::*, JsCast}; #[derive(GraphQLQuery)] #[graphql( schema_path = "schema.json", - query_path = "examples/puppy_smiles.graphql", + query_path = "src/puppy_smiles.graphql", response_derives = "Debug" )] struct PuppySmiles; @@ -23,28 +19,24 @@ lazy_static! { static ref LAST_ENTRY: Mutex>> = Mutex::new(RefCell::new(None)); } -fn load_more() -> impl Future { - let client = graphql_client::web::Client::new("https://www.graphqlhub.com/graphql"); +async fn load_more() -> Result { + let client = web::Client::new("https://www.graphqlhub.com/graphql"); let variables = puppy_smiles::Variables { after: LAST_ENTRY .lock() .ok() .and_then(|opt| opt.borrow().to_owned()), }; - let response = client.call(PuppySmiles, variables); - - response - .map(|response| { - render_response(response); - JsValue::NULL - }) - .map_err(|err| { - log(&format!( - "Could not fetch puppies. graphql_client_web error: {:?}", - err - )); - JsValue::NULL - }) + let response = client.call(PuppySmiles, variables).await.map_err(|err| { + log(&format!( + "Could not fetch puppies. graphql_client_web error: {:?}", + err + )); + JsValue::NULL + })?; + + render_response(response); + Ok(JsValue::NULL) } fn document() -> web_sys::Document { @@ -60,7 +52,12 @@ fn add_load_more_button() { .expect_throw("could not create button"); btn.set_inner_html("I WANT MORE PUPPIES"); let on_click = Closure::wrap( - Box::new(move || future_to_promise(load_more())) as Box js_sys::Promise> + Box::new(move || { + wasm_bindgen_futures::spawn_local(async { + let _ = load_more().await; + }); + JsValue::NULL + }) as Box JsValue>, // Box::new(move || future_to_promise(load_more().boxed())) as Box js_sys::Promise> ); btn.add_event_listener_with_callback( "click", @@ -78,14 +75,14 @@ fn add_load_more_button() { on_click.forget(); } -fn render_response(response: graphql_client_web::Response) { +fn render_response(response: Response) { use std::fmt::Write; log(&format!("response body\n\n{:?}", response)); let parent = document().body().expect_throw("no body"); - let json: graphql_client_web::Response = response; + let json: Response = response; let response = document() .create_element("div") .expect_throw("could not create div"); diff --git a/examples/web/examples/puppy_smiles.graphql b/examples/web/src/puppy_smiles.graphql similarity index 100% rename from examples/web/examples/puppy_smiles.graphql rename to examples/web/src/puppy_smiles.graphql diff --git a/graphql_client/Cargo.toml b/graphql_client/Cargo.toml index 282c6db63..b9cdb5bd9 100644 --- a/graphql_client/Cargo.toml +++ b/graphql_client/Cargo.toml @@ -1,68 +1,25 @@ [package] -name = "graphql_client" -version = "0.9.0" authors = ["Tom Houlé "] -description = "Typed GraphQL requests and responses" -repository = "https://github.com/graphql-rust/graphql-client" -license = "Apache-2.0 OR MIT" -keywords = ["graphql", "api", "web", "webassembly", "wasm"] categories = ["network-programming", "web-programming", "wasm"] +description = "Typed GraphQL requests and responses" edition = "2018" +keywords = ["graphql", "api", "web", "webassembly", "wasm"] +license = "Apache-2.0 OR MIT" +name = "graphql_client" +repository = "https://github.com/graphql-rust/graphql-client" +version = "0.9.0" [dependencies] +anyhow = {version = "1.0", optional = true} doc-comment = "^0.3" -anyhow = { version = "1.0", optional = true } -thiserror = { version = "1.0", optional = true } -graphql_query_derive = { path = "../graphql_query_derive", version = "0.9.0" } +graphql_query_derive = {path = "../graphql_query_derive", version = "0.9.0"} +reqwest = {version = "0.10.7", optional = true} +serde = {version = "^1.0.78", features = ["derive"]} serde_json = "1.0" -serde = { version = "^1.0.78", features = ["derive"] } - -[dependencies.futures] -version = "^0.1" -optional = true - -[dependencies.js-sys] -version = "^0.3" -optional = true - -[dependencies.log] -version = "^0.4" -optional = true - -[dependencies.web-sys] -version = "^0.3" -optional = true -features = [ - "Headers", - "Request", - "RequestInit", - "Response", - "Window", -] - -[dependencies.wasm-bindgen] -version = "^0.2" -optional = true - -[dependencies.wasm-bindgen-futures] -version = "^0.3" -optional = true - -[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] -reqwest = "^0.9" +thiserror = {version = "1.0", optional = true} -[dev-dependencies] -# Note: If we bumpup wasm-bindge-test version, we should change CI setting. -wasm-bindgen-test = "^0.2" +# [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] +# reqwest = "^0.9" [features] -web = [ - "anyhow", - "thiserror", - "futures", - "js-sys", - "log", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] +web = ["reqwest"] diff --git a/graphql_client/src/web.rs b/graphql_client/src/web.rs index 8f4b6031b..918b1d8be 100644 --- a/graphql_client/src/web.rs +++ b/graphql_client/src/web.rs @@ -2,12 +2,7 @@ //! [wasm-bindgen](https://github.com/rustwasm/wasm-bindgen). use crate::*; -use futures::{Future, IntoFuture}; -use log::*; use std::collections::HashMap; -use thiserror::*; -use wasm_bindgen::{JsCast, JsValue}; -use wasm_bindgen_futures::JsFuture; /// The main interface to the library. /// @@ -19,39 +14,7 @@ use wasm_bindgen_futures::JsFuture; pub struct Client { endpoint: String, headers: HashMap, -} - -/// All the ways a request can go wrong. -/// -/// not exhaustive -#[derive(Debug, Error, PartialEq)] -pub enum ClientError { - /// The body couldn't be built - #[error("Request body is not a valid string")] - Body, - /// An error caused by window.fetch - #[error("Network error")] - Network(String), - /// Error in a dynamic JS cast that should have worked - #[error("JS casting error")] - Cast, - /// No window object could be retrieved - #[error( - "No Window object available - the client works only in a browser (non-worker) context" - )] - NoWindow, - /// Response shape does not match the generated code - #[error("Response shape error")] - ResponseShape, - /// Response could not be converted to text - #[error("Response conversion to text failed (Response.text threw)")] - ResponseText, - /// Exception thrown when building the request - #[error("Error building the request")] - RequestError, - /// Other JS exception - #[error("Unexpected JS exception")] - JsException, + reqwest_client: reqwest::Client, } impl Client { @@ -63,6 +26,7 @@ impl Client { Client { endpoint: endpoint.into(), headers: HashMap::new(), + reqwest_client: reqwest::Client::new(), } } @@ -75,71 +39,25 @@ impl Client { /// // Lint disabled: We can pass by value because it's always an empty struct. #[allow(clippy::needless_pass_by_value)] - pub fn call( + pub async fn call( &self, _query: Q, variables: Q::Variables, - ) -> impl Future, Error = ClientError> + 'static { - // this can be removed when we convert to async/await - let endpoint = self.endpoint.clone(); - let custom_headers = self.headers.clone(); - - web_sys::window() - .ok_or_else(|| ClientError::NoWindow) - .into_future() - .and_then(move |window| { - serde_json::to_string(&Q::build_query(variables)) - .map_err(|_| ClientError::Body) - .map(move |body| (window, body)) - }) - .and_then(move |(window, body)| { - let mut request_init = web_sys::RequestInit::new(); - request_init - .method("POST") - .body(Some(&JsValue::from_str(&body))); - - web_sys::Request::new_with_str_and_init(&endpoint, &request_init) - .map_err(|_| ClientError::JsException) - .map(|request| (window, request)) - // "Request constructor threw"); - }) - .and_then(move |(window, request)| { - let headers = request.headers(); - headers - .set("Content-Type", "application/json") - .map_err(|_| ClientError::RequestError)?; - headers - .set("Accept", "application/json") - .map_err(|_| ClientError::RequestError)?; + ) -> Result, reqwest::Error> { + // TODO: remove the unwrap + // TODO: remove tests and test harness + // TODO: custom headers + let reqwest_response = self + .reqwest_client + .post(&self.endpoint) + .header("Content-Type", "application/json") + .body(serde_json::to_string(&Q::build_query(variables)).unwrap()) + .send() + .await?; - for (header_name, header_value) in custom_headers.iter() { - headers - .set(header_name, header_value) - .map_err(|_| ClientError::RequestError)?; - } + let text_response = reqwest_response.text().await?; - Ok((window, request)) - }) - .and_then(move |(window, request)| { - JsFuture::from(window.fetch_with_request(&request)) - .map_err(|err| ClientError::Network(js_sys::Error::from(err).message().into())) - }) - .and_then(move |res| { - debug!("response: {:?}", res); - res.dyn_into::() - .map_err(|_| ClientError::Cast) - }) - .and_then(move |cast_response| { - cast_response.text().map_err(|_| ClientError::ResponseText) - }) - .and_then(move |text_promise| { - JsFuture::from(text_promise).map_err(|_| ClientError::ResponseText) - }) - .and_then(|text| { - let response_text = text.as_string().unwrap_or_default(); - debug!("response text as string: {:?}", response_text); - serde_json::from_str(&response_text).map_err(|_| ClientError::ResponseShape) - }) + Ok(serde_json::from_str(&text_response).unwrap()) } }