Skip to content

Commit c2128f9

Browse files
authored
Merge pull request #174 from graphql-rust/graphql-client-web
Add a graphql_client_web crate for browser usage
2 parents 88913dc + 62ac7e9 commit c2128f9

File tree

10 files changed

+1484
-2
lines changed

10 files changed

+1484
-2
lines changed

.cargo/config

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[target.wasm32-unknown-unknown]
2+
runner = 'wasm-bindgen-test-runner'

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@ node_modules/
33
**/*.rs.bk
44
Cargo.lock
55
.idea
6+
scripts/*
7+
!scripts/*.sh

.travis.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,25 @@ rust:
44
- beta
55
- nightly
66
cache: cargo
7+
addons:
8+
firefox: latest
9+
before_install:
10+
- if [ "$TRAVIS_RUST_VERSION" = "stable" ]; then (wget https://github.com/mozilla/geckodriver/releases/download/v0.23.0/geckodriver-v0.23.0-linux64.tar.gz) fi
11+
- if [ "$TRAVIS_RUST_VERSION" = "stable" ]; then (mkdir geckodriver) fi
12+
- if [ "$TRAVIS_RUST_VERSION" = "stable" ]; then (tar -xzf geckodriver-v0.23.0-linux64.tar.gz -C geckodriver) fi
13+
- export PATH=$PATH:$PWD/geckodriver
714
before_script:
815
- sudo apt-get update
916
- sudo apt-get install -y nodejs
1017
- npm i -g prettier
1118
- if [ "$TRAVIS_RUST_VERSION" = "stable" ]; then (rustup component add rustfmt-preview clippy-preview) fi
19+
- if [ "$TRAVIS_RUST_VERSION" = "stable" ]; then (rustup target add wasm32-unknown-unknown) fi
20+
- if [ "$TRAVIS_RUST_VERSION" = "stable" ]; then (cargo install -f wasm-bindgen-cli) fi
1221
script:
1322
- if [ "$TRAVIS_RUST_VERSION" = "stable" ]; then (cargo fmt --all -- --check) fi
1423
- if [ "$TRAVIS_RUST_VERSION" = "stable" ]; then (cargo clippy -- -D warnings) fi
1524
- prettier --debug-check -l './**/*.json' './**/*.graphql'
1625
- cargo test --all
1726
- cargo build --manifest-path=./graphql_client/examples/github/Cargo.toml
1827
- cargo build --manifest-path=./graphql_client_cli/Cargo.toml
28+
- if [ "$TRAVIS_RUST_VERSION" = "stable" ]; then (xvfb-run cargo test --manifest-path=./graphql_client_web/Cargo.toml --target wasm32-unknown-unknown) fi

Cargo.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
[workspace]
22
members = [
3+
"graphql_client_cli",
4+
"graphql_client_codegen",
35
"graphql_client",
6+
"graphql_client_web",
47
"graphql_client/examples/example_module",
58
"graphql_client/examples/github",
69
"graphql_query_derive",
7-
"graphql_client_codegen",
8-
"graphql_client_cli",
910
]

graphql_client_web/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/wasm-pack.log
2+
/bin

graphql_client_web/Cargo.toml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
[package]
2+
name = "graphql_client_web"
3+
version = "0.5.1"
4+
authors = ["Tom Houlé <[email protected]>"]
5+
edition = "2018"
6+
7+
[lib]
8+
crate-type = ["cdylib", "rlib"]
9+
10+
[dependencies]
11+
failure = "0.1.2"
12+
futures = "*"
13+
graphql_client = { path = "../graphql_client", version = "0.5.1" }
14+
log = "0.4.5"
15+
serde_json = "1.0.32"
16+
wasm-bindgen = "0.2.29"
17+
wasm-bindgen-futures = "0.3.2"
18+
js-sys = "0.3.5"
19+
20+
[dependencies.web-sys]
21+
version = "0.3.2"
22+
features = [
23+
"Headers",
24+
"Request",
25+
"RequestInit",
26+
"Response",
27+
"Window",
28+
]
29+
30+
[dev-dependencies]
31+
serde = "*"
32+
serde_derive = "*"
33+
wasm-bindgen-test = "0.2.25"

