Skip to content

[WIP] [web] Replace custom HTTP client with external crate #327

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
47 changes: 24 additions & 23 deletions examples/web/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,36 +1,37 @@
[package]
name = "web"
version = "0.1.0"
authors = ["Tom Houlé <tom@tomhoule.com>"]
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"]
51 changes: 24 additions & 27 deletions examples/web/examples/web.rs → examples/web/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -23,28 +19,24 @@ lazy_static! {
static ref LAST_ENTRY: Mutex<RefCell<Option<String>>> = Mutex::new(RefCell::new(None));
}

fn load_more() -> impl Future<Item = JsValue, Error = JsValue> {
let client = graphql_client::web::Client::new("https://www.graphqlhub.com/graphql");
async fn load_more() -> Result<JsValue, JsValue> {
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 {
Expand All @@ -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<dyn FnMut() -> js_sys::Promise>
Box::new(move || {
wasm_bindgen_futures::spawn_local(async {
let _ = load_more().await;
});
JsValue::NULL
}) as Box<dyn FnMut() -> JsValue>, // Box::new(move || future_to_promise(load_more().boxed())) as Box<dyn FnMut() -> js_sys::Promise>
);
btn.add_event_listener_with_callback(
"click",
Expand All @@ -78,14 +75,14 @@ fn add_load_more_button() {
on_click.forget();
}

fn render_response(response: graphql_client_web::Response<puppy_smiles::ResponseData>) {
fn render_response(response: Response<puppy_smiles::ResponseData>) {
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<puppy_smiles::ResponseData> = response;
let json: Response<puppy_smiles::ResponseData> = response;
let response = document()
.create_element("div")
.expect_throw("could not create div");
Expand Down
71 changes: 14 additions & 57 deletions graphql_client/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,68 +1,25 @@
[package]
name = "graphql_client"
version = "0.9.0"
authors = ["Tom Houlé <tom@tomhoule.com>"]
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"]
114 changes: 16 additions & 98 deletions graphql_client/src/web.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand All @@ -19,39 +14,7 @@ use wasm_bindgen_futures::JsFuture;
pub struct Client {
endpoint: String,
headers: HashMap<String, String>,
}

/// 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 {
Expand All @@ -63,6 +26,7 @@ impl Client {
Client {
endpoint: endpoint.into(),
headers: HashMap::new(),
reqwest_client: reqwest::Client::new(),
}
}

Expand All @@ -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<Q: GraphQLQuery + 'static>(
pub async fn call<Q: GraphQLQuery + 'static>(
&self,
_query: Q,
variables: Q::Variables,
) -> impl Future<Item = crate::Response<Q::ResponseData>, 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<crate::Response<Q::ResponseData>, 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::<web_sys::Response>()
.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())
}
}

Expand Down