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