async_graphql/http/
graphiql_source.rs

1use std::{collections::HashMap, fmt};
2
3use askama::Template;
4
5/// Indicates whether the user agent should send or receive user credentials
6/// (cookies, basic http auth, etc.) from the other domain in the case of
7/// cross-origin requests.
8#[derive(Debug, Default)]
9pub enum Credentials {
10    /// Send user credentials if the URL is on the same origin as the calling
11    /// script. This is the default value.
12    #[default]
13    SameOrigin,
14    /// Always send user credentials, even for cross-origin calls.
15    Include,
16    /// Never send or receive user credentials.
17    Omit,
18}
19
20impl fmt::Display for Credentials {
21    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
22        match self {
23            Self::SameOrigin => write!(f, "same-origin"),
24            Self::Include => write!(f, "include"),
25            Self::Omit => write!(f, "omit"),
26        }
27    }
28}
29
30struct GraphiQLVersion<'a>(&'a str);
31
32impl Default for GraphiQLVersion<'_> {
33    fn default() -> Self {
34        Self("5.2.2")
35    }
36}
37
38impl fmt::Display for GraphiQLVersion<'_> {
39    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40        self.0.fmt(f)
41    }
42}
43
44/// A builder for constructing a GraphiQL (v2) HTML page.
45///
46/// # Example
47///
48/// ```rust
49/// use async_graphql::http::*;
50///
51/// GraphiQLSource::build()
52///     .endpoint("/")
53///     .subscription_endpoint("/ws")
54///     .header("Authorization", "Bearer [token]")
55///     .ws_connection_param("token", "[token]")
56///     .credentials(Credentials::Include)
57///     .finish();
58/// ```
59#[derive(Default, Template)]
60#[template(path = "graphiql_source.jinja")]
61pub struct GraphiQLSource<'a> {
62    endpoint: &'a str,
63    subscription_endpoint: Option<&'a str>,
64    version: GraphiQLVersion<'a>,
65    headers: Option<HashMap<&'a str, &'a str>>,
66    ws_connection_params: Option<HashMap<&'a str, &'a str>>,
67    title: Option<&'a str>,
68    credentials: Credentials,
69}
70
71impl<'a> GraphiQLSource<'a> {
72    /// Creates a builder for constructing a GraphiQL (v2) HTML page.
73    pub fn build() -> GraphiQLSource<'a> {
74        Default::default()
75    }
76
77    /// Sets the endpoint of the server GraphiQL will connect to.
78    #[must_use]
79    pub fn endpoint(self, endpoint: &'a str) -> GraphiQLSource<'a> {
80        GraphiQLSource { endpoint, ..self }
81    }
82
83    /// Sets the subscription endpoint of the server GraphiQL will connect to.
84    pub fn subscription_endpoint(self, endpoint: &'a str) -> GraphiQLSource<'a> {
85        GraphiQLSource {
86            subscription_endpoint: Some(endpoint),
87            ..self
88        }
89    }
90
91    /// Sets a header to be sent with requests GraphiQL will send.
92    pub fn header(self, name: &'a str, value: &'a str) -> GraphiQLSource<'a> {
93        let mut headers = self.headers.unwrap_or_default();
94        headers.insert(name, value);
95        GraphiQLSource {
96            headers: Some(headers),
97            ..self
98        }
99    }
100
101    /// Sets the version of GraphiQL to be fetched.
102    pub fn version(self, value: &'a str) -> GraphiQLSource<'a> {
103        GraphiQLSource {
104            version: GraphiQLVersion(value),
105            ..self
106        }
107    }
108
109    /// Sets a WS connection param to be sent during GraphiQL WS connections.
110    pub fn ws_connection_param(self, name: &'a str, value: &'a str) -> GraphiQLSource<'a> {
111        let mut ws_connection_params = self.ws_connection_params.unwrap_or_default();
112        ws_connection_params.insert(name, value);
113        GraphiQLSource {
114            ws_connection_params: Some(ws_connection_params),
115            ..self
116        }
117    }
118
119    /// Sets the html document title.
120    pub fn title(self, title: &'a str) -> GraphiQLSource<'a> {
121        GraphiQLSource {
122            title: Some(title),
123            ..self
124        }
125    }
126
127    /// Sets credentials option for the fetch requests.
128    pub fn credentials(self, credentials: Credentials) -> GraphiQLSource<'a> {
129        GraphiQLSource {
130            credentials,
131            ..self
132        }
133    }
134
135    /// Returns a GraphiQL (v2) HTML page.
136    pub fn finish(self) -> String {
137        self.render().expect("Failed to render template")
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use expect_test::expect;
144
145    use super::*;
146
147    #[test]
148    fn test_with_only_url() {
149        let graphiql_source = GraphiQLSource::build().endpoint("/").finish();
150        let mut expected = expect![[r#"
151<!DOCTYPE html>
152<html lang="en">
153  <head>
154    <meta charset="UTF-8" />
155    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
156    <meta name="robots" content="noindex">
157    <meta name="referrer" content="origin">
158
159    
160      <title>GraphiQL</title>
161    
162    
163    <style>
164      body {
165        margin: 0;
166      }
167
168      #graphiql {
169        height: 100dvh;
170      }
171
172      .loading {
173        height: 100%;
174        display: flex;
175        align-items: center;
176        justify-content: center;
177        font-size: 4rem;
178      }
179    </style>
180
181    <link rel="stylesheet" href="https://esm.sh/graphiql/dist/style.css" />
182    <link
183      rel="stylesheet"
184      href="https://esm.sh/@graphiql/plugin-explorer/dist/style.css"
185    />
186
187    <script type="importmap">
188      {
189        "imports": {
190          "react": "https://esm.sh/react@19.1.0",
191          "react/": "https://esm.sh/react@19.1.0/",
192
193          "react-dom": "https://esm.sh/react-dom@19.1.0",
194          "react-dom/": "https://esm.sh/react-dom@19.1.0/",
195
196          "graphiql": "https://esm.sh/graphiql@5.2.2?standalone&external=react,react-dom,@graphiql/react,graphql",
197          "graphiql/": "https://esm.sh/graphiql@5.2.2/",
198          "@graphiql/plugin-explorer": "https://esm.sh/@graphiql/plugin-explorer?standalone&external=react,@graphiql/react,graphql",
199          "@graphiql/react": "https://esm.sh/@graphiql/react?standalone&external=react,react-dom,graphql,@graphiql/toolkit,@emotion/is-prop-valid",
200
201          "@graphiql/toolkit": "https://esm.sh/@graphiql/toolkit?standalone&external=graphql",
202          "graphql": "https://esm.sh/graphql@16.11.0",
203          "@emotion/is-prop-valid": "data:text/javascript,"
204        }
205      }
206    </script>
207
208    <script type="module">
209      import React from 'react';
210      import ReactDOM from 'react-dom/client';
211      import { GraphiQL, HISTORY_PLUGIN } from 'graphiql';
212      import { createGraphiQLFetcher } from '@graphiql/toolkit';
213      import { explorerPlugin } from '@graphiql/plugin-explorer';
214      import 'graphiql/setup-workers/esm.sh';
215
216      const customFetch = (url, opts = {}) => {
217        return fetch(url, {...opts, credentials: 'same-origin'})
218      }
219
220      const createUrl = (endpoint, subscription = false) => {
221        const url = new URL(endpoint, window.location.origin);
222        if (subscription) {
223          url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
224        }
225        return url.toString();
226      }
227
228      const fetcher = createGraphiQLFetcher({
229        url: createUrl('/'),
230        fetch: customFetch,
231        
232        
233        
234      });
235      const plugins = [HISTORY_PLUGIN, explorerPlugin()];
236
237      function App() {
238        return React.createElement(GraphiQL, {
239          fetcher,
240          plugins,
241          defaultEditorToolsVisibility: true,
242        });
243      }
244
245      const container = document.getElementById('graphiql');
246      const root = ReactDOM.createRoot(container);
247      root.render(React.createElement(App));
248    </script>
249  </head>
250  <body>
251    <div id="graphiql">
252      <div class="loading">Loading…</div>
253    </div>
254  </body>
255</html>"#]];
256
257        expected.indent(false);
258        expected.assert_eq(&graphiql_source);
259    }
260
261    #[test]
262    fn test_with_both_urls() {
263        let graphiql_source = GraphiQLSource::build()
264            .endpoint("/")
265            .subscription_endpoint("/ws")
266            .finish();
267
268        let mut expected = expect![[r#"
269<!DOCTYPE html>
270<html lang="en">
271  <head>
272    <meta charset="UTF-8" />
273    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
274    <meta name="robots" content="noindex">
275    <meta name="referrer" content="origin">
276
277    
278      <title>GraphiQL</title>
279    
280    
281    <style>
282      body {
283        margin: 0;
284      }
285
286      #graphiql {
287        height: 100dvh;
288      }
289
290      .loading {
291        height: 100%;
292        display: flex;
293        align-items: center;
294        justify-content: center;
295        font-size: 4rem;
296      }
297    </style>
298
299    <link rel="stylesheet" href="https://esm.sh/graphiql/dist/style.css" />
300    <link
301      rel="stylesheet"
302      href="https://esm.sh/@graphiql/plugin-explorer/dist/style.css"
303    />
304
305    <script type="importmap">
306      {
307        "imports": {
308          "react": "https://esm.sh/react@19.1.0",
309          "react/": "https://esm.sh/react@19.1.0/",
310
311          "react-dom": "https://esm.sh/react-dom@19.1.0",
312          "react-dom/": "https://esm.sh/react-dom@19.1.0/",
313
314          "graphiql": "https://esm.sh/graphiql@5.2.2?standalone&external=react,react-dom,@graphiql/react,graphql",
315          "graphiql/": "https://esm.sh/graphiql@5.2.2/",
316          "@graphiql/plugin-explorer": "https://esm.sh/@graphiql/plugin-explorer?standalone&external=react,@graphiql/react,graphql",
317          "@graphiql/react": "https://esm.sh/@graphiql/react?standalone&external=react,react-dom,graphql,@graphiql/toolkit,@emotion/is-prop-valid",
318
319          "@graphiql/toolkit": "https://esm.sh/@graphiql/toolkit?standalone&external=graphql",
320          "graphql": "https://esm.sh/graphql@16.11.0",
321          "@emotion/is-prop-valid": "data:text/javascript,"
322        }
323      }
324    </script>
325
326    <script type="module">
327      import React from 'react';
328      import ReactDOM from 'react-dom/client';
329      import { GraphiQL, HISTORY_PLUGIN } from 'graphiql';
330      import { createGraphiQLFetcher } from '@graphiql/toolkit';
331      import { explorerPlugin } from '@graphiql/plugin-explorer';
332      import 'graphiql/setup-workers/esm.sh';
333
334      const customFetch = (url, opts = {}) => {
335        return fetch(url, {...opts, credentials: 'same-origin'})
336      }
337
338      const createUrl = (endpoint, subscription = false) => {
339        const url = new URL(endpoint, window.location.origin);
340        if (subscription) {
341          url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
342        }
343        return url.toString();
344      }
345
346      const fetcher = createGraphiQLFetcher({
347        url: createUrl('/'),
348        fetch: customFetch,
349        
350        subscriptionUrl: createUrl('/ws'),
351        
352        
353        
354      });
355      const plugins = [HISTORY_PLUGIN, explorerPlugin()];
356
357      function App() {
358        return React.createElement(GraphiQL, {
359          fetcher,
360          plugins,
361          defaultEditorToolsVisibility: true,
362        });
363      }
364
365      const container = document.getElementById('graphiql');
366      const root = ReactDOM.createRoot(container);
367      root.render(React.createElement(App));
368    </script>
369  </head>
370  <body>
371    <div id="graphiql">
372      <div class="loading">Loading…</div>
373    </div>
374  </body>
375</html>"#]];
376
377        expected.indent(false);
378        expected.assert_eq(&graphiql_source);
379    }
380
381    #[test]
382    fn test_with_all_options() {
383        let graphiql_source = GraphiQLSource::build()
384            .endpoint("/")
385            .subscription_endpoint("/ws")
386            .header("Authorization", "Bearer [token]")
387            .version("3.9.0")
388            .ws_connection_param("token", "[token]")
389            .title("Awesome GraphiQL IDE Test")
390            .credentials(Credentials::Include)
391            .finish();
392
393        let mut expected = expect![[r#"
394<!DOCTYPE html>
395<html lang="en">
396  <head>
397    <meta charset="UTF-8" />
398    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
399    <meta name="robots" content="noindex">
400    <meta name="referrer" content="origin">
401
402    
403      <title>Awesome GraphiQL IDE Test</title>
404    
405    
406    <style>
407      body {
408        margin: 0;
409      }
410
411      #graphiql {
412        height: 100dvh;
413      }
414
415      .loading {
416        height: 100%;
417        display: flex;
418        align-items: center;
419        justify-content: center;
420        font-size: 4rem;
421      }
422    </style>
423
424    <link rel="stylesheet" href="https://esm.sh/graphiql/dist/style.css" />
425    <link
426      rel="stylesheet"
427      href="https://esm.sh/@graphiql/plugin-explorer/dist/style.css"
428    />
429
430    <script type="importmap">
431      {
432        "imports": {
433          "react": "https://esm.sh/react@19.1.0",
434          "react/": "https://esm.sh/react@19.1.0/",
435
436          "react-dom": "https://esm.sh/react-dom@19.1.0",
437          "react-dom/": "https://esm.sh/react-dom@19.1.0/",
438
439          "graphiql": "https://esm.sh/graphiql@3.9.0?standalone&external=react,react-dom,@graphiql/react,graphql",
440          "graphiql/": "https://esm.sh/graphiql@3.9.0/",
441          "@graphiql/plugin-explorer": "https://esm.sh/@graphiql/plugin-explorer?standalone&external=react,@graphiql/react,graphql",
442          "@graphiql/react": "https://esm.sh/@graphiql/react?standalone&external=react,react-dom,graphql,@graphiql/toolkit,@emotion/is-prop-valid",
443
444          "@graphiql/toolkit": "https://esm.sh/@graphiql/toolkit?standalone&external=graphql",
445          "graphql": "https://esm.sh/graphql@16.11.0",
446          "@emotion/is-prop-valid": "data:text/javascript,"
447        }
448      }
449    </script>
450
451    <script type="module">
452      import React from 'react';
453      import ReactDOM from 'react-dom/client';
454      import { GraphiQL, HISTORY_PLUGIN } from 'graphiql';
455      import { createGraphiQLFetcher } from '@graphiql/toolkit';
456      import { explorerPlugin } from '@graphiql/plugin-explorer';
457      import 'graphiql/setup-workers/esm.sh';
458
459      const customFetch = (url, opts = {}) => {
460        return fetch(url, {...opts, credentials: 'include'})
461      }
462
463      const createUrl = (endpoint, subscription = false) => {
464        const url = new URL(endpoint, window.location.origin);
465        if (subscription) {
466          url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
467        }
468        return url.toString();
469      }
470
471      const fetcher = createGraphiQLFetcher({
472        url: createUrl('/'),
473        fetch: customFetch,
474        
475        subscriptionUrl: createUrl('/ws'),
476        
477        
478        headers: {
479          
480          'Authorization': 'Bearer [token]',
481          
482        }
483        
484        
485        wsConnectionParams: {
486          
487          'token': '[token]',
488          
489        }
490        
491      });
492      const plugins = [HISTORY_PLUGIN, explorerPlugin()];
493
494      function App() {
495        return React.createElement(GraphiQL, {
496          fetcher,
497          plugins,
498          defaultEditorToolsVisibility: true,
499        });
500      }
501
502      const container = document.getElementById('graphiql');
503      const root = ReactDOM.createRoot(container);
504      root.render(React.createElement(App));
505    </script>
506  </head>
507  <body>
508    <div id="graphiql">
509      <div class="loading">Loading…</div>
510    </div>
511  </body>
512</html>"#]];
513
514        expected.indent(false);
515        expected.assert_eq(&graphiql_source);
516    }
517}