1use std::collections::HashMap;
2
3use handlebars::Handlebars;
4use serde::Serialize;
5
6use crate::http::graphiql_plugin::GraphiQLPlugin;
7
8#[derive(Debug, Serialize, Default)]
12#[serde(rename_all = "kebab-case")]
13pub enum Credentials {
14 #[default]
17 SameOrigin,
18 Include,
20 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#[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 pub fn build() -> GraphiQLSource<'a> {
63 Default::default()
64 }
65
66 #[must_use]
68 pub fn endpoint(self, endpoint: &'a str) -> GraphiQLSource<'a> {
69 GraphiQLSource { endpoint, ..self }
70 }
71
72 pub fn subscription_endpoint(self, endpoint: &'a str) -> GraphiQLSource<'a> {
74 GraphiQLSource {
75 subscription_endpoint: Some(endpoint),
76 ..self
77 }
78 }
79
80 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 pub fn version(self, value: &'a str) -> GraphiQLSource<'a> {
92 GraphiQLSource {
93 version: GraphiQLVersion(value),
94 ..self
95 }
96 }
97
98 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 pub fn title(self, title: &'a str) -> GraphiQLSource<'a> {
110 GraphiQLSource {
111 title: Some(title),
112 ..self
113 }
114 }
115
116 pub fn credentials(self, credentials: Credentials) -> GraphiQLSource<'a> {
118 GraphiQLSource {
119 credentials,
120 ..self
121 }
122 }
123
124 pub fn plugins(self, plugins: &'a [GraphiQLPlugin]) -> GraphiQLSource<'a> {
126 GraphiQLSource { plugins, ..self }
127 }
128
129 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}