|
| 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 | +} |
0 commit comments