1use std::{collections::HashMap, fmt};
2
3use askama::Template;
4
5#[derive(Debug, Default)]
9pub enum Credentials {
10 #[default]
13 SameOrigin,
14 Include,
16 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#[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 pub fn build() -> GraphiQLSource<'a> {
74 Default::default()
75 }
76
77 #[must_use]
79 pub fn endpoint(self, endpoint: &'a str) -> GraphiQLSource<'a> {
80 GraphiQLSource { endpoint, ..self }
81 }
82
83 pub fn subscription_endpoint(self, endpoint: &'a str) -> GraphiQLSource<'a> {
85 GraphiQLSource {
86 subscription_endpoint: Some(endpoint),
87 ..self
88 }
89 }
90
91 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 pub fn version(self, value: &'a str) -> GraphiQLSource<'a> {
103 GraphiQLSource {
104 version: GraphiQLVersion(value),
105 ..self
106 }
107 }
108
109 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 pub fn title(self, title: &'a str) -> GraphiQLSource<'a> {
121 GraphiQLSource {
122 title: Some(title),
123 ..self
124 }
125 }
126
127 pub fn credentials(self, credentials: Credentials) -> GraphiQLSource<'a> {
129 GraphiQLSource {
130 credentials,
131 ..self
132 }
133 }
134
135 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}