graphql_client_web/src/lib.rs

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
//! Use graphql_client inside browsers with [wasm-bindgen].
2+
//!
3+
//! This crate reexports all you need from graphql-client, so you do not need any other explicit dependencies.
4+
5+
#![deny(warnings)]
6+
#![deny(missing_docs)]
7+
8+
use failure::*;
9+
use futures::{Future, IntoFuture};
10+
pub use graphql_client::{self, GraphQLQuery};
11+
use log::*;
12+
use std::collections::HashMap;
13+
use wasm_bindgen::{JsCast, JsValue};
14+
use wasm_bindgen_futures::JsFuture;
15+
16+
/// The main interface to the library.
17+
///
18+
/// The workflow is the following:
19+
///
20+
/// - create a client
21+
/// - (optionally) configure it
22+
/// - use it to perform queries with the [call] method
23+
pub struct Client {
24+
endpoint: String,
25+
headers: HashMap<String, String>,
26+
}
27+
28+
/// All the ways a request can go wrong.
29+
///
30+
/// not exhaustive
31+
#[derive(Debug, Fail, PartialEq)]
32+
pub enum ClientError {
33+
/// The body couldn't be built
34+
#[fail(display = "Request body is not a valid string")]
35+
Body,
36+
/// An error caused by window.fetch
37+
#[fail(display = "Network error")]
38+
Network(String),
39+
/// Error in a dynamic JS cast that should have worked
40+
#[fail(display = "JS casting error")]
41+
Cast,
42+
/// No window object could be retrieved
43+
#[fail(
44+
display = "No Window object available - the client works only in a browser (non-worker) context"
45+
)]
46+
NoWindow,
47+
/// Response shape does not match the generated code
48+
#[fail(display = "Response shape error")]
49+
ResponseShape,
50+
/// Response could not be converted to text
51+
#[fail(display = "Response conversion to text failed (Response.text threw)")]
52+
ResponseText,
53+
/// Exception thrown when building the request
54+
#[fail(display = "Error building the request")]
55+
RequestError,
56+
/// Other JS exception
57+
#[fail(display = "Unexpected JS exception")]
58+
JsException,
59+
}
60+
61+
impl Client {
62+
/// Initialize a client. The `endpoint` parameter is the URI of the GraphQL API.
63+
pub fn new<Endpoint>(endpoint: Endpoint) -> Client
64+
where
65+
Endpoint: Into<String>,
66+
{
67+
Client {
68+
endpoint: endpoint.into(),
69+
headers: HashMap::new(),
70+
}
71+
}
72+
73+
/// Add a header to those sent with the requests. Can be used for things like authorization.
74+
pub fn add_header(&mut self, name: &str, value: &str) {
75+
self.headers.insert(name.into(), value.into());
76+
}
77+
78+
/// Perform a query.
79+
///
80+
// Lint disabled: We can pass by value because it's always an empty struct.
81+
#[allow(clippy::needless_pass_by_value)]
82+
pub fn call<Q: GraphQLQuery + 'static>(
83+
&self,
84+
_query: Q,
85+
variables: Q::Variables,
86+
) -> impl Future<Item = graphql_client::Response<Q::ResponseData>, Error = ClientError> + 'static
87+
{
88+
// this can be removed when we convert to async/await
89+
let endpoint = self.endpoint.clone();
90+
let custom_headers = self.headers.clone();
91+
92+
web_sys::window()
93+
.ok_or_else(|| ClientError::NoWindow)
94+
.into_future()
95+
.and_then(move |window| {
96+
serde_json::to_string(&Q::build_query(variables))
97+
.map_err(|_| ClientError::Body)
98+
.map(move |body| (window, body))
99+
})
100+
.and_then(move |(window, body)| {
101+
let mut request_init = web_sys::RequestInit::new();
102+
request_init
103+
.method("POST")
104+
.body(Some(&JsValue::from_str(&body)));
105+
106+
web_sys::Request::new_with_str_and_init(&endpoint, &request_init)
107+
.map_err(|_| ClientError::JsException)
108+
.map(|request| (window, request))
109+
// "Request constructor threw");
110+
})
111+
.and_then(move |(window, request)| {
112+
let headers = request.headers();
113+
headers
114+
.set("Content-Type", "application/json")
115+
.map_err(|_| ClientError::RequestError)?;
116+
headers
117+
.set("Accept", "application/json")
118+
.map_err(|_| ClientError::RequestError)?;
119+
120+
for (header_name, header_value) in custom_headers.iter() {
121+
headers
122+
.set(header_name, header_value)
123+
.map_err(|_| ClientError::RequestError)?;
124+
}
125+
126+
Ok((window, request))
127+
})
128+
.and_then(move |(window, request)| {
129+
JsFuture::from(window.fetch_with_request(&request))
130+
.map_err(|err| ClientError::Network(js_sys::Error::from(err).message().into()))
131+
})
132+
.and_then(move |res| {
133+
debug!("response: {:?}", res);
134+
res.dyn_into::<web_sys::Response>()
135+
.map_err(|_| ClientError::Cast)
136+
})
137+
.and_then(move |cast_response| {
138+
cast_response.text().map_err(|_| ClientError::ResponseText)
139+
})
140+
.and_then(move |text_promise| {
141+
JsFuture::from(text_promise).map_err(|_| ClientError::ResponseText)
142+
})
143+
.and_then(|text| {
144+
let response_text = text.as_string().unwrap_or_default();
145+
debug!("response text as string: {:?}", response_text);
146+
serde_json::from_str(&response_text).map_err(|_| ClientError::ResponseShape)
147+
})
148+
}
149+
}
150+
151+
#[cfg(test)]
152+
mod tests {
153+
use super::*;
154+
155+
#[test]
156+
fn client_new() {
157+
Client::new("https://example.com/graphql");
158+
Client::new("/graphql");
159+
}
160+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
query Germany {
2+
country(code: "DE") {
3+
name
4+
continent {
5+
name
6+
}
7+
}
8+
}
9+
10+
query Country($countryCode: String!) {
11+
country(code: $countryCode) {
12+
name
13+
continent {
14+
name
15+
}
16+
}
17+
}

0 commit comments

Comments
 (0)