async_graphql/http/
graphiql_v2_source.rs

1use std::collections::HashMap;
2
3use handlebars::Handlebars;
4use serde::Serialize;
5
6use crate::http::graphiql_plugin::GraphiQLPlugin;
7
8/// Indicates whether the user agent should send or receive user credentials
9/// (cookies, basic http auth, etc.) from the other domain in the case of
10/// cross-origin requests.
11#[derive(Debug, Serialize, Default)]
12#[serde(rename_all = "kebab-case")]
13pub enum Credentials {
14    /// Send user credentials if the URL is on the same origin as the calling
15    /// script. This is the default value.
16    #[default]
17    SameOrigin,
18    /// Always send user credentials, even for cross-origin calls.
19    Include,
20    /// Never send or receive user credentials.
21    Omit,
22}
23
24/// A builder for constructing a GraphiQL (v2) HTML page.
25///
26/// # Example
27///
28/// ```rust
29/// use async_graphql::http::*;
30///
31/// GraphiQLSource::build()
32///     .endpoint("/")
33///     .subscription_endpoint("/ws")
34///     .header("Authorization", "Bearer [token]")
35///     .ws_connection_param("token", "[token]")
36///     .credentials(Credentials::Include)
37///     .finish();
38/// ```
39#[derive(Default, Serialize)]
40pub struct GraphiQLSource<'a> {
41    endpoint: &'a str,
42    subscription_endpoint: Option<&'a str>,
43    headers: Option<HashMap<&'a str, &'a str>>,
44    ws_connection_params: Option<HashMap<&'a str, &'a str>>,
45    title: Option<&'a str>,
46    credentials: Credentials,
47    plugins: &'a [GraphiQLPlugin<'a>],
48}
49
50impl<'a> GraphiQLSource<'a> {
51    /// Creates a builder for constructing a GraphiQL (v2) HTML page.
52    pub fn build() -> GraphiQLSource<'a> {
53        Default::default()
54    }
55
56    /// Sets the endpoint of the server GraphiQL will connect to.
57    #[must_use]
58    pub fn endpoint(self, endpoint: &'a str) -> GraphiQLSource<'a> {
59        GraphiQLSource { endpoint, ..self }
60    }
61
62    /// Sets the subscription endpoint of the server GraphiQL will connect to.
63    pub fn subscription_endpoint(self, endpoint: &'a str) -> GraphiQLSource<'a> {
64        GraphiQLSource {
65            subscription_endpoint: Some(endpoint),
66            ..self
67        }
68    }
69
70    /// Sets a header to be sent with requests GraphiQL will send.
71    pub fn header(self, name: &'a str, value: &'a str) -> GraphiQLSource<'a> {
72        let mut headers = self.headers.unwrap_or_default();
73        headers.insert(name, value);
74        GraphiQLSource {
75            headers: Some(headers),
76            ..self
77        }
78    }
79
80    /// Sets a WS connection param to be sent during GraphiQL WS connections.
81    pub fn ws_connection_param(self, name: &'a str, value: &'a str) -> GraphiQLSource<'a> {
82        let mut ws_connection_params = self.ws_connection_params.unwrap_or_default();
83        ws_connection_params.insert(name, value);
84        GraphiQLSource {
85            ws_connection_params: Some(ws_connection_params),
86            ..self
87        }
88    }
89
90    /// Sets the html document title.
91    pub fn title(self, title: &'a str) -> GraphiQLSource<'a> {
92        GraphiQLSource {
93            title: Some(title),
94            ..self
95        }
96    }
97
98    /// Sets credentials option for the fetch requests.
99    pub fn credentials(self, credentials: Credentials) -> GraphiQLSource<'a> {
100        GraphiQLSource {
101            credentials,
102            ..self
103        }
104    }
105
106    /// Sets plugins
107    pub fn plugins(self, plugins: &'a [GraphiQLPlugin]) -> GraphiQLSource<'a> {
108        GraphiQLSource { plugins, ..self }
109    }
110
111    /// Returns a GraphiQL (v2) HTML page.
112    pub fn finish(self) -> String {
113        let mut handlebars = Handlebars::new();
114        handlebars
115            .register_template_string(
116                "graphiql_v2_source",
117                include_str!("./graphiql_v2_source.hbs"),
118            )
119            .expect("Failed to register template");
120
121        handlebars
122            .render("graphiql_v2_source", &self)
123            .expect("Failed to render template")
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    #[test]
132    fn test_with_only_url() {
133        let graphiql_source = GraphiQLSource::build().endpoint("/").finish();
134
135        assert_eq!(
136            graphiql_source,
137            r#"<!DOCTYPE html>
138<html lang="en">
139  <head>
140    <meta charset="utf-8">
141    <meta name="robots" content="noindex">
142    <meta name="viewport" content="width=device-width, initial-scale=1">
143    <meta name="referrer" content="origin">
144
145    <title>GraphiQL IDE</title>
146
147    <style>
148      body {
149        height: 100%;
150        margin: 0;
151        width: 100%;
152        overflow: hidden;
153      }
154
155      #graphiql {
156        height: 100vh;
157      }
158    </style>
159    <script
160      crossorigin
161      src="https://unpkg.com/react@17/umd/react.development.js"
162    ></script>
163    <script
164      crossorigin
165      src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"
166    ></script>
167    <link rel="icon" href="https://graphql.org/favicon.ico">
168    <link rel="stylesheet" href="https://unpkg.com/graphiql/graphiql.min.css" />
169  </head>
170
171  <body>
172    <div id="graphiql">Loading...</div>
173    <script
174      src="https://unpkg.com/graphiql/graphiql.min.js"
175      type="application/javascript"
176    ></script>
177    <script>
178      customFetch = (url, opts = {}) => {
179        return fetch(url, {...opts, credentials: 'same-origin'})
180      }
181
182      createUrl = (endpoint, subscription = false) => {
183        const url = new URL(endpoint, window.location.origin);
184        if (subscription) {
185          url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
186        }
187        return url.toString();
188      }
189
190      ReactDOM.render(
191        React.createElement(GraphiQL, {
192          fetcher: GraphiQL.createFetcher({
193            url: createUrl('/'),
194            fetch: customFetch,
195          }),
196          defaultEditorToolsVisibility: true,
197        }),
198        document.getElementById("graphiql")
199      );
200    </script>
201  </body>
202</html>"#
203        )
204    }
205
206    #[test]
207    fn test_with_both_urls() {
208        let graphiql_source = GraphiQLSource::build()
209            .endpoint("/")
210            .subscription_endpoint("/ws")
211            .finish();
212
213        assert_eq!(
214            graphiql_source,
215            r#"<!DOCTYPE html>
216<html lang="en">
217  <head>
218    <meta charset="utf-8">
219    <meta name="robots" content="noindex">
220    <meta name="viewport" content="width=device-width, initial-scale=1">
221    <meta name="referrer" content="origin">
222
223    <title>GraphiQL IDE</title>
224
225    <style>
226      body {
227        height: 100%;
228        margin: 0;
229        width: 100%;
230        overflow: hidden;
231      }
232
233      #graphiql {
234        height: 100vh;
235      }
236    </style>
237    <script
238      crossorigin
239      src="https://unpkg.com/react@17/umd/react.development.js"
240    ></script>
241    <script
242      crossorigin
243      src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"
244    ></script>
245    <link rel="icon" href="https://graphql.org/favicon.ico">
246    <link rel="stylesheet" href="https://unpkg.com/graphiql/graphiql.min.css" />
247  </head>
248
249  <body>
250    <div id="graphiql">Loading...</div>
251    <script
252      src="https://unpkg.com/graphiql/graphiql.min.js"
253      type="application/javascript"
254    ></script>
255    <script>
256      customFetch = (url, opts = {}) => {
257        return fetch(url, {...opts, credentials: 'same-origin'})
258      }
259
260      createUrl = (endpoint, subscription = false) => {
261        const url = new URL(endpoint, window.location.origin);
262        if (subscription) {
263          url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
264        }
265        return url.toString();
266      }
267
268      ReactDOM.render(
269        React.createElement(GraphiQL, {
270          fetcher: GraphiQL.createFetcher({
271            url: createUrl('/'),
272            fetch: customFetch,
273            subscriptionUrl: createUrl('/ws', true),
274          }),
275          defaultEditorToolsVisibility: true,
276        }),
277        document.getElementById("graphiql")
278      );
279    </script>
280  </body>
281</html>"#
282        )
283    }
284
285    #[test]
286    fn test_with_all_options() {
287        use crate::http::graphiql_plugin_explorer;
288        let graphiql_source = GraphiQLSource::build()
289            .endpoint("/")
290            .subscription_endpoint("/ws")
291            .header("Authorization", "Bearer [token]")
292            .ws_connection_param("token", "[token]")
293            .title("Awesome GraphiQL IDE Test")
294            .credentials(Credentials::Include)
295            .plugins(&[graphiql_plugin_explorer()])
296            .finish();
297
298        assert_eq!(
299            graphiql_source,
300            r#"<!DOCTYPE html>
301<html lang="en">
302  <head>
303    <meta charset="utf-8">
304    <meta name="robots" content="noindex">
305    <meta name="viewport" content="width=device-width, initial-scale=1">
306    <meta name="referrer" content="origin">
307
308    <title>Awesome GraphiQL IDE Test</title>
309
310    <style>
311      body {
312        height: 100%;
313        margin: 0;
314        width: 100%;
315        overflow: hidden;
316      }
317
318      #graphiql {
319        height: 100vh;
320      }
321    </style>
322    <script
323      crossorigin
324      src="https://unpkg.com/react@17/umd/react.development.js"
325    ></script>
326    <script
327      crossorigin
328      src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"
329    ></script>
330    <link rel="icon" href="https://graphql.org/favicon.ico">
331    <link rel="stylesheet" href="https://unpkg.com/graphiql/graphiql.min.css" />
332    <link rel="stylesheet" href="https://unpkg.com/@graphiql/plugin-explorer/dist/style.css" />
333  </head>
334
335  <body>
336    <div id="graphiql">Loading...</div>
337    <script
338      src="https://unpkg.com/graphiql/graphiql.min.js"
339      type="application/javascript"
340    ></script>
341    <script
342      src="https://unpkg.com/@graphiql/plugin-explorer/dist/index.umd.js"
343      crossorigin
344    ></script>
345    <script>
346      customFetch = (url, opts = {}) => {
347        return fetch(url, {...opts, credentials: 'include'})
348      }
349
350      createUrl = (endpoint, subscription = false) => {
351        const url = new URL(endpoint, window.location.origin);
352        if (subscription) {
353          url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
354        }
355        return url.toString();
356      }
357
358      const plugins = [];
359      plugins.push(GraphiQLPluginExplorer.explorerPlugin());
360
361      ReactDOM.render(
362        React.createElement(GraphiQL, {
363          fetcher: GraphiQL.createFetcher({
364            url: createUrl('/'),
365            fetch: customFetch,
366            subscriptionUrl: createUrl('/ws', true),
367            headers: {
368              'Authorization': 'Bearer [token]',
369            },
370            wsConnectionParams: {
371              'token': '[token]',
372            },
373          }),
374          defaultEditorToolsVisibility: true,
375          plugins,
376        }),
377        document.getElementById("graphiql")
378      );
379    </script>
380  </body>
381</html>"#
382        )
383    }
384}