Skip to content

Commit c183af0

Browse files
committed
Add a graphql_client_web crate for browser usage
1 parent 1d0ccd5 commit c183af0

File tree

8 files changed

+1433
-2
lines changed

8 files changed

+1433
-2
lines changed

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
[workspace]
22
members = [
3+
"graphql_client_cli",
4+
"graphql_client_codegen",
35
"graphql_client",
46
"graphql_client/examples/example_module",
57
"graphql_client/examples/github",
68
"graphql_query_derive",
7-
"graphql_client_codegen",
8-
"graphql_client_cli",
99
]

graphql_client_web/.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'

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: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
[package]
2+
name = "graphql_client_web"
3+
version = "0.1.0"
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.25"
17+
wasm-bindgen-futures = "0.3.2"
18+
19+
[dependencies.web-sys]
20+
version = "0.3.2"
21+
features = [
22+
"Headers",
23+
"Request",
24+
"RequestInit",
25+
"Response",
26+
"Window",
27+
]
28+
29+
[dev-dependencies]
30+
serde = "*"
31+
serde_derive = "*"
32+
wasm-bindgen-test = "0.2.25"
33+
34+
[workspace]
35+
members = ["."]

graphql_client_web/src/lib.rs

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
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+
#[macro_use]
9+
pub extern crate graphql_client;
10+
#[macro_use]
11+
extern crate wasm_bindgen;
12+
13+
use failure::*;
14+
use futures::{Future, IntoFuture};
15+
pub use graphql_client::GraphQLQuery;
16+
use log::*;
17+
use wasm_bindgen::JsCast;
18+
use wasm_bindgen::JsValue;
19+
use wasm_bindgen_futures::JsFuture;
20+
21+
/// The main interface to the library.
22+
///
23+
/// The workflow is the following:
24+
///
25+
/// - create a client
26+
/// - (optionally) configure it
27+
/// - use it to perform queries with the [call] method
28+
pub struct Client {
29+
endpoint: String,
30+
}
31+
32+
/// All the ways a request can go wrong.
33+
///
34+
/// not exhaustive
35+
#[derive(Debug, Fail)]
36+
pub enum ClientError {
37+
/// The body couldn't be built
38+
#[fail(display = "Request body is not a valid string")]
39+
Body,
40+
/// An error caused by window.fetch
41+
#[fail(display = "Network error")]
42+
Network,
43+
/// Error in a dynamic JS cast that should have worked
44+
#[fail(display = "JS casting error")]
45+
Cast,
46+
/// No window object could be retrieved
47+
#[fail(
48+
display = "No Window object available - the client works only in a browser (non-worker) context"
49+
)]
50+
NoWindow,
51+
/// Response shape does not match the generated code
52+
#[fail(display = "Response shape error",)]
53+
ResponseShape,
54+
/// Response could not be converted to text
55+
#[fail(display = "Response conversion to text failed (Response.text threw)")]
56+
ResponseText,
57+
/// Exception thrown when building the request
58+
#[fail(display = "Error building the request",)]
59+
RequestError,
60+
/// Other JS exception
61+
#[fail(display = "Unexpected JS exception")]
62+
JsException,
63+
}
64+
65+
impl Client {
66+
/// Initialize a client. The `endpoint` parameter is the URI of the GraphQL API.
67+
pub fn new<Endpoint>(endpoint: Endpoint) -> Client
68+
where
69+
Endpoint: Into<String>,
70+
{
71+
Client {
72+
endpoint: endpoint.into(),
73+
}
74+
}
75+
76+
/// Perform a query.
77+
pub fn call<Q: GraphQLQuery + 'static>(
78+
&self,
79+
_query: Q,
80+
variables: Q::Variables,
81+
) -> impl Future<Item = graphql_client::Response<Q::ResponseData>, Error = ClientError> + 'static
82+
{
83+
// this can be removed when we convert to async/await
84+
let endpoint = self.endpoint.clone();
85+
86+
web_sys::window()
87+
.ok_or_else(|| ClientError::NoWindow)
88+
.into_future()
89+
.and_then(move |window| {
90+
serde_json::to_string(&Q::build_query(variables))
91+
.map_err(|_| ClientError::Body)
92+
.map(move |body| (window, body))
93+
}).and_then(move |(window, body)| {
94+
let mut request_init = web_sys::RequestInit::new();
95+
request_init
96+
.method("POST")
97+
.body(Some(&JsValue::from_str(&body)));
98+
99+
web_sys::Request::new_with_str_and_init(&endpoint, &request_init)
100+
.map_err(|_| ClientError::JsException)
101+
.map(|request| (window, request))
102+
// "Request constructor threw");
103+
}).and_then(move |(window, request)| {
104+
let request: Result<web_sys::Request, _> = request
105+
.headers()
106+
.set("Content-Type", "application/json")
107+
.map_err(|_| ClientError::RequestError)
108+
.map(|_| request);
109+
110+
let request: Result<web_sys::Request, _> = request.and_then(|req| {
111+
req.headers()
112+
.set("Accept", "application/json")
113+
.map_err(|_| ClientError::RequestError)
114+
.map(|_| req)
115+
});
116+
117+
request.map(move |request| (window, request))
118+
}).and_then(move |(window, request)| {
119+
JsFuture::from(window.fetch_with_request(&request))
120+
.map_err(|_| ClientError::Network)
121+
}).and_then(move |res| {
122+
debug!("response: {:?}", res);
123+
res.dyn_into::<web_sys::Response>()
124+
.map_err(|_| ClientError::Cast)
125+
}).and_then(move |cast_response| {
126+
cast_response.text().map_err(|_| ClientError::ResponseText)
127+
}).and_then(move |text_promise| {
128+
JsFuture::from(text_promise).map_err(|_| ClientError::ResponseText)
129+
}).and_then(|text| {
130+
let response_text = text.as_string().unwrap_or_else(|| String::new());
131+
debug!("response text as string: {:?}", response_text);
132+
serde_json::from_str(&response_text).map_err(|_| ClientError::ResponseShape)
133+
})
134+
}
135+
}
136+
137+
#[cfg(test)]
138+
mod tests {
139+
use super::*;
140+
141+
#[test]
142+
fn client_new() {
143+
Client::new("https://example.com/graphql");
144+
Client::new("/graphql");
145+
}
146+
}
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)